Search code examples
swiftreactive-programmingcombine

How to terminate Swift Combine response after first emit?


I am requesting a single value from a publisher and would like to terminate after I get the response. Below I'm just deallocating the cancel token after the first time, is there a better way to do this?

extension MyInteractor {
    private static var locationPermissionToken: Cancellable?

    func requestLocationPermission(completion: @escaping (Result<Void, LocationError>) -> Void) {
        Self.locationPermissionToken = locationProxy.authorizationPublisher
            .sink { status in
                Self.locationPermissionToken = nil
                status ? completion(.success(())) : completion(.failure(.deniedLocationServices))
            }

        locationProxy.requestAuthorization()
    }
}

Solution

  • Considering that you have a custom Publisher, I would suggest modifying the implementation of that so that it sends a completion straight after it emitted a value.

    Since you are using a PassthroughSubject rather than a custom type conforming to Publisher, you can create a custom method that sends both a value and a completion. Then you need to call this method instead of calling authorizationSubject.send from inside your type.

    private func emitAndComplete(authorizationStatus: Bool) {
        Self.authorizationSubject.send(authorizationStatus)
        Self.authorizationSubject.send(completion: .finished)
    }
    

    Full modified code:

    @available(OSX 10.15, iOS 13, tvOS 13, watchOS 6, *)
    public class LocationProxy: NSObject {
        private lazy var manager = CLLocationManager()
    
        private static let authorizationSubject = PassthroughSubject<Bool, Never>()
        public private(set) lazy var authorizationPublisher: AnyPublisher<Bool, Never> = Self.authorizationSubject.eraseToAnyPublisher()
    
        var isAuthorized: Bool { CLLocationManager.isAuthorized }
    
        func requestAuthorization(for type: LocationAPI.AuthorizationType = .whenInUse) {
            // Handle authorized and exit
            guard !isAuthorized(for: type) else {
                emitAndComplete(authorizationStatus: true)
                return
            }
    
            // Request appropiate authorization before exit
            defer {
                #if os(macOS)
                if #available(OSX 10.15, *) {
                    manager.requestAlwaysAuthorization()
                }
                #elseif os(tvOS)
                manager.requestWhenInUseAuthorization()
                #else
                switch type {
                case .whenInUse:
                    manager.requestWhenInUseAuthorization()
                case .always:
                    manager.requestAlwaysAuthorization()
                }
                #endif
            }
    
            // Handle mismatched allowed and exit
            guard !isAuthorized else {
                // Process callback in case authorization dialog not launched by OS
                // since user will be notified first time only and ignored subsequently
                emitAndComplete(authorizationStatus: false)
                return
            }
    
            // Handle denied and exit
            guard CLLocationManager.authorizationStatus() == .notDetermined else {
                emitAndComplete(authorizationStatus: false)
                return
            }
        }
    
        public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
            guard status != .notDetermined else { return }
            emitAndComplete(authorizationStatus: isAuthorized)
        }
    
        private func emitAndComplete(authorizationStatus: Bool) {
            Self.authorizationSubject.send(authorizationStatus)
            Self.authorizationSubject.send(completion: .finished)
        }
    }