How to get use BGTask in objective-c with iOS 13(Background Fetch)

Viewed 1464

So i want to build a iOS, i am pretty new to the world of objective-c and one feature i want to implement is the ability to send a API request and do a bit of background processing while the app is not "in focus/in background". I have researched for a couple days about this BGTask API for iOS 13 and have created a projected to see if i can get "background fetch" working. I have not be able to. Im pretty sure i have everything setup correctly but i can not get background fetch functionality to trigger on my iPhone, not even once over the past couple days.

  • I am using a actual iOS device to test this with iOS 13.4.1
  • "Permitted background task scheduler identifiers" is setup properly in Info.plist
  • App is signed
  • Background processing and Background fetch is checked in Background Modes
  • I waited the 15 minute interval as per Apples documentation

Here is my code. All this is just a blank iOS project using objective-c. I only edited AppDelegate.m and Info.plist

AppDelegate.m

#import "AppDelegate.h"
#import <BackgroundTasks/BackgroundTasks.h>


static NSString* TaskID = @"com.myapp.task";

@interface AppDelegate ()

@end

@implementation AppDelegate


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.


    [[BGTaskScheduler sharedScheduler] registerForTaskWithIdentifier:TaskID
                                                          usingQueue:nil
                                                       launchHandler:^(BGProcessingTask *task) {

        [self handleAppRefreshTask:task];
    }];

    return YES;
}


#pragma mark - UISceneSession lifecycle


- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options {
    // Called when a new scene session is being created.
    // Use this method to select a configuration to create the new scene with.
    return [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role];
}


- (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet<UISceneSession *> *)sceneSessions {
    // Called when the user discards a scene session.
    // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
    // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}


-(void)handleAppRefreshTask:(BGProcessingTask *)task {
    //do things with task
  NSLog(@"Process started!");
  task.expirationHandler = ^{
    NSLog(@"WARNING: expired before finish was executed.");

  };

  NSString *targetUrl = @"https://webhook.site/1b274a6f-016f-4edf-8e31-4ed7058eaeac";
  NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
  [request setHTTPMethod:@"GET"];
  [request setURL:[NSURL URLWithString:targetUrl]];

  [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:
    ^(NSData * _Nullable data,
      NSURLResponse * _Nullable response,
      NSError * _Nullable error) {

        NSString *myString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
        NSLog(@"Data received: %@", myString);
  }] resume];

  task.expirationHandler = ^{
    NSLog(@"WARNING: expired before finish was executed.");

  };

  [task setTaskCompletedWithSuccess:YES];
}

- (void)applicationDidEnterBackground:(UIApplication *)application
{
  NSLog(@"Entering background");
  BGProcessingTaskRequest *request = [[BGProcessingTaskRequest alloc] initWithIdentifier:TaskID];
  request.requiresNetworkConnectivity = true;
  request.requiresExternalPower = false;
  request.earliestBeginDate = [NSDate dateWithTimeIntervalSinceNow:60];

  @try {
    [[BGTaskScheduler sharedScheduler] submitTaskRequest:request error:nil];
  }
  @catch(NSException *e){
    NSLog(@" Unable to submit request");
  }


}

@end

Is background fetch broken in iOS 13? Even clicking on the “Simulate background fetch" in Xcode debug menu does not work. It just closes the app and nothing happens. Can anybody help/give any advice?

1 Answers

A few observations:

  1. The setTaskCompletedWithSuccess should be inside the network request’s completion handler. You don’t want to mark the task as complete until the request has had a chance to run and you’ve processed the result.

  2. You are calling submitTaskRequest, but passing nil for the NSError reference. You have also wrapped that in an exception handler. But this API call doesn’t throw exceptions, but rather just passes back errors. But you have to supply it an error reference. E.g.

    NSLog(@"Entering background");
    BGProcessingTaskRequest *request = [[BGProcessingTaskRequest alloc] initWithIdentifier:TaskID];
    request.requiresNetworkConnectivity = true;
    request.requiresExternalPower = false;
    request.earliestBeginDate = [NSDate dateWithTimeIntervalSinceNow:60];
    
    NSError *error;
    if (![[BGTaskScheduler sharedScheduler] submitTaskRequest:request error:&error]) {
        NSLog(@"BGTaskScheduler failed: %@", error);
    }
    

    In your code, if it failed, you would never know.

  3. You have placed this code in applicationDidEnterBackground. I.e., are you seeing this “Entering background” message at all? The reason I ask is that if you’ve supplied a scene delegate (common if you just created a new iOS 13 app), this method won’t be called, whereas sceneDidEnterBackground will.

  4. You said that you tried “Simulate background fetch”. But you haven’t created a background fetch request (a BGAppRefreshTask). You created a background task (a BGProcessingTask), which is a different thing. To test background processing requests, refer to Starting and Terminating Tasks During Development.

  5. There’s an interesting question as to how you know that the fetch request was processed. You’re just using NSLog (which presumes that you’re keeping your app attached to the Xcode debugger). I would suggest testing this without the app being attached to Xcode. There are a few options:

    • If you can watch your server logs for requests, that works.

    • I personally will often put in UserNotifications (and make sure to go into settings and turn on persistent notifications so I don’t miss them).

    • Another approach that I’ve done is to log these events in some table in my app’s persistent storage and then have some UI within the app to fetch this data so I can confirm what happened.

    • I’ll often use Unified Logging so that I can watch os_log statements issued by my device from the macOS Console even when Xcode is not running. This is very useful in logging app/scene methods. See WWDC 2016 Unified Logging and Activity Tracing
       

    Whatever you do, for things like background processing, background app refresh, etc., I will program some mechanism so that I can check to see if the requests/tasks took place, even when not attached to Xcode. Being attached to the debugger can, in some cases, affect the app lifecycle, and I want to make sure I’ve got some way to confirm what was going on without the benefit of the console.

  6. Likely obvious, but make sure you never “force quit” the app, as that will stop background processes from taking place.

For more information, See WWDC 2019 video Advances in App Background Execution.

Related