Best Reactive-Cocoa approach for writing a CLLocationManagerDelegate, which will infrequently fetch location

StackOverflow https://stackoverflow.com/questions/22087276

  •  18-10-2022
  •  | 
  •  

Frage

Background

I'm really excited by the ReactiveCocoa framework and the potential it has, so I've decided that I'm going to bite the bullet and write my first app using it. In my app, I've already written the various services and delegates, but I now need to 'Reactive-Cocoa-ise' them so that I can get on with actual GUI side of things.

That said, so that I better understand this, I'm writing a simple bit of code just to try out concepts. In this case, writing a wrapper for CLLocationManagerDelegate.

In the actual app, the use case would be this:

  • 1) When the app is loaded up (viewDidLoad) then 2) Attempt to fetch the location of the device by
  • 2.1) if location services not enabled then
  • 2.1.1) check authorisation status, and if allowed to startMonitoringSignificantLocationChanges,
  • 2.1.2) else return an error
  • 2.2) else (location services are enabled)
  • 2.2.1) if the location manager last location was 'recent' (6 hours or less) return that
  • 2.2.2) else startMonitoringSignificantLocationChanges
  • 3) when returning the location (either straight away, or via locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations), then we also stopMonitoringSignificantLocationChanges
  • 4) If code that calls on the LocationManagerDelegate receives and error, then ask the delegate for a fixed 'default' value
  • 5) Next piece of code then uses the location (either fetched, or default) to go off and do a bunch of calls on third party services (calls to WebServices etc)

You can see a test harness at https://github.com/ippoippo/reactive-core-location-test

Concerns

The harness 'works' in that it goes off and fetches the location. However, I'm really concerned I'm doing this in the right way.

Consider this code

RACSignal *fetchLocationSignal = [lmDelegate latestLocationSignal];

RACSignal *locationSignal = [fetchLocationSignal catch:^RACSignal *(NSError *error) {
    NSLog(@"Unable to fetch location, going to grab default instead: %@", error);
    CLLocation *defaultLocation = [lmDelegate defaultLocation];
    NSLog(@"defaultLocation = [%@]", defaultLocation);
    // TODO OK, so now what. I just want to handle the error and 
    // pass back this default location as if nothing went wrong???
    return [RACSignal empty];
}];

NSLog(@"Created signal, about to bind on self.locationLabel text");

RAC(self.locationLabel, text) = [[locationSignal filter:^BOOL(CLLocation *newLocation) {
    NSLog(@"Doing filter first, newLocation = [%@]", newLocation);
    return newLocation != nil;
}] map:^(CLLocation *newLocation) {
    NSLog(@"Going to return the coordinates in map function");
    return [NSString stringWithFormat:@"%d, %d", newLocation.coordinate.longitude, newLocation.coordinate.latitude];
}];

Questions

  • 1) How do I handle errors? I can use catch which then gives me the opportunity to then ask the delegate for a default location. But, I'm then stuck on how to then pass back that default location as a signal? Or do I just change my defaultLocation method to return a RACSignal rather than CLLocation??
  • 2) When I return the location, should it be done as sendNext or sendCompleted? Currently it's coded as sendNext, but it seems like something that would be done as sendCompleted.
  • 3) Actually, does the answer to that depend on how I create the Delegate. This test app creates a new Delegate each time the view is loaded. Is that something I should do, I should I make the Delegate a singleton. If it's a singleton, then sendNext seems the right thing to do. But if would then imply that I move the code that I have in latestLocationSignal in my delegate into an init method instead?

Apologies for the rambling question(s), just seem to be going around in circles in my head.

War es hilfreich?

Lösung

Updating the answer above to ReactiveCocoa 4 and Swift 2.1:

import Foundation
import ReactiveCocoa

class SignalCollector: NSObject, CLLocationManagerDelegate {

let (signal,sink) = Signal<CLLocation, NoError>.pipe()

let locationManager = CLLocationManager()

  func start(){
    locationManager.delegate = self
    locationManager.desiredAccuracy = kCLLocationAccuracyBest
    locationManager.startUpdatingLocation()
  }

  func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {

    for item in locations {

        guard let location = item as CLLocation! else { return }         
        sink.sendNext(location)
    }
}

And to listen the events:

 var locationSignals : [CLLocation]  = []  
 signal.observeNext({ newLocation in
      locationSignals.append(newLocation)
    })

Andere Tipps

How do I handle errors? I can use catch which then gives me the opportunity to then ask the delegate for a default location.

You're so close. Use +[RACSignal return:] to create a signal which just sends your default location:

RACSignal *locationSignal = [fetchLocationSignal catch:^RACSignal *(NSError *error) {
    NSLog(@"Unable to fetch location, going to grab default instead: %@", error);
    CLLocation *defaultLocation = [lmDelegate defaultLocation];
    NSLog(@"defaultLocation = [%@]", defaultLocation);
    return [RACSignal return:defaultLocation];
}];

When I return the location, should it be done as sendNext or sendCompleted? Currently it's coded as sendNext, but it seems like something that would be done as sendCompleted.

In order to actually send data, you'll need to use -sendNext:. If that's the only thing that the signal is sending, then it should also -sendCompleted after that. But if it's going to continue to send the location as accuracy improves or location changes, then it shouldn't complete yet.

More generally, signals can send any numbers of nexts (0*) but can only complete or error. Once completion or error is sent, the signal's done. It won't send any more values.

Actually, does the answer to that depend on how I create the Delegate. This test app creates a new Delegate each time the view is loaded. Is that something I should do, I should I make the Delegate a singleton. If it's a singleton, then sendNext seems the right thing to do. But if would then imply that I move the code that I have in latestLocationSignal in my delegate into an init method instead?

I'm not sure I have enough context to answer this, except to say that singletons are never the answer ;) Creating a new delegate each time the view is loaded seems reasonable to me.

It's hard to answer broad questions like this because I don't know your design nearly as well as you do. Hopefully these answers help a bit. If you need more clarification, specific examples are really helpful.

Seems to be super easy in ReactiveCocoa 3.0 and swift :

import ReactiveCocoa
import LlamaKit

class SignalCollector: NSObject, CLLocationManagerDelegate {

    let (signal,sink) = Signal<CLLocation, NoError>.pipe()

    let locationManager = CLLocationManager()

    func start(){
        locationManager.delegate = self
        locationManager.desiredAccuracy = kCLLocationAccuracyBest
        locationManager.startUpdatingLocation()
    }

    func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
        for item in locations {
            if let location = item as? CLLocation {
                sink.put(Event.Next(Box(location))
            }
        }
    }

}

And wherever you need to listen to the location events, use the signal with an observer :

var locationSignals : [CLLocation]  = []
signal.observe(next: { value in locationSignals.append(value)})

Note that since it's pre-alpha at the moment the syntax is likely to change.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top