Closed npvisual closed 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.
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.
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))
}
}
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.
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.
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 ?