【问题标题】:Swift Combine - Create publisher for CoreLocationSwift Combine - 为 CoreLocation 创建发布者
【发布时间】:2019-11-23 11:13:45
【问题描述】:

我才开始学习结合,所以对我来说还是有点模糊。我想创建一个自定义Publisher,它将使用CLLocationManager 来公开当前用户位置。我希望它以这样的方式工作,locationManager 只有在连接了一些订阅者时才开始更新位置。在所有订阅者都被删除、取消等之后,它应该停止更新位置。这可能吗?如何创建这样的publisher?这也是正确的方法,还是有什么问题?

【问题讨论】:

  • 谢谢,我已经看到了,但它的行为与我想要的有点不同。您需要明确告诉它开始通知更新,我希望它自动开始/停止更新位置,具体取决于是否有一些订阅者连接到发布者。
  • 看看这个:github.com/broadwaylamb/OpenCombine,从那里你应该能够通过扩展现有的 PassthroughSubject 来实现你自己的 PassthroughSubject 说 PassthroughLocationSubject 包含对位置管理器的引用。然后,当您添加第一个订阅时,您启动位置管理器,当您删除最后一个订阅者时,您停止位置管理器,并且在每次位置更新/失败时,您通过 send() 方法发送值。希望有帮助
  • 我可能是错的,但我相信因为 PassthroughSubject 是一个类,它不能像 Timer.publish 一样与 @State 一起使用,它是一个结构。

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


【解决方案1】:

您想要的基础知识非常简单。 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 的概念,尽管大多数发布者和操作员都会在创建发布者或订阅时触发。

一般来说,如果你应该这样做最好由你的用例回答。在某些情况下,您会在更新视图之前创建管道和订阅 - 如果是这种情况,那么推迟请求后台更新会为您节省一些处理能力和能源消耗。

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

【讨论】:

  • 如果您以带有标题的 UI 为目标,您最好在此对象的委托方法中格式化您的字符串并将其设置在已发布的属性上。这样,格式仅在标题更改时完成,而不是在每次 UI 更新时完成,例如如果这是 SwiftUI 的 ObservableObject。
【解决方案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.

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-11-08
    • 1970-01-01
    • 2021-05-25
    • 1970-01-01
    • 2020-08-05
    • 2021-07-10
    • 2020-11-06
    • 1970-01-01
    相关资源
    最近更新 更多