35

I am trying to get iOS background app fetch to work in my app. While testing in Xcode it works, when running on the device it doesn't!

  • My test device is running iOS 9.3.5 (my deployment target is 7.1)
  • I have enabled "Background fetch" under "Background modes" under "Capabilities" on the target in Xcode

enter image description here

In application:didFinishLaunchingWithOptions I have tried various intervals with setMinimumBackgroundFetchInterval, including UIApplicationBackgroundFetchIntervalMinimum

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{

    // tell the system we want background fetch
    //[application setMinimumBackgroundFetchInterval:3600]; // 60 minutes
    [application setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];
    //[application setMinimumBackgroundFetchInterval:1800]; // 30 minutes

    return YES;
}

I have implemented application:performFetchWithCompletionHandler

void (^fetchCompletionHandler)(UIBackgroundFetchResult);
NSDate *fetchStart;

-(void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
    fetchCompletionHandler = completionHandler;

    fetchStart = [NSDate date];

    [[NSUserDefaults standardUserDefaults] setObject:fetchStart forKey:kLastCheckedContentDate];
    [[NSUserDefaults standardUserDefaults] synchronize];

    [FeedParser parseFeedAtUrl:url withDelegate:self];
}

 -(void)onParserFinished
{
    DDLogVerbose(@"AppDelegate/onParserFinished");

    UIBackgroundFetchResult result = UIBackgroundFetchResultNoData;

    NSDate *fetchEnd = [NSDate date];
    NSTimeInterval timeElapsed = [fetchEnd timeIntervalSinceDate:fetchStart];
    DDLogVerbose(@"Background Fetch Duration: %f seconds", timeElapsed);
    if ([self.mostRecentContentDate compare:item.date] < 0) {
        DDLogVerbose(@"got new content: %@", item.date);
        self.mostRecentContentDate = item.date;
        [self scheduleNotificationWithItem:item];
        result = UIBackgroundFetchResultNewData;
    }
    else {
        DDLogVerbose(@"no new content.");
        UILocalNotification* localNotification = [[UILocalNotification alloc] init];
        localNotification.fireDate = [NSDate dateWithTimeIntervalSinceNow:60];
        localNotification.alertBody = [NSString stringWithFormat:@"Checked for new posts in %f seconds", timeElapsed];
        localNotification.timeZone = [NSTimeZone defaultTimeZone];
        [[UIApplication sharedApplication] scheduleLocalNotification:localNotification];
    }

    fetchCompletionHandler(result);
}
  • I have (successfully!) tested with the simulator and device using Xcode's Debug/SimulateBackgroundFetch

  • I have successfully tested with a new scheme as shown in another SO answer (https://stackoverflow.com/a/29923802/519030)

  • My tests show code executing in the performFetch method in about 0.3 seconds (so it's not taking a long time)
  • I have verified that the device has background refresh enabled within settings.
  • Of course, I've looked at the other SO questions hoping someone else experienced the same thing. :)

When running on the device and not connected to Xcode, my code is not executing. I've opened the app, closed the app (not killed the app!), waited hours and days. I have tried logging in the fetch handers, and also written code to send local notifications.

I once successfully saw my local notifications test on the device, and in fact iOS seemed to trigger the fetch three times, each about about fifteen minutes apart, but then it never occurred again.

I know the algorithm used to determine how frequently to allow the background fetch to occur is a mystery, but I would expect it to run at least occasionally within a span of days.

I am at a loss for what else to test, or how to troubleshoot why it seems to work in the simulator but not on the device.

Appreciate any advice!

Community
  • 1
  • 1
Jason
  • 1,993
  • 2
  • 17
  • 26
  • Can you show your code. Often problems are caused by not calling the completion handler properly – Paulw11 Aug 29 '16 at 03:09
  • @Paulw11I added the relevant code. Hopefully someone can edit my code snippet because I cannot seem to get the formatting right. Thanks for looking. – Jason Aug 30 '16 at 03:12
  • BTW I tried a fresh deploy last night. I got two notifications that the check had been performed, and it's been silent since then. :( – Jason Aug 30 '16 at 03:14
  • I have been battling hard with the EXACT same issue. Interestingly if I build an `ipa` file and deploy that it seems to work for at least a little bit of time. But even after that it is very unreliable. I think something about building it and deploying via XCode causes issues, I am not sure. I did read something about it needing execution stats that it does not get when running via XCode. – Sam Saffron Aug 30 '16 at 01:29
  • Sam, thanks for the reply. Curious if you tried TestFlight or something and got any different/better results? – Jason Aug 30 '16 at 03:13
  • Jason did you try just running the completion handler without doing any work ? Does the work as expected? Also what about iOS 10? – Sam Saffron Aug 31 '16 at 20:12
  • @jason are you seeing a log of "BKNewProcess: has active assertions beyond permitted time" in your device logs, I seem to have gotten 2 of them yesterday (both are duration 180 which I think is app moving into background vs bg fetch, both appear to be waiting on network" – Sam Saffron Aug 31 '16 at 21:33
  • 1
    Sam, I will look at the logs and try your suggestion this evening. Curious if at this point my question should be rephrased? "background fetch voodoo" indeed! – Jason Aug 31 '16 at 21:42
  • Sam, just to follow-up, I did not see that error within my app, however I did see it several times for Google Photos (irrelevant but interesting). I tried rewriting the completion handler without doing any work (just returning success), and it worked similarly to what we've discussed: it fetched a few times, then went silent for many hours, then returned. It does seem as though it's trying to learn behavior of the app usage and only wake up a few times before then. I will consider remote push notifications, but there will be a cost to that. Too bad Parse died. – Jason Sep 01 '16 at 13:58
  • @Jason I don't know if it is worth posting another reply here but one very interesting thing I have found now that my app is in testflight external beta, "background fetch" seems to be working ultra reliably. I wonder if Apple do some fiddling with approved apps to make the work better or if somehow my phone has figured out what is going on. datapoint is that I have seen 80 background fetches in the last 26 hours. It fetched through the night a few times. – Sam Saffron Sep 04 '16 at 06:24
  • @Jason interestingly, since I upgraded my phone to iOS 10, the rate of fetches has gone up significantly. – Paulw11 Sep 16 '16 at 00:43
  • I'm getting about 6 checks a day (set for one hour apart) pretty consistently for a week now. It's enough, I just wish it were spaced out a bit more evenly. BTW your sample app crashed after ios10 upgrade. I'm not sure why as I'm not very good with Swift. – Jason Sep 16 '16 at 03:38

2 Answers2

27

Your problem is that you are returning from performFetchWithCompletionHandler before you call the completion handler, since the network fetch operation is occurring the in the background and you call the completion handler in your delegate method. Since iOS thinks you aren't playing by the rules it will deny your ability to use background fetch.

To fix the problem you need to call beginBackgroundTaskWithExpirationHandler and then end that task after you have called the completion handler.

Something like:

UIBackgroundTaskIdentifier backgroundTask

-(void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
    fetchCompletionHandler = completionHandler;

    fetchStart = [NSDate date];

    self.backgroundTask = [application beginBackgroundTaskWithExpirationHandler:^{
        [application endBackgroundTask:self.backgroundUpdateTask];
        self.backgroundTask = UIBackgroundTaskInvalid;
    }];

    [[NSUserDefaults standardUserDefaults] setObject:fetchStart forKey:kLastCheckedContentDate];

    [FeedParser parseFeedAtUrl:url withDelegate:self];
}

-(void)onParserFinished
{
    DDLogVerbose(@"AppDelegate/onParserFinished");

    UIBackgroundFetchResult result = UIBackgroundFetchResultNoData;

    NSDate *fetchEnd = [NSDate date];
    NSTimeInterval timeElapsed = [fetchEnd timeIntervalSinceDate:fetchStart];
    DDLogVerbose(@"Background Fetch Duration: %f seconds", timeElapsed);
    if ([self.mostRecentContentDate compare:item.date] < 0) {
        DDLogVerbose(@"got new content: %@", item.date);
        self.mostRecentContentDate = item.date;
        [self scheduleNotificationWithItem:item];
        result = UIBackgroundFetchResultNewData;
    }
    else {
        DDLogVerbose(@"no new content.");
        UILocalNotification* localNotification = [[UILocalNotification alloc] init];
        localNotification.alertBody = [NSString stringWithFormat:@"Checked for new posts in %f seconds", timeElapsed];
        [[UIApplication sharedApplication] scheduleLocalNotification:localNotification];
    }
    fetchCompletionHandler(result);
    [[UIApplication sharedApplication] application endBackgroundTask:self.backgroundUpdateTask];
    self.backgroundTask = UIBackgroundTaskInvalid;
}

My test app using this approach has been executing a fetch every 15 minutes initially, but it becomes less frequent over time. Without the background task it exhibited the same issue you are seeing.

I found that setting the background fetch interval to something other than UIApplicationBackgroundFetchIntervalMinimum also helps. My test app is running with a background fetch interval of 3600 (one hour) and has been reliably triggering for several days now; even after a phone restart and not running the app again. The actual trigger interval is 2-3 hours however.

My sample app is here

Paulw11
  • 95,291
  • 12
  • 135
  • 153
  • curious, is this technique legit? https://github.com/SamSaffron/DiscourseMobile/commit/989c3a3eac40a5bc6b54dc700cccdf1fa7bbf62c wrapping completion handler with another that terminates the bg task ? – Sam Saffron Aug 30 '16 at 21:14
  • 1
    Yes, that's fine. That code isn't quite right since it starts the background transfer twice, one with the standard completion handler and once with the wrapped handler, but as long as a background task is started and ended and the completion handler for the fetch is called it doesn't matter how that is achieved – Paulw11 Aug 30 '16 at 21:19
  • 1
    Thank you! I will try this on a build tonight. Curious why you have two task identifiers (backgroundTask and backgroundUpdateTask)? Is that a cutting and pasting error or am I missing something? – Jason Aug 31 '16 at 03:29
  • Sadly I am still seeing the same failure mode even after the patch, overnight I got 3 fetches last one was 7 hours ago – Sam Saffron Aug 31 '16 at 20:08
  • I have seen something similar. I suspect that there is a limited number of background fetches you are allowed without the user launching your app again. Increasing the background fetch interval will give you a longer period of background fetch but still the same number of fetches. – Paulw11 Aug 31 '16 at 20:46
  • I just checked my phone. The background update triggered about 16:10 my time yesterday and then nothing until about an hour ago (approximately 6 am my time). I did not launch the app during that time. I think it may take some patience and time while iOS works out an appropriate pattern for your app. – Paulw11 Aug 31 '16 at 21:15
  • @Paulw11this feature is shrouded with so much voodoo :) I wonder if I should simply have an hourly job that sends hidden notifications to app, that way I can properly ensure stuff is updated with some sort of regularity. – Sam Saffron Aug 31 '16 at 21:37
  • I tried a new build using this technique. Weirdly nothing happened for over 30 minutes, then I saw five updates last night before midnight. It was then quiet for six hours. It then checked five times this morning, and nothing since (about 8 hours ago). I don't know what this means, other than the timing is maddeningly inconsistent. I have opened the app several times in between. I think tonight I will try Paulw11's suggestion of increasing the window. Really if it just did it four times a day I'd be happy -- if they were spaced out so the odds of picking up new content were better. – Jason Aug 31 '16 at 21:39
  • @Jason if you have a limited number of clients using `content-available=1` on a remote push notification and scheduling hourly could work around the issue – Sam Saffron Aug 31 '16 at 22:13
  • @Jason - Updated my answer with my results after several days testing – Paulw11 Sep 07 '16 at 04:43
  • @Paulw11thanks for the update AND the code, really nice. I have been testing a lot lately as well. I get pretty consistent updates spaced an hour apart, but there are always huge, multi-hour gaps between. During the wee hours of the morning this makes sense to me (phone not in use), but not so much during the day. May try to build yours and put on alongside it. Will accept your answer once bounty period over, but I think you've given us the most to go on- thanks. – Jason Sep 08 '16 at 02:25
  • @Paulw11 I put your build on my phone and in the last 24 hours it has only checked twice. Weird, eh? This is iOS 9... not sure if you are on a different version. – Jason Sep 09 '16 at 03:53
  • GIve it a few more days. It seems to take a few days to determine a suitable pattern for the app, based on the data/no data results – Paulw11 Sep 09 '16 at 03:54
  • @SamSaffron and Paulw11 - My code, which was working in 10.0, seems to have quit working in 10.1 (no polling at all). Have you guys experienced this? – Jason Nov 09 '16 at 22:38
  • I just reinstalled my sample app (I updated it for Swift 3) and it is working as before – Paulw11 Nov 10 '16 at 04:42
  • Hi Paul. I installed your updated app (thank you for updating it!) on the 10th. I have been opening each day since. It hasn't done a fetch yet. I'm on 10.1 (have not gone up to the next bug fix). I don't think it's programming at this point, but rather system or version inconsistency. – Jason Nov 15 '16 at 21:36
  • I re-istalled the app on the 10th, ran it and then just left it. On the first day I only got one background fetch notification. On about the 3rd day I started getting them every hour or so. I am running 10.1 (14B72). It definitely seems to take a couple of days before iOS regularly schedules the background fetch. – Paulw11 Nov 15 '16 at 21:48
  • The statement `Your problem is that you are returning from performFetchWithCompletionHandler before you call the completion handler,` doesn't seem consistent with Apple's documentation: `When this method is called, your app has up to 30 seconds of wall-clock time to perform the download operation and call the specified completion handler block.` (https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623125-application). Is this a case of incorrect docs on the part of Apple? – Chris Prince Jun 19 '17 at 16:33
  • 1
    I don't know about incorrect documentation. Perhaps just unclear. If the app delegate function returns then iOS considers the execution complete, even if tasks are still executing in the background *unless you have told iOS about that background work* – Paulw11 Jun 19 '17 at 21:07
  • @Paulw11 That does indeed look to be the solution. I've got it working now. Thanks! – Chris Prince Jun 20 '17 at 03:32
13

In order to implement background fetch there are three things you must do:

  • Check the box Background fetch in the Background Modes of your app’s Capabilities.
  • Use setMinimumBackgroundFetchInterval(_:) to set a time interval appropriate for your app.
  • Implement application(_:performFetchWithCompletionHandler:) in your app delegate to handle the background fetch.

Background Fetch Frequency

How frequent our application can perform Background Fetch is determined by the system and it depends on:

  • Whether network connectivity is available at that particular time
  • Whether the device is awake i.e. running
  • How much time and data your application has consumed in the previous Background Fetch

In other words, your application is entirely at the mercy of the system to schedule background fetch for you. In addition, each time your application uses background fetch, it has at most 30 seconds to complete the process.

Include in your AppDelegate (change the below code to your fetching needs)

-(void) application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {

    NSLog(@"Background fetch started...");

    //---do background fetch here---
    // You have up to 30 seconds to perform the fetch

    BOOL downloadSuccessful = YES;

    if (downloadSuccessful) {
        //---set the flag that data is successfully downloaded---
        completionHandler(UIBackgroundFetchResultNewData);
    } else {
        //---set the flag that download is not successful---
        completionHandler(UIBackgroundFetchResultFailed);
    }

    NSLog(@"Background fetch completed...");
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    [[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];
    return YES;
}

To check if Background Refresh is enabled for your app use the below code:

UIBackgroundRefreshStatus status = [[UIApplication sharedApplication] backgroundRefreshStatus];
switch (status) {
    case UIBackgroundRefreshStatusAvailable:
    // We can do background fetch! Let's do this!
    break;
    case UIBackgroundRefreshStatusDenied:
    // The user has background fetch turned off. Too bad.
    break;
    case UIBackgroundRefreshStatusRestricted:
    // Parental Controls, Enterprise Restrictions, Old Phones, Oh my!
    break;
}
TechSeeko
  • 1,511
  • 10
  • 19
  • 6
    TechSeeko - this answer is a great summary of the requirements to background refresh, but doesn't address the core issues. The first, which I think Paul nailed, was a coding issue for not marking the background task. The second is basically trying to understand the frequency and pattern with which we can expect our apps to fetch remote data. It seems as though the answer to that may be, "don't expect consistency," which is unfortunate. Your summary is probably useful to others though - thank you. – Jason Sep 01 '16 at 14:03
  • @Jason you are right, I took a look at Paul's answer and thought that I should provide more info on how to implement this feature instead of just pointing out the error all over again. The issue above is calling the completionHandler before starting the background task and even before fetching new data. Don't forget you need to enable push in the Capabilities section of your project settings and register for notifications in cases where the fetch is supposed to be initiated by a silent push. A silent push payload will have to include the "content-available" = 1. Thanks for the comment tho. – TechSeeko Sep 02 '16 at 12:14
  • 1
    The setMinimumBackgroundFetchInterval was helpful for my react native project. Thank you. – Chris Jun 13 '17 at 15:38