SwiftRex / SwiftRex

Swift + Redux + (Combine|RxSwift|ReactiveSwift) -> SwiftRex
https://swiftrex.github.io/SwiftRex/
Apache License 2.0
624 stars 28 forks source link

Middleware sample for CoreLocation #72

Closed npvisual closed 4 years ago

npvisual commented 4 years ago

I am writing a Middleware specifically designed to handle CoreLocation and I was wondering if there were any existing samples for Middleware that get triggered by off-band effects (like a timer).

This is specifically to handle the case where the Middleware responds to an event not triggered by any action but rather by the location manager delegate methods. In other words nothing is coming from the dispatcher.

Also, would EffectMiddleware be the right tool for the job ?

luizmb commented 4 years ago

Your middleware must implement the CLLocatioManagerDelegate and dispatch an action every time something relevant happens. I recommend that you have at least one action for start monitoring CoreLocation and another for stopping. The start action will probably trigger the permission popup, that you can also handle using this middleware.

This is similar to my proposal, although it uses closure delegations instead of protocol, but the idea is the same: https://github.com/SwiftRex/MultipeerMiddleware/blob/master/Sources/MultipeerMiddleware/Connectivity/MultipeerConnectivityMiddleware.swift

EffectMiddleware requires that your effect is wrapped already in a Combine/RxSwift monad, so there would be a two-step implementation: first make a CLLocationManager wrapper using the reactive framework, and then use the observation from your middleware. It's my favourite way as it's easier to mock without replacing the middleware itself, but you should start with the direct approach first, the middleware is the CoreLocation wrapper itself.

npvisual commented 4 years ago

Your middleware must implement the CLLocatioManagerDelegate and dispatch an action every time something relevant happens.

Yes, definitely. No argument here :) .

EffectMiddleware requires that your effect is wrapped already in a Combine/RxSwift monad, so there would be a two-step implementation: first make a CLLocationManager wrapper using the reactive framework, and then use the observation from your middleware. It's my favourite way as it's easier to mock without replacing the middleware itself, but you should start with the direct approach first, the middleware is the CoreLocation wrapper itself.

Ok. I already have an existing wrapper but for a reactive framework that's not implemented in SwiftRex : ReactiveKit. I'll probably try and refactor it for Combine once I am done with the "direct approach" first.

Btw, you should check it out. I love ReactiveKit's simplicity and the work that was put into its "binding" companion, Bond -- by @srdanrasic.

luizmb commented 4 years ago

I would love to support OpenCombine, ReactiveKit and other frameworks. But unfortunately I can't do it in a short-term future, the 1.0 version is already super late and docs are behind. But I'm pretty sure you can easily implement this CoreLocation Middleware with not much effort.

This is a rough example but I'm pretty sure lots of CL edge cases are missing. But that's because of CoreLocation complexity, not redux on your way, meaning that you're probably good to go from here and make it more robust.

If you do it, a SwiftRex/CoreLocationMiddleware repo can be created and I add you as owner, in case you're interested.

import CoreLocation

enum LocationState {
    case unknown
    case notAuthorized
    case authorized(lastPosition: CLLocation?)
}

enum LocationAction {
    case startMonitoring
    case stopMonitoring
    case gotPosition(CLLocation)
    case authorized
    case unauthorized
    case authorizationUnknown
    case receiveError(Error)
}

let locationReducer = Reducer<LocationAction, LocationState> { action, state in
    var state = state
    switch action {
    case .authorized:
        if case .authorized = state { return state }
        state = .authorized(lastPosition: nil)
    case .startMonitoring, .stopMonitoring:
        break
    case let .gotPosition(position):
        state = .authorized(lastPosition: position)
    case .unauthorized:
        state = .notAuthorized
    case .authorizationUnknown:
        state = .unknown
    case .receiveError:
        break
    }

    return state
}

class LocationMiddleware: NSObject, Middleware {
    private var getState: GetState<LocationState>?
    private var output: AnyActionHandler<LocationAction>?
    private let manager = CLLocationManager()

    func receiveContext(getState: @escaping GetState<LocationState>, output: AnyActionHandler<LocationAction>) {
        self.getState = getState
        self.output = output
        manager.delegate = self
    }
    func handle(action: LocationAction, from dispatcher: ActionSource, afterReducer: inout AfterReducer) {
        switch action {
        case .startMonitoring, .authorized:
            startMonitoring()
        case .stopMonitoring:
            stopMonitoring()
        default: return
        }
    }
    func startMonitoring() {
        switch getState?() {
        case .authorized?:
            manager.startUpdatingLocation()
        default:
            // requestAlwaysAuthorization or requestWhenInUseAuthorization could be decided on
            // the middleware init or as payload for startMonitoring
            manager.requestAlwaysAuthorization()
        }
    }
    func stopMonitoring() {
        manager.stopUpdatingLocation()
    }
}
extension LocationMiddleware: CLLocationManagerDelegate {
    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        switch status {
        case .notDetermined:
            output?.dispatch(.authorizationUnknown)
        case .denied, .restricted:
            output?.dispatch(.unauthorized)
        case .authorizedAlways, .authorizedWhenInUse:
            output?.dispatch(.authorized)
        @unknown default:
            return
        }
    }
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let last = locations.last else { return }
        output?.dispatch(.gotPosition(last))
    }
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        output?.dispatch(.receiveError(error))
    }
}
npvisual commented 4 years ago

Awesome !

If you do it, a SwiftRex/CoreLocationMiddleware repo can be created and I add you as owner, in case you're interested.

Sure ! I am trying to make it as complete as possible since I am using about 2/3 of the use cases that CoreLocation offers. So shouldn't be a big deal to add the last tier.

luizmb commented 4 years ago

Thanks @npvisual for the amazing work on https://github.com/SwiftRex/CoreLocationMiddleware and https://github.com/npvisual/CoreLocation-Redux

I'll archive this issue now, as it seems to be very well addressed by your examples at this point.