Swift Combine-为CoreLocation创建发布者

时间:2019-11-23 11:13:45

标签: swift reactive-programming core-location combine publisher

我才刚刚开始学习组合,所以对我还是有点模糊。我想创建一个自定义import tkinter as tk from tkinter import messagebox from collections import OrderedDict class ProjectInfo(tk.Frame): def __init__(self, parent, controller, *args, **kwargs): super().__init__(parent, *args, **kwargs) self.parent = parent self.controller = controller self.widgets_init() def widgets_init(self): tk.Label(self, text = "Rock Controller!", width = 10, anchor = "w", justify = "left").grid(row = 0, column = 0) tk.Label(self, text = "Input Name: ").grid(row = 1, column = 0) self.entry = tk.Entry(self) self.entry.grid(row = 1, column = 1) class ConfirmItems(tk.Frame): def __init__(self, parent, frames, controller, *args, **kwargs): super().__init__(parent, *args, **kwargs) self.parent = parent self.frames = frames self.controller = controller self.widgets_init() def update_entries(self): self.controller.project_info.name = self.controller.project_info.entry.get() def update_frames(self): self.message = 'Click Cancel go back to reset!\n' for key, values in self.frames.items(): for v in values: x = getattr(key, v) self.message += v + ': ' + str(x) + '\n' def show_settings(self): answer = tk.messagebox.askokcancel("Check Settings", self.message) if answer in ["yes", 1]: self.quit() def combine_funcs(self, *funcs): def combined_func(*args, **kwargs): for f in funcs: f(*args, **kwargs) return combined_func def widgets_init(self): self.cancel = tk.Button(self, text = "Cancel", command = self.quit) self.cancel.grid(row = 0, column = 0) self.submit = tk.Button(self, text = "Submit", command = self.combine_funcs( self.update_entries, self.update_frames, self.show_settings)) # command = lambda:[self.update_frames(), self.show_settings()] self.submit.grid(row = 0, column = 1) class MainWindow(tk.Frame): def __init__(self, parent, *args, **kwargs): super().__init__(parent, *args, **kwargs) self.parent = parent self.controller = self self.project_info = ProjectInfo(self.parent, self.controller) self.project_info.grid(row = 0) self.widgets_init() self.confirm_items = ConfirmItems(self.parent, self.frames, self.controller) self.confirm_items.grid(row = 1) def widgets_init(self): self.dict_list = [(self.project_info, ('name',))] self.frames = OrderedDict(self.dict_list) def main(): root = tk.Tk() root.title("Welcome to Controller World!") root.geometry("300x300") gui = MainWindow(root) root.mainloop() if __name__ == "__main__": main() ,它将使用Publisher来显示当前用户位置。我希望它以这种方式工作:CLLocationManager仅在连接了一些订户时才开始更新位置。并且在所有订户被删除,取消等之后,它应该停止更新位置。这可能吗?如何创建这样的locationManager?这也是正确的方法,还是存在问题?

2 个答案:

答案 0 :(得分:2)

我是Combine的新手,但是今天刚创建的my attempt是正在进行中的工作,因此可能没有正确完成。这个想法是使用CLLocationManager的设计方式,即在需要时使用多个实例。

// Requirements: a NSLocationWhenInUseUsageDescription entry in Info.plist and call requestWhenInUseAuthorization
  
import Foundation
import Combine
import CoreLocation

extension CLLocationManager {
    public static func publishLocation() -> LocationPublisher{
        return .init()
    }

    public struct LocationPublisher: Publisher {
        public typealias Output = CLLocation
        public typealias Failure = Never

        public func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
            let subscription = LocationSubscription(subscriber: subscriber)
            subscriber.receive(subscription: subscription)
        }
        
        final class LocationSubscription<S: Subscriber> : NSObject, CLLocationManagerDelegate, Subscription where S.Input == Output, S.Failure == Failure{
            var subscriber: S
            var locationManager = CLLocationManager()
            
            init(subscriber: S) {
                self.subscriber = subscriber
                super.init()
                locationManager.delegate = self
            }

            func request(_ demand: Subscribers.Demand) {
                locationManager.startUpdatingLocation()
                locationManager.requestWhenInUseAuthorization()
            }
            
            func cancel() {
                locationManager.stopUpdatingLocation()
            }
            
            func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
                for location in locations {
                    _ = subscriber.receive(location)
                }
            }
        }
    }
}

测试器

import SwiftUI
import CoreData
import CoreLocation
import Combine

class Locator : ObservableObject {
    @Published var location = CLLocation()
    var cancellable : AnyCancellable?
    init() {
        
    }
    
    func start(){
        cancellable = CLLocationManager.publishLocation()
            .assign(to: \.location, on: self)
    }
}

struct ContentView: View {
    
    @StateObject var locator = Locator()

    var body: some View {
        VStack {
            Text("Location \(locator.location)")
        }
        .onAppear(){
            locator.start()
        }
    }
}

接下来,我计划添加publishAuthorization并建立一个管道。我还想为init提供配置参数,这样,如果有多个订户,他们可以以相同的方式配置位置管理器,例如distanceFilter

答案 1 :(得分:0)

所需内容的基础非常简单。 Using Combine中有一个示例,该示例将CoreLocation与充当代理的对象包装在一起,返回了CLHeading更新的发布者。

CoreLocation本身不会自动启动或停止这种事情,在代理对象中,我复制了该模式以允许您手动启动和停止更新过程。

代码的核心位于https://github.com/heckj/swiftui-notes/blob/master/UIKit-Combine/LocationHeadingProxy.swift

import Foundation
import Combine
import CoreLocation

final class LocationHeadingProxy: NSObject, CLLocationManagerDelegate {
    let mgr: CLLocationManager
    private let headingPublisher: PassthroughSubject<CLHeading, Error>
    var publisher: AnyPublisher<CLHeading, Error>

    override init() {
        mgr = CLLocationManager()
        headingPublisher = PassthroughSubject<CLHeading, Error>()
        publisher = headingPublisher.eraseToAnyPublisher()

        super.init()
        mgr.delegate = self
    }

    func enable() {
        mgr.startUpdatingHeading()
    }

    func disable() {
        mgr.stopUpdatingHeading()
    }
    // MARK - delegate methods
    func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
        headingPublisher.send(newHeading)
    }

    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        headingPublisher.send(completion: Subscribers.Completion.failure(error))
    }
}

这不能完全满足您的要求,因为它不会在订阅后自动启动更新,但是我怀疑您可以扩展此功能以启​​用该功能。

到目前为止,我还没有通过实现协议所需的所有方法来研究自己的发布者,因此我没有扩展到该机制的细节。当您希望对更新进行明确控制时,尽管大多数发布者和运营商会在创建发布者或订阅时触发,但是Combine本身具有ConnectablePublisher的概念。

通常, IF 应该最好用您的用例来回答。在某些情况下,您需要先创建管道并进行订阅,然后再更新视图-如果是这种情况,那么推迟要求后台更新将为您节省一些处理能力和能耗。

在使用此CoreLocation发布者的UIKit示例中,我还具有适当的机制来验证请求的权限以允许位置更新,该机制嵌入在示例视图控制器中:https://github.com/heckj/swiftui-notes/blob/master/UIKit-Combine/HeadingViewController.swift