iosbackground-fetch

Will performFetchWithCompletionHandler be called if the app has been terminated


It's surprisingly difficult to find a definitive answer to this; couldn't find it mentioned in the Apple documentation and couldn't find a definite yes/no after searching past questions.

The question is simple - if the app requests a background fetch to be performed after N time, then the user terminates the app. Will the OS still launch the app into the background to perform the background fetch?


Solution

  • Okay, once again background modes cause confusion. No offense to the other people trying to help, but this is more complicated than it seems.

    First of all: This is out of date, as Sausage guessed in the comments. I know this for a fact, because the section about VoIP apps is still explaining the "old way" to do this, with a handler that gets called periodically. I investigated this a bit for this answer, so I suggest you go and read that. The important lesson for this case here is that iOS makes a distinction between an app being terminated by the user or by the system, plus it also plays a role whether the phone was rebooted or not.

    So to sum this (and your question) up you basically want to know whether this part of the above, outdated documentation is still correct word for word:

    In most cases, the system does not relaunch apps after they are force quit by the user. One exception is location apps, which in iOS 8 and later are relaunched after being force quit by the user. In other cases, though, the user must launch the app explicitly or reboot the device before the app can be launched automatically into the background by the system. When password protection is enabled on the device, the system does not launch an app in the background before the user first unlocks the device.

    Apple: Understanding When Your App Gets Launched into the Background

    I thoroughly investigated the rest of the docs, but did not find any definite answer, so it unfortunately boils down to what dan already suggested: Test it. My gut feeling is that the documentation is still correct in that regard, though (as said what's not is the VoIP stuff). I say that because the UI in the Settings app calls the feature "Background App Refresh", so users are probably supposed to understand that an app having this permission won't refresh when they "push" them out of background (i.e. home button -> swipe it out). For regular users, apps are either quit (not in the task manager at all), in the foreground (using them) or in background (they're in the task manager and another app is in foreground and/or the phone is locked).


    To really test this you'd have to write an app and actually carry it around a bit (I assume at least two days) in each condition. First while it is in background (the OS should periodically let it fetch, as you probably know this can also be triggered in Xcode) and then while it is force-quit. The problem is to verify that it fetched stuff. I'd go with a logfile that can be shared via iTunes. I have typed up some code for this:

    -(void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
    {
        NSLog(@"We're awake! Booyah!");
        
        NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
        NSURLSession *session = [NSURLSession sessionWithConfiguration:config
                                                              delegate:nil
                                                         delegateQueue:[NSOperationQueue mainQueue]];
        NSMutableURLRequest *request = [NSMutableURLRequest new];
        request.HTTPMethod = @"GET";
        request.URL = [NSURL URLWithString:@"https://www.google.com"];
        NSURLSessionDataTask *task = [session dataTaskWithRequest:request
                                                completionHandler:^(NSData * _Nullable data,
                                                                    NSURLResponse * _Nullable response,
                                                                    NSError * _Nullable error) {
                                                    NSDate *now = [NSDate date];
                                                    NSString *toLog = [NSString stringWithFormat:@"%@ - fetched\n",
                                                                       [now description]];
                                                    [self updateTestDocumentWithString:toLog];
                                                    NSLog(@"Yay, done!");
                                                    completionHandler(UIBackgroundFetchResultNewData);
                                                }];
        [task resume];
    }
    
    - (void)updateTestDocumentWithString:(NSString *)toAppend {
        NSString *docDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
        NSString *filePath = [[docDir stringByAppendingPathComponent:@"logfile.txt"] copy];
        if (![[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
            if (![[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:nil]) {
                NSLog(@"We're effed...");
                return;
            }
        }
        NSFileHandle *file = [NSFileHandle fileHandleForUpdatingAtPath:filePath];
        if (!file) {
            NSLog(@"We're effed again...");
            return;
        }
        [file seekToEndOfFile];
        // ensure this is never nil
        [file writeData:[toAppend dataUsingEncoding:NSUTF8StringEncoding]];
        [file closeFile];
    }
    

    This would go into the app delegate, and don't forget to add the Application supports iTunes file sharing boolean setting in your app's plist. I will leave this running on my development device for a bit and check the logfile, eventually reporting back here. Feel free to test it yourself, too.