Swift Combine - Создайте издателя для CoreLocation

Я только начал изучать комбайн, так что для меня это все еще немного нечеткое. Я хотел бы создать пользовательский Publisher, который будет использовать CLLocationManager для отображения текущего местоположения пользователя. Я бы хотел, чтобы он работал таким образом, чтобы locationManager начинал обновлять местоположение только тогда, когда есть подключенные подписчики. И после того, как все подписчики были удалены, отменены и т. Д., Он должен перестать обновлять местоположение. Возможно ли это сделать? Как мне создать такой publisher? Также это правильный подход или что-то не так?


person Damian Dudycz    schedule 23.11.2019    source источник
comment
Загляните сюда: heckj.github.io/swiftui-notes   -  person Adrian    schedule 23.11.2019
comment
Спасибо, я видел это, но это ведет себя немного иначе, чем хотелось бы. Вам нужно явно указать ему запускать обновления уведомлений, где я хотел бы, чтобы он автоматически запускал / останавливал обновление местоположения, в зависимости от того, есть ли какие-то подписчики, подключенные к издателю.   -  person Damian Dudycz    schedule 23.11.2019
comment
Проверьте это: github.com/broadwaylamb/OpenCombine, оттуда вы сможете реализовать свой собственный PassthroughSubject, расширяя существующий PassthroughSubject, чтобы сказать PassthroughLocationSubject, который содержит ссылку на диспетчер местоположения. Затем, когда вы добавляете первую подписку, вы запускаете диспетчер местоположения, и когда вы удаляете последнего подписчика, вы останавливаете диспетчер местоположения, и при каждом обновлении / сбое местоположения вы отправляете значение через метод send (). надеюсь, это поможет   -  person AntonijoDev    schedule 22.12.2019
comment
Я мог ошибаться, но я считаю, что, поскольку PassthroughSubject - это класс, его нельзя использовать с @State, как Timer.publish, который является структурой.   -  person malhal    schedule 24.09.2020


Ответы (2)


Основы того, что вы хотите, довольно просты. Есть пример из Использование Combine, в котором CoreLocation заключен в объект, который действует как прокси, возвращая издателя обновлений _ 1_.

Сам 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 для случаев, когда вам нужен явный контроль над обновлениями, хотя большинство издателей и операторов запускаются либо при создании издателя, либо при подписке.

В общем случае вариант ЕСЛИ лучше подходит для вашего варианта использования. В некоторых случаях вы создаете конвейеры и подписываетесь перед обновлением представлений - в этом случае отказ от запроса фоновых обновлений сэкономит вам некоторую вычислительную мощность и потребление энергии.

В примерах UIKit, в которых используется этот издатель CoreLocation, у меня также есть механизмы для проверки запрошенных разрешений для обновления местоположения, встроенных в пример контроллера представления: https://github.com/heckj/swiftui-notes/blob/master/UIKit-Combine/HeadingViewController.swift

person heckj    schedule 17.12.2019
comment
Если вы нацеливаетесь на пользовательский интерфейс с заголовком, вам лучше отформатировать строку в методе делегата в этом объекте и установить ее в опубликованном свойстве. Таким образом, форматирование выполняется только при изменении заголовка, а не при каждом обновлении пользовательского интерфейса, например если это был ObservableObject для SwiftUI. - person malhal; 21.09.2020

Я новичок в Combine, но вот моя попытка, которую я создал сегодня, так что это работа в процессе и, возможно, не было сделано правильно. Идея состоит в том, чтобы использовать 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.

person malhal    schedule 24.09.2020