I have location processing newly implemented in my app, testing in foreground and background with satisfactory results. The application is monitoring significant location changes as well as several regions. I have not yet figured out if I will get the same results when the app is suspended or terminated.
As I understand it, when the app is woken from these states it will be as if the app just started except the launchOptionsLocation key will be found in the launchOptions Dictionary param. My question is, can I allow the app delegate to proceed as normal and assume all is well? Is it necessary to intercept all the view setup code?
The very first lines in my didFinishLaunchingWithOptions:launchOptions method are:
NSManagedObjectContext *context = [self managedObjectContext];
if (!context) {
//Handle Error
}
self.sharedLocationHandler = [[[TTLocationHandler alloc] init] autorelease];
self.siteLogger = [[[ProjectSiteLogger alloc] initWithOptions:nil] autorelease];
self.siteLogger.locationHandler = self.sharedLocationHandler;
self.siteLogger.managedObjectContext = context;
In all likelihood, this covers all that I need in order to respond to the location event. I could easily test for the location key in launchOptions and skip the entire remainder of the method, though I am not sure what unforeseen complications that may entail.
I also question what would result then if a user happened to start the app intentionally while it was in that incomplete state having no views set up at all.
Is this something that has been tried, is it even necessary at all? I don't see how to test this as I don't know of a way to stay connected to Xcode debugger when the app is suspended.
---Additional Updated Info----
Initial testing on a day of carrying test phone around, my location processing seemed to do all the tasks I wanted it to with no changes to appDelegate. So I presume that wake from suspend/term came up and executed the full appDelegate procedure even though no view controllers ever became visible.
So, while it seems that it is not required to alter the startup procedures, may there still be a performance or battery concern that would make it prudent to cut the appDelegate procedure short and minimize processing?
After a good bit of testing and tweaking I find:
The app will wake from inactive or termed with no issues.
My location methods ran and completed regardless of the changes to the App Delegate.
When I cut the app delegate processes short, I did have intermittent start issues as anticipated.
Even though there were no apparent performance benefits, I went ahead and separated out the view setup code and added a flag so that it would not be run unless the application was being presented in the foreground. It just seems right to run no more processing than needed.
The code I ended up with is:
// Initialize a flag
BOOL needsViewsSetup = YES;
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
// All my response to location events are handled in the these two classes
self.sharedLocationHandler = [[[TTLocationHandler alloc] init] autorelease];
self.siteLogger = [[[ProjectSiteLogger alloc] initWithOptions:nil] autorelease];
self.siteLogger.locationHandler = self.sharedLocationHandler;
self.siteLogger.managedObjectContext = self.managedObjectContext;
// check launchOptions, skip all the views if there we were woken by location event
if (![launchOptions objectForKey:UIApplicationLaunchOptionsLocationKey]) {
[self confirmDataExistsAtStartup];
[self setupTabbarWithViews];
}
return YES;
}
Then you have the views setup:
-(void)setupTabbarsWithViews
{
// Code to setup initial views here
// ending with flag toggle to prevent repeat processing
needsViewsSetup = NO;
}
And in application will enter foreground:
- (void)applicationWillEnterForeground:(UIApplication *)application
{
if (needsViewsSetup) {
[self setupTabbarWithViews];
}
}
Note: My application is not running location services in the background, only listening for significant location changes and geofence.
Related
In my iOS app I turned on location services in background and also set on always. I also set up region monitoring for every 500 meters so if iOS kills my app in background then it will wake up using region monitoring.
But I found one major issue in updating location. I disabled location services of iOS and re enable it, my app is still in background but it's location icon showing disable and other app which is not even in background shows enable. Please see attached screenshot.
If any one knows about this then please guide me.
I think you have wrong concept, the arrow unfilled means that the app is using geofences, if you scroll to the bottom in that list you can read the legend as you can se in this screenshot
Hope this helps you
In app delegate.m,
- (BOOL)application:(UIApplication )application didFinishLaunchingWithOptions:(NSDictionary )launchOptions
{
[self setUpLocationManager];
return YES;
}
#pragma mark - CLLocationManager Delegate
-(void)setUpLocationManager
{
LocationManager=[[CLLocationManager alloc]init];
LocationManager.delegate=self;
LocationManager.desiredAccuracy=kCLLocationAccuracyBest;
[LocationManager requestWhenInUseAuthorization];
[LocationManager startUpdatingLocation];
}
-(void)locationManager:(CLLocationManager )manager didUpdateLocations:(NSArray<CLLocation > *)locations
{
CLGeocoder *GioCoder=[[CLGeocoder alloc]init];
[GioCoder reverseGeocodeLocation:LocationManager.location completionHandler:^(NSArray<CLPlacemark > Nullable placemarks, NSError * Nullable error)
{
if (error !=nil)
{
}
else
{
CLPlacemark *place=[placemarks lastObject];
NSLog(#"%#",place);
NSString *Path=[[NSBundle mainBundle]pathForResource:#"countries" ofType:#"json"];
NSError *Error;
NSData *JsonData=[[NSData alloc]initWithContentsOfFile:Path options:NSDataReadingMappedAlways error:&Error];
NSMutableArray *DataArray=[[NSMutableArray alloc]init];
if (Error==nil)
{
NSError *err;
DataArray=[NSJSONSerialization JSONObjectWithData:JsonData options:NSJSONReadingMutableContainers error:&err];
}
for (int i=0; i<DataArray.count;i++)
{
if ([[[DataArray objectAtIndex:i]valueForKey:#"code"] isEqualToString:place.ISOcountryCode])
{
// Insert your code here
[LocationManager stopUpdatingLocation];
break;
}
}
}
}];
}
-(void)locationManager:(CLLocationManager )manager didFailWithError:(NSError )error
{
if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusNotDetermined || [CLLocationManager authorizationStatus] == kCLAuthorizationStatusDenied || [CLLocationManager authorizationStatus] == kCLAuthorizationStatusRestricted)
{
NSLog(#"Location not enabled in your device");
}
else
{
}
}
Just as Reiner said in his answer, your understanding is wrong. If you have an empty purple arrow it means your geofence is set correctly.
If another app is staying filled purple. It could be because of multiple reasons:
It has pausesLocationUpdatesAutomatically set to false and is always tracking.
It has pausesLocationUpdatesAutomatically set to true but hasn't yet paused long enough at a location to trigger a pause which would technically turn that filled purple into either gray or empty purple (which means the app is continuing tracking using significant location changes or regionMonitoring or visits monitoring)
AFAIK if you region is setup for 500meters then it's better to use SignificantLocation Changes. Because you can use cellTower information. For regionMonitoring app turns it's location Tracking on and off every few minutes to make sure it's been exited the app or not.
VERY IMPORTANT NOTE
Apple documents on this are very confusing. It took me a whole week to figure it out!
Region Monitoring or significant location Tracking or visit Monitoring will only launch app in the background if and only if your app was terminated. If your app is launched again then right from the DidFinishLaunching you can startLocationUpdates.
If your app was suspended or for some reason still in the background you would only get the didExitRegion callback. And AFIAK from the callback you're not able to startUpdateLocations. This is a very vague text from WWDC.
Finally, you must start your location updates while in the
foreground. If you don’t start your updates in the foreground, you
won’t get this special behavior. So what happens if you do start your
updates in the background? Well, first off, your app will probably
need AlwaysAuthorization since your app won’t be considered in use at
that time. Furthermore, Core Location won’t take any action to
ensure your app continues to run. So if you have background runtime
for some reason, and you decide to start a location session, you
might get some updates, but you might also get suspended before you receive all the information you had hoped to receive.
WWDC 2016: Session 716
I don't know why Apple is saying might...it could be something that varies based on battery/cell coverage/ usage patterns / iOS version. It's obviously something they don't want us developers know how. They want to force us to go for other ways.
The only way for you to get your app into terminated state is either if you user manually kills the app or you wait for a very very long time ie wait for any or all other background tasks to finish + then once app is suspended wait for it to become terminated and then exit the region to launch app and be able to start tracking Locations
I've been able to reproduce a defect in our app twice, but most times I fail. So I'm trying to understand what could possibly be going on here and hopefully have some new things to try. Our app times out and logs the user out after 10 minutes using an NSTimer. Every time the screen is touched the timer is reset, this all works great.
When the user backgrounds the app and comes back, the following code gets called:
- (BOOL)sessionShouldTimeOut {
if (self.timeoutManager) {
NSTimeInterval timeIntervalSinceNow = [self.timeoutManager.updateTimer.fireDate timeIntervalSinceDate:[NSDate date]];
if (timeIntervalSinceNow < 0) {
return YES;
} else {
return NO;
}
}
return NO;
}
- (void)timeoutIfSessionShouldTimeOut {
if ([self sessionShouldTimeOut]) {
[self.timeoutManager sendNotificationForTimeout];
}
}
This (I suspect) is the code that's failing. What happens when it fails is the user logs in, hits the home page and locks their phone. After 10+ minutes, they unlock and the app isn't logged out. When they come back, it's the code above that gets executed to log the user out, but in some scenarios it fails - leaving the user still on the homepage when they shouldn't be.
Here's my current theories I'm trying to test:
The timer somehow fires in the background, which then runs the logout routine, but since we're in the background the UI isn't updated but the timer is invalidated (we invalidate the timer after logout) I'm not sure if UI code called from the background will be shown after the app is in the foreground, so this may not be a possibility.
The user actually is coming back a few seconds before the timer fires, then after a few seconds when it should have fired it doesn't since it was backgrounded for 10 minutes. Do timers continue to hit their original fire time if the app goes to the background?
Somehow, while in the background, self.timeoutManager, updateTimer, or fireDate are being released and set to nil, causing the sessionShouldTimeOut method to return NO. Can variables be nilled in the background? What would cause them to if they could be?
The logout routine gets run while the phone is taking a while to actually move to the app, potentially causing the UI updates to not be reflected?
I'm very open to other theories, as you can see a lot of mine are very very edge case since I'm not sure at all what's happening.
I'd appreciate any guidance anyone can offer as to what else I may be able to try, or even any insights into the underworkings of NSTimer or NSRunLoop that may be helpful in this scenario (the documentation on those is terrible for the questions I have)
In AppDelegate.h set
applicationDidEnterBackground:
UIBackgroundTaskIdentifier locationUpdater =[[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[[UIApplication sharedApplication] endBackgroundTask:locationUpdater];
locationUpdater=UIBackgroundTaskInvalid;
} ];
This tells the os that you still have things going and not to stop it.
One question and one issue:
I have the following code:
- (void) registerForLocalCalendarChanges
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(localCalendarStoreChanged) name:EKEventStoreChangedNotification object:store ];
}
- (void) localCalendarStoreChanged
{
// This gets call when an event in store changes
// you have to go through the calendar to look for changes
[self getCalendarEvents];
}
These methods are in a class/object called CalendarEventReporter which contains the method getCalendarEvents (in the callback).
Two things:
1) If the app is in the background the callback does not run. Is there a way to make it do that?
2) When I bring the app back into the foreground (after having changed the calendar on the device) the app crashes without any error message in the debug window or on the device. My guess is that the CalendarEventReporter object that contains the callback is being garbage-collected. Is that possible? Any other thoughts on what might be causing the crash? Or how to see any error messages?
1) In order for the app to run in the background you should be using one of the modes mentioned in the "Background Execution and Multitasking section here:
uses location services
records or plays audio
provides VOIP
services
background refresh
connection to external devices
like through BLE
If you are not using any of the above, it is not possible to get asynchronous events in the background.
2) In order to see the crash logs/call stack place an exception breakpoint or look into the "Device Logs" section here: Window->Organizer->Devices->"Device Name" on left->Device Logs on Xcode.
To answer your first question, take a look at https://developer.apple.com/library/ios/documentation/iphone/conceptual/iphoneosprogrammingguide/ManagingYourApplicationsFlow/ManagingYourApplicationsFlow.html
What I did to get code running in the background is to do something like
In the .h file
UIBackgroundTaskIdentifier backgroundUploadTask;
In the .m file
-(void) functionYouWantToRunInTheBackground
{
self.backgroundUploadTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[self endBackgroundUpdateTask];
}];
//code to do something
}
-(void) endBackgroundUpdateTask
{
[[UIApplication sharedApplication] endBackgroundTask: self.backgroundUploadTask];
self.backgroundUploadTask = UIBackgroundTaskInvalid;
}
The code above I pretty much learned from objective c - Proper use of beginBackgroundTaskWithExpirationHandler
As for your second question, you should set a breakpoint where code is supposed to run when you bring the app back to the foreground. No one can figure out why an app crashes if not given enough code or information.
The solution to the second part of the question was to raise the scope of the object containing the callback code. I raised it to the level of the containing ViewController. This seems to work. I still can't figure out how to raise the Notification (i.e. execute the call back) if the notification comes while the app is in the background/suspended. This prevented the object containing the callback from being cleaned up.
How would I implement a RACSignal that would stop publishing when there are no subscribers to it and auto start when there are subscribers?
Here is a scenario:
Let us say I have a currentLocationSignal in the AppDelegate.
My LocationViewController would subscribe to the currentLocationSignal when view loads and unsubscribe (dispose) when view unloads. Since it takes few seconds to get the current location, I would like to always subscribe to the currentLocationSignal when the app opens (and auto unsubscribe after few seconds), so by the time I arrive to LocationViewController I would get an accurate location.
So there can be more then one subscribers to the signal. When the first subscriber listens, it needs to start calling startUpdatingLocation and when there are no subscribers it needs to call stopUpdatingLocation.
Good question! Normally, you'd use RACMulticastConnection for use cases like this, but, because you want the signal to be able to reactivate later, a connection isn't suitable on its own.
The simplest answer is probably to mimic how a connection works, but with the specific behaviors you want. Basically, we'll keep track of how many subscribers there are at any given time, and start/stop updating the location based on that number.
Let's start by adding a locationSubject property. The subject needs to be a RACReplaySubject, because we always want new subscribers to get the most recently sent location immediately. Implementing updates with that subject is easy enough:
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations {
[self.locationSubject sendNext:locations.lastObject];
}
Then, we want to implement the signal that tracks and increments/decrements the subscriber count. This works by using a numberOfLocationSubscribers integer property:
- (RACSignal *)currentLocationSignal {
return [RACSignal createSignal:^(id<RACSubscriber> subscriber) {
#synchronized (self) {
if (self.numberOfLocationSubscribers == 0) {
[self.locationManager startUpdatingLocation];
}
++self.numberOfLocationSubscribers;
}
[self.locationSubject subscribe:subscriber];
return [RACDisposable disposableWithBlock:^{
#synchronized (self) {
--self.numberOfLocationSubscribers;
if (self.numberOfLocationSubscribers == 0) {
[self.locationManager stopUpdatingLocation];
}
}
}];
}];
}
In the above code, the +createSignal: block is invoked every time a new subscriber is added to the returned signal. When that happens:
We check to see if the number of subscribers is currently zero. If so, the just-added subscriber is the first one, so we need to enable (or re-enable) location updates.
We hook the subscriber directly up to our locationSubject, so the values from the latter are automatically fed into the former.
Then, at some future time, when the subscription is disposed of, we decrement the count and stop location updates if appropriate.
Now, all that's left is subscribing to the currentLocationSignal on startup, and automatically unsubscribing after a few seconds:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Use a capacity of 1 because we only ever care about the latest
// location.
self.locationSubject = [RACReplaySubject replaySubjectWithCapacity:1];
[[self.currentLocationSignal
takeUntil:[RACSignal interval:3]]
subscribeCompleted:^{
// We don't actually need to do anything here, but we need
// a subscription to keep the location updating going for the
// time specified.
}];
return YES;
}
This subscribes to self.currentLocationSignal immediately, and then automatically disposes of that subscription when the +interval: signal sends its first value.
Interestingly, -[RACMulticastConnection autoconnect] used to behave like -currentLocationSignal above, but that behavior was changed because it makes side effects wildly unpredictable. This use case should be safe, but there are other times (like when making a network request or running a shell command) when automatic reconnection would be horrible.
App being built on Xcode 4, for a base IOS level of 4 and above, tested on a variety of IPhones. Info in app is loaded from a JSON feed.
In my attempts to track down what's happening I've repeatedly debugged through this app, a fairly basic one that has a few views, overall nothing amazing or new in it as far as apps go.
The problem manifests itself occasionally, a lot of the time in our testing it has been if we've turned off Wifi to test on a cellular connection the app will hang when loading.
What happens is that the splash screen appears and the white spinning activity wheel spins as if it's loading the ad (an ad is displayed as part of the overall loading process) but it just sits there spinning.
So, most obvious question, is the ad the problem? Is it large or loading slowly? No, the JSOn feed is speedy (we check every time the app hangs and inbetween times as well to make sure it's all loading ok) and the ad is about 20k.
Debugging gets as far as the didFinishLaunchingWithOptions method which I'll list below, it reaches the end of that method and the debugger essentially reports nothing else. It just sits there, nothing visible in the XCode debug navigator, no break points hit.
If step through that method, once it gets to the end it goes about 6-7 steps into compiled code then simply does the same thing.
Here's the code from that method:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions: (NSDictionary *)launchOptions {
[self loadData];
splashViewController = [[OurSplashViewController alloc] initWithNibName:nil bundle:nil];
splashViewController.delegate = self;
OurWebViewController *webViewController = (OurWebViewController *)[[[tabBarController viewControllers] objectAtIndex:2] topViewController];
webViewController.url = [NSURL URLWithString:#"http://m.testURL.com/this_app/"];
webViewController.path = #"/MyPath/Sample";
[window addSubview:splashViewController.view];
[window makeKeyAndVisible];
return YES;
}
So, it gets to the 'YES' of that method and then the debugger sails off into the land of ones and zeroes, reporting nothing back and hanging the app.
Has anyone got any idea what could be causing this?
UPDATE:
Seem to have isolated that the hanging comes down to a JSON call that is sent but no response is received. So my code should handle that after a while.
Here's the JSON code, maybe someone can notice something I'm missing in there:
- (void)fetchWithURL:(NSURL *)URL {
if (!URL) {
return;
}
self.jsonURL = URL;
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:self.jsonURL];
if (timeout) {
[request setTimeoutInterval:timeout];
}
[request addValue:#"gzip" forHTTPHeaderField:#"Accept-Encoding"];
connection_ = [[NSURLConnection alloc] initWithRequest:request
delegate:self
startImmediately:YES];
[request release];
if (connection_ != nil) {
if ([self.delegate respondsToSelector:#selector(JSONFetchWillBeginDownload:)]) {
[self.delegate JSONFetchWillBeginDownload:self];
}
receivedData_ = [[NSMutableData alloc] initWithCapacity:0];
} else {
if ([self.delegate respondsToSelector:#selector(JSONFetch:downloadDidFailWithError:)]) {
[self.delegate JSONFetch:self downloadDidFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorUnknown userInfo:nil]];
}
}
}
According to Charles the reason for the failed response was:
Client closed connection before receiving entire response
I wonder is my timeout period too short?
If the spinning wheel (which is presumably part of the splash screen) is appearing, then the method you've posted is obviously working. If the splash screen is not disappearing, the obvious place to start debugging is in the code that sends the request and the code that receives the response then dismisses the splash screen.
Use a proxy like Charles to verify that the request is going out and a response is coming back. If that is the case, debug the code that receives the response and dismisses the splash screen. If that isn't the case, debug the code that sends the request.
If you don't get anywhere, I suggest posting the relevant code.
Use Charles Proxy for debugging the data exchange with your remote services and the ad-server.
See this tutorial for setting everything up.
Looks like my timeout period was too short for the ads and as a result if the user was off WiFi then the JSON took a little too long to load, causing the issue.
Fixed it by upping the timeout to a normal level.