diff --git a/LocationManagerExample/LocationManagerExample/Base.lproj/Main.storyboard b/LocationManagerExample/LocationManagerExample/Base.lproj/Main.storyboard index 203753d..c9472ec 100644 --- a/LocationManagerExample/LocationManagerExample/Base.lproj/Main.storyboard +++ b/LocationManagerExample/LocationManagerExample/Base.lproj/Main.storyboard @@ -1,7 +1,8 @@ - + - + + @@ -13,14 +14,14 @@ @@ -59,7 +60,7 @@ - + @@ -73,26 +74,40 @@ - + - @@ -100,9 +115,11 @@ + + diff --git a/LocationManagerExample/LocationManagerExample/INTUViewController.m b/LocationManagerExample/LocationManagerExample/INTUViewController.m index 1bea980..bcc6697 100644 --- a/LocationManagerExample/LocationManagerExample/INTUViewController.m +++ b/LocationManagerExample/LocationManagerExample/INTUViewController.m @@ -30,7 +30,9 @@ @interface INTUViewController () @property (weak, nonatomic) IBOutlet UILabel *statusLabel; +@property (weak, nonatomic) IBOutlet UISwitch *subscriptionSwitch; @property (weak, nonatomic) IBOutlet UILabel *timeoutLabel; +@property (weak, nonatomic) IBOutlet UILabel *desiredAccuracyLabel; @property (weak, nonatomic) IBOutlet UISegmentedControl *desiredAccuracyControl; @property (weak, nonatomic) IBOutlet UISlider *timeoutSlider; @property (weak, nonatomic) IBOutlet UIButton *requestCurrentLocationButton; @@ -41,7 +43,7 @@ @interface INTUViewController () @property (assign, nonatomic) INTULocationAccuracy desiredAccuracy; @property (assign, nonatomic) NSTimeInterval timeout; -@property (assign, nonatomic) NSInteger locationRequestID; +@property (assign, nonatomic) INTULocationRequestID locationRequestID; @end @@ -51,6 +53,7 @@ - (void)viewDidLoad { [super viewDidLoad]; + self.subscriptionSwitch.on = NO; self.desiredAccuracyControl.selectedSegmentIndex = 0; self.desiredAccuracy = INTULocationAccuracyCity; self.timeoutSlider.value = 10.0; @@ -61,44 +64,89 @@ - (void)viewDidLoad } /** - Callback when the "Request Current Location" button is tapped. + Starts a new subscription for location updates. */ -- (IBAction)startLocationRequest:(id)sender +- (void)startLocationUpdateSubscription +{ + __weak __typeof(self) weakSelf = self; + INTULocationManager *locMgr = [INTULocationManager sharedInstance]; + self.locationRequestID = [locMgr subscribeToLocationUpdatesWithBlock:^(CLLocation *currentLocation, INTULocationAccuracy achievedAccuracy, INTULocationStatus status) { + __typeof(weakSelf) strongSelf = weakSelf; + + if (status == INTULocationStatusSuccess) { + // A new updated location is available in currentLocation, and achievedAccuracy indicates how accurate this particular location is + strongSelf.statusLabel.text = [NSString stringWithFormat:@"Subscription block called with Current Location:\n%@", currentLocation]; + } + else { + // An error occurred, which causes the subscription to cancel automatically (this block will not execute again unless it is used to start a new subscription). + strongSelf.locationRequestID = NSNotFound; + + if (status == INTULocationStatusServicesNotDetermined) { + strongSelf.statusLabel.text = @"Error: User has not responded to the permissions alert."; + } else if (status == INTULocationStatusServicesDenied) { + strongSelf.statusLabel.text = @"Error: User has denied this app permissions to access device location."; + } else if (status == INTULocationStatusServicesRestricted) { + strongSelf.statusLabel.text = @"Error: User is restricted from using location services by a usage policy."; + } else if (status == INTULocationStatusServicesDisabled) { + strongSelf.statusLabel.text = @"Error: Location services are turned off for all apps on this device."; + } else { + strongSelf.statusLabel.text = @"An unknown error occurred.\n(Are you using iOS Simulator with location set to 'None'?)"; + } + } + }]; +} + +/** + Starts a new one-time request for the current location. + */ +- (void)startSingleLocationRequest { __weak __typeof(self) weakSelf = self; - INTULocationManager *locMgr = [INTULocationManager sharedInstance]; self.locationRequestID = [locMgr requestLocationWithDesiredAccuracy:self.desiredAccuracy timeout:self.timeout delayUntilAuthorized:YES - block:^(CLLocation *currentLocation, INTULocationAccuracy achievedAccuracy, INTULocationStatus status) { - __typeof(weakSelf) strongSelf = weakSelf; - - if (status == INTULocationStatusSuccess) { - // achievedAccuracy is at least the desired accuracy (potentially better) - strongSelf.statusLabel.text = [NSString stringWithFormat:@"Location request successful! Current Location:\n%@", currentLocation]; - } - else if (status == INTULocationStatusTimedOut) { - // You may wish to inspect achievedAccuracy here to see if it is acceptable, if you plan to use currentLocation - strongSelf.statusLabel.text = [NSString stringWithFormat:@"Location request timed out. Current Location:\n%@", currentLocation]; - } - else { - // An error occurred - if (status == INTULocationStatusServicesNotDetermined) { - strongSelf.statusLabel.text = @"Error: User has not responded to the permissions alert."; - } else if (status == INTULocationStatusServicesDenied) { - strongSelf.statusLabel.text = @"Error: User has denied this app permissions to access device location."; - } else if (status == INTULocationStatusServicesRestricted) { - strongSelf.statusLabel.text = @"Error: User is restricted from using location services by a usage policy."; - } else if (status == INTULocationStatusServicesDisabled) { - strongSelf.statusLabel.text = @"Error: Location services are turned off for all apps on this device."; - } else { - strongSelf.statusLabel.text = @"An unknown error occurred.\n(Are you using iOS Simulator with location set to 'None'?)"; - } - } - - strongSelf.locationRequestID = NSNotFound; - }]; + block: + ^(CLLocation *currentLocation, INTULocationAccuracy achievedAccuracy, INTULocationStatus status) { + __typeof(weakSelf) strongSelf = weakSelf; + + if (status == INTULocationStatusSuccess) { + // achievedAccuracy is at least the desired accuracy (potentially better) + strongSelf.statusLabel.text = [NSString stringWithFormat:@"Location request successful! Current Location:\n%@", currentLocation]; + } + else if (status == INTULocationStatusTimedOut) { + // You may wish to inspect achievedAccuracy here to see if it is acceptable, if you plan to use currentLocation + strongSelf.statusLabel.text = [NSString stringWithFormat:@"Location request timed out. Current Location:\n%@", currentLocation]; + } + else { + // An error occurred + if (status == INTULocationStatusServicesNotDetermined) { + strongSelf.statusLabel.text = @"Error: User has not responded to the permissions alert."; + } else if (status == INTULocationStatusServicesDenied) { + strongSelf.statusLabel.text = @"Error: User has denied this app permissions to access device location."; + } else if (status == INTULocationStatusServicesRestricted) { + strongSelf.statusLabel.text = @"Error: User is restricted from using location services by a usage policy."; + } else if (status == INTULocationStatusServicesDisabled) { + strongSelf.statusLabel.text = @"Error: Location services are turned off for all apps on this device."; + } else { + strongSelf.statusLabel.text = @"An unknown error occurred.\n(Are you using iOS Simulator with location set to 'None'?)"; + } + } + + strongSelf.locationRequestID = NSNotFound; + }]; +} + +/** + Callback when the "Request Current Location" or "Start Subscription" button is tapped. + */ +- (IBAction)startButtonTapped:(id)sender +{ + if (self.subscriptionSwitch.on) { + [self startLocationUpdateSubscription]; + } else { + [self startSingleLocationRequest]; + } } /** @@ -107,6 +155,12 @@ - (IBAction)startLocationRequest:(id)sender - (IBAction)forceCompleteRequest:(id)sender { [[INTULocationManager sharedInstance] forceCompleteLocationRequest:self.locationRequestID]; + if (self.subscriptionSwitch.on) { + // Clear the location request ID, since this will not be handled inside the subscription block + // (This is not necessary for regular one-time location requests, since they will handle this inside the completion block.) + self.locationRequestID = NSNotFound; + self.statusLabel.text = @"Subscription canceled."; + } } /** @@ -116,7 +170,24 @@ - (IBAction)cancelRequest:(id)sender { [[INTULocationManager sharedInstance] cancelLocationRequest:self.locationRequestID]; self.locationRequestID = NSNotFound; - self.statusLabel.text = @"Location request cancelled."; + self.statusLabel.text = self.subscriptionSwitch.on ? @"Subscription canceled." : @"Location request canceled."; +} + +- (IBAction)subscriptionSwitchChanged:(UISwitch *)sender +{ + self.desiredAccuracyControl.userInteractionEnabled = !sender.on; + self.timeoutSlider.userInteractionEnabled = !sender.on; + + CGFloat alpha = sender.on ? 0.2 : 1.0; + [UIView animateWithDuration:0.3 animations:^{ + self.desiredAccuracyLabel.alpha = alpha; + self.desiredAccuracyControl.alpha = alpha; + self.timeoutLabel.alpha = alpha; + self.timeoutSlider.alpha = alpha; + }]; + + NSString *requestLocationButtonTitle = sender.on ? @"Start Subscription" : @"Request Current Location"; + [self.requestCurrentLocationButton setTitle:requestLocationButtonTitle forState:UIControlStateNormal]; } - (IBAction)desiredAccuracyControlChanged:(UISegmentedControl *)sender @@ -145,22 +216,29 @@ - (IBAction)desiredAccuracyControlChanged:(UISegmentedControl *)sender - (IBAction)timeoutSliderChanged:(UISlider *)sender { self.timeout = round(sender.value); - self.timeoutLabel.text = [NSString stringWithFormat:@"Timeout: %ld seconds", (long)self.timeout]; + if (self.timeout == 0) { + self.timeoutLabel.text = [NSString stringWithFormat:@"Timeout: 0 seconds (no limit)"]; + } else if (self.timeout == 1) { + self.timeoutLabel.text = [NSString stringWithFormat:@"Timeout: 1 second"]; + } else { + self.timeoutLabel.text = [NSString stringWithFormat:@"Timeout: %ld seconds", (long)self.timeout]; + } } /** Implement the setter for locationRequestID in order to update the UI as needed. */ -- (void)setLocationRequestID:(NSInteger)locationRequestID +- (void)setLocationRequestID:(INTULocationRequestID)locationRequestID { _locationRequestID = locationRequestID; BOOL isProcessingLocationRequest = (locationRequestID != NSNotFound); + self.subscriptionSwitch.enabled = !isProcessingLocationRequest; self.desiredAccuracyControl.enabled = !isProcessingLocationRequest; self.timeoutSlider.enabled = !isProcessingLocationRequest; self.requestCurrentLocationButton.enabled = !isProcessingLocationRequest; - self.forceCompleteRequestButton.enabled = isProcessingLocationRequest; + self.forceCompleteRequestButton.enabled = isProcessingLocationRequest && !self.subscriptionSwitch.on; self.cancelRequestButton.enabled = isProcessingLocationRequest; if (isProcessingLocationRequest) { diff --git a/README.md b/README.md index a96ee50..f4e96e9 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # [![INTULocationManager](https://github.com/intuit/LocationManager/blob/master/Images/INTULocationManager.png?raw=true)](#) INTULocationManager makes it easy to get the device's current location on iOS. -INTULocationManager provides a block-based asynchronous API to request the current location. It internally manages multiple simultaneous location requests, and each request specifies its own desired accuracy level and timeout duration. INTULocationManager automatically starts location services when the first request comes in, and stops location services once all requests have been completed. +INTULocationManager provides a block-based asynchronous API to request the current location, either once or continuously. It internally manages multiple simultaneous location requests, and each one-time request can specify its own desired accuracy level and timeout duration. INTULocationManager automatically starts location services when the first request comes in, and stops location services as soon as all requests have been completed to conserve power. ## What's wrong with CLLocationManager? -The CLLocationManager API works best when you need to track changes in the user's location over time, such as for turn-by-turn GPS navigation apps. However, requesting one-off location updates is a common task for many apps, such as when you want to autofill an address from the current location, or determine which city the user is currently in. If you just need to ask "Where am I?" every now and then, CLLocationManager is fairly difficult to work with because you must check for and handle a variety of things like user permissions, stale/inaccurate locations, errors, and more. +CLLocationManager requires you to manually detect and handle things like permissions, stale/inaccurate locations, errors, and more. CLLocationManager uses a more traditional delegate pattern instead of the modern block-based callback pattern. And while it works fine to track changes in the user's location over time (such as for turn-by-turn navigation), it is extremely cumbersome to correctly request a single location update (such as to determine the user's current city to get a weather forecast, or to autofill an address from the current location). -INTULocationManager makes it easy to request the device's current location, with a simple API that allows you to specify how accurate of a location you need, and how long you're willing to wait to get it. INTULocationManager is power efficient and conserves the user's battery by powering down location services (e.g. GPS) as soon as they are no longer needed. +INTULocationManager makes it easy to request the device's current location, either once or continuously. The API is extremely simple for both one-time requests and subscriptions. For one-time location requests, you can specify how accurate of a location you need, and how long you're willing to wait to get it. INTULocationManager is power efficient and conserves the user's battery by powering down location services (e.g. GPS) as soon as they are no longer needed. ## Usage @@ -19,7 +19,7 @@ For iOS 6 & 7, it is recommended that you provide a description for how your app #### iOS 8 Starting with iOS 8, you **must** provide a description for how your app uses location services by setting a string for the key [`NSLocationWhenInUseUsageDescription`](https://developer.apple.com/library/prerelease/ios/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html#//apple_ref/doc/uid/TP40009251-SW26) or [`NSLocationAlwaysUsageDescription`](https://developer.apple.com/library/prerelease/ios/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html#//apple_ref/doc/uid/TP40009251-SW18) in your app's `Info.plist` file. INTULocationManager determines which level of permissions to request based on which description key is present. You should only request the minimum permission level that your app requires, therefore it is recommended that you use the "When In Use" level unless you require more access. If you provide values for both description keys, the more permissive "Always" level is requested. -### Getting the Current Location +### Getting the Current Location (once) To get the device's current location, use the method `requestLocationWithDesiredAccuracy:timeout:block:`. The `desiredAccuracy` parameter specifies how **accurate and recent** of a location you need. The possible values are: @@ -57,12 +57,31 @@ INTULocationManager *locMgr = [INTULocationManager sharedInstance]; }]; ``` -### Managing In-Progress Requests +### Subscribing to Continuous Location Updates +To subscribe to continuous location updates, use the method `subscribeToLocationUpdatesWithBlock:`. The block will execute indefinitely (until canceled), once for every new updated location regardless of its accuracy. + +If an error occurs, the block will execute with a status other than INTULocationStatusSuccess, and the subscription will be canceled automatically. + +Here's an example: +```objective-c +INTULocationManager *locMgr = [INTULocationManager sharedInstance]; +[locMgr subscribeToLocationUpdatesWithBlock:^(CLLocation *currentLocation, INTULocationAccuracy achievedAccuracy, INTULocationStatus status) { + if (status == INTULocationStatusSuccess) { + // A new updated location is available in currentLocation, and achievedAccuracy indicates how accurate this particular location is. + } + else { + // An error occurred, more info is available by looking at the specific status returned. The subscription has been automatically canceled. + } +}]; +``` + +### Managing In-Progress Requests or Subscriptions When issuing a location request, you can optionally store the request ID, which allows you to force complete or cancel the request at any time: ```objective-c -NSInteger requestID = [[INTULocationManager sharedInstance] requestLocationWithDesiredAccuracy:INTULocationAccuracyHouse - timeout:5.0 - block:locationRequestBlock]; +INTULocationManager *locMgr = [INTULocationManager sharedInstance]; +INTULocationRequestID requestID = [locMgr requestLocationWithDesiredAccuracy:INTULocationAccuracyHouse + timeout:5.0 + block:locationRequestBlock]; // Force the request to complete early, like a manual timeout (will execute the block) [[INTULocationManager sharedInstance] forceCompleteLocationRequest:requestID]; @@ -71,8 +90,10 @@ NSInteger requestID = [[INTULocationManager sharedInstance] requestLocationWithD [[INTULocationManager sharedInstance] cancelLocationRequest:requestID]; ``` +Note that subscriptions never timeout; calling `forceCompleteLocationRequest:` on a subscription will simply cancel it. + ## Example Project -An [example project](https://github.com/intuit/LocationManager/tree/master/LocationManagerExample) is provided. It requires Xcode 5 and iOS 7.0 or later. Please note that it can run in the iOS Simulator, but you need to go to the iOS Simulator's **Debug > Location** menu once running the app to simulate a location (the default is **None**). +An [example project](LocationManagerExample) is provided. It requires Xcode 5 and iOS 7.0 or later. Please note that it can run in the iOS Simulator, but you need to go to the iOS Simulator's **Debug > Location** menu once running the app to simulate a location (the default is **None**). ## Installation *INTULocationManager requires iOS 6.0 or later.* @@ -88,7 +109,7 @@ An [example project](https://github.com/intuit/LocationManager/tree/master/Locat **Manually from GitHub** -1. Download all the files in the [Source directory](https://github.com/intuit/LocationManager/tree/master/Source). +1. Download all the files in the [Source directory](Source). 2. Add all the files to your Xcode project (drag and drop is easiest). 3. `#import "INTULocationManager.h"` wherever you want to use it. diff --git a/Source/INTULocationManager.h b/Source/INTULocationManager.h index 8ba5bfc..4f9306a 100644 --- a/Source/INTULocationManager.h +++ b/Source/INTULocationManager.h @@ -29,10 +29,7 @@ /** An abstraction around CLLocationManager that provides a block-based asynchronous API for obtaining the device's location. - - This class will automatically start and stop location services as needed to conserve battery. As a result, this class should - not be used in combination with any other code that directly uses the -[CLLocationManager startUpdatingLocation] or - -[CLLocationManager stopUpdatingLocation] methods. + This class will automatically start and stop system location services as needed to conserve battery. */ @interface INTULocationManager : NSObject @@ -47,14 +44,14 @@ @param desiredAccuracy The accuracy level desired (refers to the accuracy and recency of the location). @param timeout The maximum amount of time (in seconds) to wait for the desired accuracy before completing. - If this value is 0.0, no timeout will be set (will wait indefinitely for success, unless request is force completed or cancelled). + If this value is 0.0, no timeout will be set (will wait indefinitely for success, unless request is force completed or canceled). @param block The block to execute upon success, failure, or timeout. @return The location request ID, which can be used to force early completion or cancel the request while it is in progress. */ -- (NSInteger)requestLocationWithDesiredAccuracy:(INTULocationAccuracy)desiredAccuracy - timeout:(NSTimeInterval)timeout - block:(INTULocationRequestBlock)block; +- (INTULocationRequestID)requestLocationWithDesiredAccuracy:(INTULocationAccuracy)desiredAccuracy + timeout:(NSTimeInterval)timeout + block:(INTULocationRequestBlock)block; /** Asynchronously requests the current location of the device using location services, optionally delaying the timeout countdown until the user has @@ -62,7 +59,7 @@ @param desiredAccuracy The accuracy level desired (refers to the accuracy and recency of the location). @param timeout The maximum amount of time (in seconds) to wait for the desired accuracy before completing. - If this value is 0.0, no timeout will be set (will wait indefinitely for success, unless request is force completed or cancelled). + If this value is 0.0, no timeout will be set (will wait indefinitely for success, unless request is force completed or canceled). @param delayUntilAuthorized A flag specifying whether the timeout should only take effect after the user responds to the system prompt requesting permission for this app to access location services. If YES, the timeout countdown will not begin until after the app receives location services permissions. If NO, the timeout countdown begins immediately when calling this method. @@ -70,16 +67,28 @@ @return The location request ID, which can be used to force early completion or cancel the request while it is in progress. */ -- (NSInteger)requestLocationWithDesiredAccuracy:(INTULocationAccuracy)desiredAccuracy - timeout:(NSTimeInterval)timeout - delayUntilAuthorized:(BOOL)delayUntilAuthorized - block:(INTULocationRequestBlock)block; +- (INTULocationRequestID)requestLocationWithDesiredAccuracy:(INTULocationAccuracy)desiredAccuracy + timeout:(NSTimeInterval)timeout + delayUntilAuthorized:(BOOL)delayUntilAuthorized + block:(INTULocationRequestBlock)block; + +/** + Creates a subscription for location updates that will execute the block once per update indefinitely (until canceled), regardless of the accuracy of each location. + If an error occurs, the block will execute with a status other than INTULocationStatusSuccess, and the subscription will be canceled automatically. + + @param block The block to execute every time an updated location is available. + The status will be INTULocationStatusSuccess unless an error occurred; it will never be INTULocationStatusTimedOut. + + @return The location request ID, which can be used to cancel the subscription of location updates to this block. + */ +- (INTULocationRequestID)subscribeToLocationUpdatesWithBlock:(INTULocationRequestBlock)block; /** Immediately forces completion of the location request with the given requestID (if it exists), and executes the original request block with the results. - This is effectively a manual timeout, and will result in the request completing with status INTULocationStatusTimedOut. */ -- (void)forceCompleteLocationRequest:(NSInteger)requestID; + For one-time location requests, this is effectively a manual timeout, and will result in the request completing with status INTULocationStatusTimedOut. + If the requestID corresponds to a subscription, then the subscription will simply be canceled. */ +- (void)forceCompleteLocationRequest:(INTULocationRequestID)requestID; -/** Immediately cancels the location request with the given requestID (if it exists), without executing the original request block. */ -- (void)cancelLocationRequest:(NSInteger)requestID; +/** Immediately cancels the location request (or subscription) with the given requestID (if it exists), without executing the original request block. */ +- (void)cancelLocationRequest:(INTULocationRequestID)requestID; @end diff --git a/Source/INTULocationManager.m b/Source/INTULocationManager.m index 4fcdef7..508b591 100644 --- a/Source/INTULocationManager.m +++ b/Source/INTULocationManager.m @@ -28,29 +28,29 @@ #ifndef INTU_ENABLE_LOGGING - #ifdef DEBUG - #define INTU_ENABLE_LOGGING 1 - #else - #define INTU_ENABLE_LOGGING 0 - #endif /* DEBUG */ +# ifdef DEBUG +# define INTU_ENABLE_LOGGING 1 +# else +# define INTU_ENABLE_LOGGING 0 +# endif /* DEBUG */ #endif /* INTU_ENABLE_LOGGING */ #if INTU_ENABLE_LOGGING - #define INTULMLog(...) NSLog(@"INTULocationManager: %@", [NSString stringWithFormat:__VA_ARGS__]); +# define INTULMLog(...) NSLog(@"INTULocationManager: %@", [NSString stringWithFormat:__VA_ARGS__]); #else - #define INTULMLog(...) +# define INTULMLog(...) #endif /* INTU_ENABLE_LOGGING */ @interface INTULocationManager () -// The instance of CLLocationManager encapsulated by this class. +/** The instance of CLLocationManager encapsulated by this class. */ @property (nonatomic, strong) CLLocationManager *locationManager; -// The most recent current location, or nil if the current location is unknown, invalid, or stale. +/** The most recent current location, or nil if the current location is unknown, invalid, or stale. */ @property (nonatomic, strong) CLLocation *currentLocation; -// Whether or not the CLLocationManager is currently sending location updates. +/** Whether or not the CLLocationManager is currently sending location updates. */ @property (nonatomic, assign) BOOL isUpdatingLocation; -// Whether an error occurred during the last location update. +/** Whether an error occurred during the last location update. */ @property (nonatomic, assign) BOOL updateFailed; // An array of pending location requests in the form: @@ -106,7 +106,7 @@ - (BOOL)locationServicesAvailable @param desiredAccuracy The accuracy level desired (refers to the accuracy and recency of the location). @param timeout The maximum amount of time (in seconds) to wait for the desired accuracy before completing. - If this value is 0.0, no timeout will be set (will wait indefinitely for success, unless request is force completed or cancelled). + If this value is 0.0, no timeout will be set (will wait indefinitely for success, unless request is force completed or canceled). @param block The block to be executed when the request succeeds, fails, or times out. Three parameters are passed into the block: - The current location (the most recent one acquired, regardless of accuracy level), or nil if no valid location was acquired - The achieved accuracy for the current location (may be less than the desired accuracy if the request failed) @@ -114,9 +114,9 @@ - (BOOL)locationServicesAvailable @return The location request ID, which can be used to force early completion or cancel the request while it is in progress. */ -- (NSInteger)requestLocationWithDesiredAccuracy:(INTULocationAccuracy)desiredAccuracy - timeout:(NSTimeInterval)timeout - block:(INTULocationRequestBlock)block +- (INTULocationRequestID)requestLocationWithDesiredAccuracy:(INTULocationAccuracy)desiredAccuracy + timeout:(NSTimeInterval)timeout + block:(INTULocationRequestBlock)block { return [self requestLocationWithDesiredAccuracy:desiredAccuracy timeout:timeout @@ -130,7 +130,7 @@ - (NSInteger)requestLocationWithDesiredAccuracy:(INTULocationAccuracy)desiredAcc @param desiredAccuracy The accuracy level desired (refers to the accuracy and recency of the location). @param timeout The maximum amount of time (in seconds) to wait for the desired accuracy before completing. - If this value is 0.0, no timeout will be set (will wait indefinitely for success, unless request is force completed or cancelled). + If this value is 0.0, no timeout will be set (will wait indefinitely for success, unless request is force completed or canceled). @param delayUntilAuthorized A flag specifying whether the timeout should only take effect after the user responds to the system prompt requesting permission for this app to access location services. If YES, the timeout countdown will not begin until after the app receives location services permissions. If NO, the timeout countdown begins immediately when calling this method. @@ -141,12 +141,15 @@ - (NSInteger)requestLocationWithDesiredAccuracy:(INTULocationAccuracy)desiredAcc @return The location request ID, which can be used to force early completion or cancel the request while it is in progress. */ -- (NSInteger)requestLocationWithDesiredAccuracy:(INTULocationAccuracy)desiredAccuracy - timeout:(NSTimeInterval)timeout - delayUntilAuthorized:(BOOL)delayUntilAuthorized - block:(INTULocationRequestBlock)block +- (INTULocationRequestID)requestLocationWithDesiredAccuracy:(INTULocationAccuracy)desiredAccuracy + timeout:(NSTimeInterval)timeout + delayUntilAuthorized:(BOOL)delayUntilAuthorized + block:(INTULocationRequestBlock)block { - NSAssert(desiredAccuracy != INTULocationAccuracyNone, @"INTULocationAccuracyNone is not a valid desired accuracy."); + if (desiredAccuracy == INTULocationAccuracyNone) { + NSAssert(desiredAccuracy != INTULocationAccuracyNone, @"INTULocationAccuracyNone is not a valid desired accuracy."); + desiredAccuracy = INTULocationAccuracyCity; // default to the lowest valid desired accuracy + } INTULocationRequest *locationRequest = [[INTULocationRequest alloc] init]; locationRequest.delegate = self; @@ -164,11 +167,31 @@ - (NSInteger)requestLocationWithDesiredAccuracy:(INTULocationAccuracy)desiredAcc return locationRequest.requestID; } +/** + Creates a subscription for location updates that will execute the block once per update indefinitely (until canceled), regardless of the accuracy of each location. + If an error occurs, the block will execute with a status other than INTULocationStatusSuccess, and the subscription will be canceled automatically. + + @param block The block to execute every time an updated location is available. + The status will be INTULocationStatusSuccess unless an error occurred; it will never be INTULocationStatusTimedOut. + + @return The location request ID, which can be used to cancel the subscription of location updates to this block. + */ +- (INTULocationRequestID)subscribeToLocationUpdatesWithBlock:(INTULocationRequestBlock)block +{ + INTULocationRequest *locationRequest = [[INTULocationRequest alloc] init]; + locationRequest.desiredAccuracy = INTULocationAccuracyNone; // This makes the location request a subscription + locationRequest.block = block; + + [self addLocationRequest:locationRequest]; + + return locationRequest.requestID; +} + /** Immediately forces completion of the location request with the given requestID (if it exists), and executes the original request block with the results. This is effectively a manual timeout, and will result in the request completing with status INTULocationStatusTimedOut. */ -- (void)forceCompleteLocationRequest:(NSInteger)requestID +- (void)forceCompleteLocationRequest:(INTULocationRequestID)requestID { INTULocationRequest *locationRequestToComplete = nil; for (INTULocationRequest *locationRequest in self.locationRequests) { @@ -177,14 +200,19 @@ - (void)forceCompleteLocationRequest:(NSInteger)requestID break; } } - [locationRequestToComplete completeLocationRequest]; - [self completeLocationRequest:locationRequestToComplete]; + if (locationRequestToComplete.isSubscription) { + // Subscription requests can only be canceled + [self cancelLocationRequest:requestID]; + } else { + [locationRequestToComplete forceTimeout]; + [self completeLocationRequest:locationRequestToComplete]; + } } /** Immediately cancels the location request with the given requestID (if it exists), without executing the original request block. */ -- (void)cancelLocationRequest:(NSInteger)requestID +- (void)cancelLocationRequest:(INTULocationRequestID)requestID { INTULocationRequest *locationRequestToCancel = nil; for (INTULocationRequest *locationRequest in self.locationRequests) { @@ -194,8 +222,8 @@ - (void)cancelLocationRequest:(NSInteger)requestID } } [self.locationRequests removeObject:locationRequestToCancel]; - [locationRequestToCancel cancelLocationRequest]; - INTULMLog(@"Location Request cancelled with ID: %ld", (long)locationRequestToCancel.requestID); + [locationRequestToCancel cancel]; + INTULMLog(@"Location Request canceled with ID: %ld", (long)locationRequestToCancel.requestID); [self stopUpdatingLocationIfPossible]; } @@ -207,8 +235,7 @@ - (void)cancelLocationRequest:(NSInteger)requestID - (void)addLocationRequest:(INTULocationRequest *)locationRequest { if ([self locationServicesAvailable] == NO) { - // Don't even bother trying to do anything since location services are off or the user has - // explcitly denied us permission to use them + // Don't even bother trying to do anything since location services are off or the user has explcitly denied us permission to use them [self completeLocationRequest:locationRequest]; return; } @@ -294,8 +321,7 @@ - (void)processLocationRequests { CLLocation *mostRecentLocation = self.currentLocation; - // Keep a separate array of location requests to complete to avoid modifying the locationRequests property - // while iterating over it at the same time + // Keep a separate array of location requests to complete to avoid modifying the locationRequests property while iterating over it NSMutableArray *locationRequestsToComplete = [NSMutableArray array]; for (INTULocationRequest *locationRequest in self.locationRequests) { @@ -306,15 +332,22 @@ - (void)processLocationRequests } if (mostRecentLocation != nil) { - NSTimeInterval currentLocationTimeSinceUpdate = fabs([mostRecentLocation.timestamp timeIntervalSinceNow]); - CLLocationAccuracy currentLocationHorizontalAccuracy = mostRecentLocation.horizontalAccuracy; - NSTimeInterval staleThreshold = [locationRequest updateTimeStaleThreshold]; - CLLocationAccuracy horizontalAccuracyThreshold = [locationRequest horizontalAccuracyThreshold]; - if (currentLocationTimeSinceUpdate <= staleThreshold && - currentLocationHorizontalAccuracy <= horizontalAccuracyThreshold) { - // The request's desired accuracy has been reached, complete it - [locationRequestsToComplete addObject:locationRequest]; + if (locationRequest.isSubscription) { + // This is a subscription request, which lives indefinitely (unless manually canceled) and receives every location update we get + [self processSubscriptionRequest:locationRequest]; continue; + } else { + // This is a regular one-time location request + NSTimeInterval currentLocationTimeSinceUpdate = fabs([mostRecentLocation.timestamp timeIntervalSinceNow]); + CLLocationAccuracy currentLocationHorizontalAccuracy = mostRecentLocation.horizontalAccuracy; + NSTimeInterval staleThreshold = [locationRequest updateTimeStaleThreshold]; + CLLocationAccuracy horizontalAccuracyThreshold = [locationRequest horizontalAccuracyThreshold]; + if (currentLocationTimeSinceUpdate <= staleThreshold && + currentLocationHorizontalAccuracy <= horizontalAccuracyThreshold) { + // The request's desired accuracy has been reached, complete it + [locationRequestsToComplete addObject:locationRequest]; + continue; + } } } } @@ -348,14 +381,14 @@ - (void)completeLocationRequest:(INTULocationRequest *)locationRequest return; } + [locationRequest complete]; + [self.locationRequests removeObject:locationRequest]; + [self stopUpdatingLocationIfPossible]; + INTULocationStatus status = [self statusForLocationRequest:locationRequest]; CLLocation *currentLocation = self.currentLocation; INTULocationAccuracy achievedAccuracy = [self achievedAccuracyForLocation:currentLocation]; - [self.locationRequests removeObject:locationRequest]; - [locationRequest completeLocationRequest]; - [self stopUpdatingLocationIfPossible]; - // INTULocationManager is not thread safe and should only be called from the main thread, so we should already be executing on the main thread now. // dispatch_async is used to ensure that the completion block for a request is not executed before the request ID is returned, for example in the // case where the user has denied permission to access location services and the request is immediately completed with the appropriate error. @@ -368,6 +401,23 @@ - (void)completeLocationRequest:(INTULocationRequest *)locationRequest INTULMLog(@"Location Request completed with ID: %ld, currentLocation: %@, achievedAccuracy: %lu, status: %lu", (long)locationRequest.requestID, currentLocation, (unsigned long) achievedAccuracy, (unsigned long)status); } +/** + Handles calling a subscription location request's block with the current location. + */ +- (void)processSubscriptionRequest:(INTULocationRequest *)locationRequest +{ + NSAssert(locationRequest.isSubscription, @"This method should only be called for subscription location requests."); + + INTULocationStatus status = [self statusForLocationRequest:locationRequest]; + CLLocation *currentLocation = self.currentLocation; + INTULocationAccuracy achievedAccuracy = [self achievedAccuracyForLocation:currentLocation]; + + // No need for dispatch_async when calling this block, since this method is only called from a CLLocationManager callback + if (locationRequest.block) { + locationRequest.block(currentLocation, achievedAccuracy, status); + } +} + /** Returns the location manager status for the given location request. */ diff --git a/Source/INTULocationRequest.h b/Source/INTULocationRequest.h index 46b0523..13a55d6 100644 --- a/Source/INTULocationRequest.h +++ b/Source/INTULocationRequest.h @@ -50,27 +50,31 @@ */ @interface INTULocationRequest : NSObject -// The delegate for this location request. +/** The delegate for this location request. */ @property (nonatomic, weak) id delegate; -// The request ID for this location request (set during initialization). -@property (nonatomic, readonly) NSInteger requestID; -// The desired accuracy for this location request. +/** The request ID for this location request (set during initialization). */ +@property (nonatomic, readonly) INTULocationRequestID requestID; +/** Whether this is a subscription request (desired accuracy is INTULocationAccuracyNone). */ +@property (nonatomic, readonly) BOOL isSubscription; +/** The desired accuracy for this location request. + If set to INTULocationAccuracyNone, this will be a subscription request (executes block on each location update indefinitely until canceled). */ @property (nonatomic, assign) INTULocationAccuracy desiredAccuracy; -// The maximum amount of time the location request should be allowed to live before completing. -// If this value is exactly 0.0, it will be ignored (the request will never timeout by itself). +/** The maximum amount of time the location request should be allowed to live before completing. + If this value is exactly 0.0, it will be ignored (the request will never timeout by itself). */ @property (nonatomic, assign) NSTimeInterval timeout; -// How long the location request has been alive since the timeout value was last set. +/** How long the location request has been alive since the timeout value was last set. */ @property (nonatomic, readonly) NSTimeInterval timeAlive; -// Whether this location request has timed out (will also be YES if it has been completed). +/** Whether this location request has timed out (will also be YES if it has been completed). */ @property (nonatomic, readonly) BOOL hasTimedOut; -// The block to execute when the location request completes. +/** The block to execute when the location request completes. */ @property (nonatomic, copy) INTULocationRequestBlock block; /** Completes the location request. */ -- (void)completeLocationRequest; - +- (void)complete; +/** Forces the location request to consider itself timed out. */ +- (void)forceTimeout; /** Cancels the location request. */ -- (void)cancelLocationRequest; +- (void)cancel; /** Starts the location request's timeout timer if a nonzero timeout value is set, and the timer has not already been started. */ - (void)startTimeoutTimerIfNeeded; diff --git a/Source/INTULocationRequest.m b/Source/INTULocationRequest.m index 7a08dd1..8bb74e7 100644 --- a/Source/INTULocationRequest.m +++ b/Source/INTULocationRequest.m @@ -31,9 +31,9 @@ @interface INTULocationRequest () // Redeclare this property as readwrite for internal use. @property (nonatomic, assign, readwrite) BOOL hasTimedOut; -// The NSDate representing the time when the request started. Set when the |timeout| property is set. +/** The NSDate representing the time when the request started. Set when the |timeout| property is set. */ @property (nonatomic, strong) NSDate *requestStartTime; -// The timer that will fire to notify this request that it has timed out. Started when the |timeout| property is set. +/** The timer that will fire to notify this request that it has timed out. Started when the |timeout| property is set. */ @property (nonatomic, strong) NSTimer *timeoutTimer; @end @@ -41,12 +41,12 @@ @interface INTULocationRequest () @implementation INTULocationRequest -static NSInteger _nextRequestID = 0; +static INTULocationRequestID _nextRequestID = 0; /** Returns a unique request ID (within the lifetime of the application). */ -+ (NSInteger)getUniqueRequestID ++ (INTULocationRequestID)getUniqueRequestID { _nextRequestID++; return _nextRequestID; @@ -64,7 +64,7 @@ - (id)init Designated initializer. Use regular init method to autogenerate a unique requestID. */ -- (id)initWithRequestID:(NSInteger)requestID +- (id)initWithRequestID:(INTULocationRequestID)requestID { self = [super init]; if (self) { @@ -133,18 +133,28 @@ - (CLLocationAccuracy)horizontalAccuracyThreshold /** Completes the location request. */ -- (void)completeLocationRequest +- (void)complete { - self.hasTimedOut = YES; [self.timeoutTimer invalidate]; self.timeoutTimer = nil; self.requestStartTime = nil; } +/** + Forces the location request to consider itself timed out. + */ +- (void)forceTimeout +{ + if (self.desiredAccuracy > INTULocationAccuracyNone) { + // Only one-off location requests (not subscription requests) should ever be considered timed out + self.hasTimedOut = YES; + } +} + /** Cancels the location request. */ -- (void)cancelLocationRequest +- (void)cancel { [self.timeoutTimer invalidate]; self.timeoutTimer = nil; @@ -162,6 +172,14 @@ - (void)startTimeoutTimerIfNeeded } } +/** + Dynamic property that returns whether this is a subscription request (desired accuracy is INTULocationAccuracyNone). + */ +- (BOOL)isSubscription +{ + return self.desiredAccuracy == INTULocationAccuracyNone; +} + /** Dynamic property that returns how long the request has been alive (since the timeout value was set). */ diff --git a/Source/INTULocationRequestDefines.h b/Source/INTULocationRequestDefines.h index bb5b2f1..bfe5b4a 100644 --- a/Source/INTULocationRequestDefines.h +++ b/Source/INTULocationRequestDefines.h @@ -26,43 +26,60 @@ #ifndef INTU_LOCATION_REQUEST_DEFINES_H #define INTU_LOCATION_REQUEST_DEFINES_H -#define kINTUHorizontalAccuracyThresholdCity 5000.0 // in meters -#define kINTUHorizontalAccuracyThresholdNeighborhood 1000.0 // in meters -#define kINTUHorizontalAccuracyThresholdBlock 100.0 // in meters -#define kINTUHorizontalAccuracyThresholdHouse 15.0 // in meters -#define kINTUHorizontalAccuracyThresholdRoom 5.0 // in meters +static const CGFloat kINTUHorizontalAccuracyThresholdCity = 5000.0; // in meters +static const CGFloat kINTUHorizontalAccuracyThresholdNeighborhood = 1000.0; // in meters +static const CGFloat kINTUHorizontalAccuracyThresholdBlock = 100.0; // in meters +static const CGFloat kINTUHorizontalAccuracyThresholdHouse = 15.0; // in meters +static const CGFloat kINTUHorizontalAccuracyThresholdRoom = 5.0; // in meters -#define kINTUUpdateTimeStaleThresholdCity 600.0 // in seconds -#define kINTUUpdateTimeStaleThresholdNeighborhood 300.0 // in seconds -#define kINTUUpdateTimeStaleThresholdBlock 60.0 // in seconds -#define kINTUUpdateTimeStaleThresholdHouse 15.0 // in seconds -#define kINTUUpdateTimeStaleThresholdRoom 5.0 // in seconds +static const CGFloat kINTUUpdateTimeStaleThresholdCity = 600.0; // in seconds +static const CGFloat kINTUUpdateTimeStaleThresholdNeighborhood = 300.0; // in seconds +static const CGFloat kINTUUpdateTimeStaleThresholdBlock = 60.0; // in seconds +static const CGFloat kINTUUpdateTimeStaleThresholdHouse = 15.0; // in seconds +static const CGFloat kINTUUpdateTimeStaleThresholdRoom = 5.0; // in seconds -// An abstraction of both the horizontal accuracy and recency of location data. -// Room is the highest level of accuracy/recency; City is the lowest level. +/** A unique ID that corresponds to one location request. */ +typedef NSInteger INTULocationRequestID; + +/** An abstraction of both the horizontal accuracy and recency of location data. + Room is the highest level of accuracy/recency; City is the lowest level. */ typedef NS_ENUM(NSInteger, INTULocationAccuracy) { - /* Not valid as a desired accuracy. */ - INTULocationAccuracyNone = 0, // Inaccurate (>5000 meters, received >10 minutes ago) + // 'None' is not valid as a desired accuracy. + /** Inaccurate (>5000 meters, and/or received >10 minutes ago). */ + INTULocationAccuracyNone = 0, - /* These options are valid desired accuracies. */ - INTULocationAccuracyCity, // 5000 meters or better, received within the last 10 minutes -- lowest accuracy - INTULocationAccuracyNeighborhood, // 1000 meters or better, received within the last 5 minutes - INTULocationAccuracyBlock, // 100 meters or better, received within the last 1 minute - INTULocationAccuracyHouse, // 15 meters or better, received within the last 15 seconds - INTULocationAccuracyRoom, // 5 meters or better, received within the last 5 seconds -- highest accuracy + // The below options are valid desired accuracies. + /** 5000 meters or better, and received within the last 10 minutes. Lowest accuracy. */ + INTULocationAccuracyCity, + /** 1000 meters or better, and received within the last 5 minutes. */ + INTULocationAccuracyNeighborhood, + /** 100 meters or better, and received within the last 1 minute. */ + INTULocationAccuracyBlock, + /** 15 meters or better, and received within the last 15 seconds. */ + INTULocationAccuracyHouse, + /** 5 meters or better, and received within the last 5 seconds. Highest accuracy. */ + INTULocationAccuracyRoom, }; +/** Statuses that can be passed to the completion block of a location request. */ typedef NS_ENUM(NSInteger, INTULocationStatus) { - /* These statuses will accompany a valid location. */ - INTULocationStatusSuccess = 0, // got a location and desired accuracy level was achieved successfully - INTULocationStatusTimedOut, // got a location, but desired accuracy level was not reached before timeout + // These statuses will accompany a valid location. + /** Got a location and desired accuracy level was achieved successfully. */ + INTULocationStatusSuccess = 0, + /** Got a location, but the desired accuracy level was not reached before timeout. (Not applicable to subscriptions.) */ + INTULocationStatusTimedOut, - /* These statuses indicate some sort of error, and will accompany a nil location. */ - INTULocationStatusServicesNotDetermined, // user has not responded to the permissions dialog - INTULocationStatusServicesDenied, // user has explicitly denied this app permission to access location services - INTULocationStatusServicesRestricted, // user does not have ability to enable location services (e.g. parental controls, corporate policy, etc) - INTULocationStatusServicesDisabled, // user has turned off device-wide location services from system settings - INTULocationStatusError // an error occurred while using the system location services + // These statuses indicate some sort of error, and will accompany a nil location. + /** User has not responded to the permissions dialog. */ + INTULocationStatusServicesNotDetermined, + /** User has explicitly denied this app permission to access location services. */ + INTULocationStatusServicesDenied, + /** User does not have ability to enable location services (e.g. parental controls, corporate policy, etc). */ + INTULocationStatusServicesRestricted, + /** User has turned off device-wide location services from system settings. */ + INTULocationStatusServicesDisabled, + /** An error occurred while using the system location services. */ + INTULocationStatusError }; /**