How to troubleshoot iOS background app fetch not working?
Asked Answered
C

2

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://mcmap.net/q/450384/-ios-background-fetch-not-working-even-though-correct-background-mode-configured)

  • 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!

Collimate answered 29/8, 2016 at 3:0 Comment(12)
Can you show your code. Often problems are caused by not calling the completion handler properlyClemenciaclemency
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.Dispersive
@Paulw11I added the relevant code. Hopefully someone can edit my code snippet because I cannot seem to get the formatting right. Thanks for looking.Collimate
Sam, thanks for the reply. Curious if you tried TestFlight or something and got any different/better results?Collimate
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. :(Collimate
Jason did you try just running the completion handler without doing any work ? Does the work as expected? Also what about iOS 10?Dispersive
@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"Dispersive
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!Collimate
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.Collimate
@Collimate 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.Dispersive
@Collimate interestingly, since I upgraded my phone to iOS 10, the rate of fetches has gone up significantly.Clemenciaclemency
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.Collimate
C
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

Clemenciaclemency answered 30/8, 2016 at 5:20 Comment(20)
curious, is this technique legit? github.com/SamSaffron/DiscourseMobile/commit/… wrapping completion handler with another that terminates the bg task ?Dispersive
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 achievedClemenciaclemency
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?Collimate
Sadly I am still seeing the same failure mode even after the patch, overnight I got 3 fetches last one was 7 hours agoDispersive
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.Clemenciaclemency
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.Clemenciaclemency
@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.Dispersive
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.Collimate
@Collimate if you have a limited number of clients using content-available=1 on a remote push notification and scheduling hourly could work around the issueDispersive
@Collimate - Updated my answer with my results after several days testingClemenciaclemency
@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.Collimate
@Clemenciaclemency 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.Collimate
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 resultsClemenciaclemency
@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?Collimate
I just reinstalled my sample app (I updated it for Swift 3) and it is working as beforeClemenciaclemency
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.Collimate
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.Clemenciaclemency
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. (developer.apple.com/documentation/uikit/uiapplicationdelegate/…). Is this a case of incorrect docs on the part of Apple?Pest
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 workClemenciaclemency
@Clemenciaclemency That does indeed look to be the solution. I've got it working now. Thanks!Pest
B
14

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;
}
Bergh answered 1/9, 2016 at 10:45 Comment(3)
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.Collimate
@Collimate 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.Bergh
The setMinimumBackgroundFetchInterval was helpful for my react native project. Thank you.Salubrious

© 2022 - 2024 — McMap. All rights reserved.