Hi,
One of the functions an app I'm working on has to do is a simple https HEAD request (in the background) using AFNetworking 3 to see if some remote files have been updated since the last time they were fetched. If they have been updated, then a local notification is supposed to be sent to let the user know that.
In the simulator, if I "Simulate background refresh" via Xcode, the notification is dispatched. But looking at the application state, it is showing UIApplicationStateActive when I do this. If I instead hit the Home button on the simulator but leave the app running in Xcode and then Simulate background refresh, the first one does not dispatch a notification but subsequent ones do. Application state also shows in the background.
The only difference between the first and the last is that although performFetchWithCompletionHandler is printed out in the console for both, the second and so on background refreshes also print this:
2018-03-16 20:34:24.176976-0400 [24582:1895552] +[CATransaction synchronize] called within transaction
2018-03-16 20:34:24.177574-0400 [24582:1895552] +[CATransaction synchronize] called within transaction
2018-03-16 20:34:24.177950-0400 [24582:1895552] +[CATransaction synchronize] called within transaction
2018-03-16 20:34:24.178400-0400 [24582:1895552] +[CATransaction synchronize] called within transaction
On device, when simulating background refresh, the first time a notification is dispatched and app state is UIApplicationStateInactive. The second try app state is UIApplicationStateBackground and nothing is dispatched. On the third and all subsequent tries, app state is UIApplicationStateBackground and a notification is dispatched.
If I hit the home button and let the app continue executing in Xcode, then simulate background refresh, the first one not dispatched but subsuquent ones are. Stepping into the AFNetworking code, it appears the on the first one, the NSURLSessionDataTask is nil and hence failing and returning. It's not using the simulator, however.
If I just run the app on device (without Xcode) or on simulator but do not send a background refresh via Xcode, neither gets a notification. I've left both running several hours.
This is how I have things set up. I am targeting the iPhone and iOS 11.2.
In my AppDelegate,
1. I subscribe to UNUserNotificationCenterDelegate
2. In didFinishLaunchingWithOptions I call [[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];
3. Request authorization for notifications and set the delegate like so:
@property (nonatomic, strong) UNUserNotificationCenter *center;
self.center = [UNUserNotificationCenter currentNotificationCenter];
self.center.delegate = self;
4. I register for notifications:
- (void) registerForNotifications{
[self.center requestAuthorizationWithOptions:(UNAuthorizationOptionSound | UNAuthorizationOptionAlert | UNAuthorizationOptionBadge | UNAuthorizationOptionCarPlay) completionHandler:^(BOOL granted, NSError * _Nullable error){
if(granted){/
NSLog(@"Notification request succeeded!");
}
else if(!granted){
NSLog(@"Notification request not granted...");
}
else if(error){
NSLog(@"Notification request failed...");
}
}];
}5. I also configure notifications:
- (void) configureNotifications{
UNNotificationAction *deleteAction = [UNNotificationAction actionWithIdentifier:@"Delete"
title:@"Delete" options:UNNotificationActionOptionDestructive];
UNNotificationAction *launchAppAction = [UNNotificationAction actionWithIdentifier:@"Launch App"
title:@"Launch" options:UNNotificationActionOptionForeground];
UNNotificationCategory *category = [UNNotificationCategory categoryWithIdentifier:@"UYLReminderCategory"
actions:@[deleteAction,launchAppAction] intentIdentifiers:@[]
options:UNNotificationCategoryOptionNone];
NSSet *categories = [NSSet setWithObject:category];
[self.center setNotificationCategories:categories];
}6. Implement this method:
- (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler{
NSLog(@"In performFetchWithCompletionHandler");
[[SharedHttpClient sharedHttpClient] checkRemoteFilesToSeeIfUpdated];
completionHandler(UIBackgroundFetchResultNewData);
}7. Added an observer:
[[NSNotificationCenter defaultCenter]
addObserver:self selector:@selector(receiveEvent:) name:@"REMOTE_JSON_UPDATE" object:nil];8. And implement the handler for it:
- (void)receiveEvent:(NSNotification *)notification {
BOOL statusUpdated = [[[notification userInfo] valueForKey:@"update_status"] boolValue];
//UIApplication *application = [UIApplication sharedApplication];
if(statusUpdated){
[self createNotificationAndDispatch];
}
}9. And finally create the notification:
- (void)createNotificationAndDispatch {
UNMutableNotificationContent *content = [UNMutableNotificationContent new];
content.title = [NSString localizedUserNotificationStringForKey:@"My Notification!" arguments:nil];
content.body = [NSString localizedUserNotificationStringForKey:@"Test" arguments:nil];
content.sound = [UNNotificationSound defaultSound];
content.categoryIdentifier = @"MyReminderCategory";
UNTimeIntervalNotificationTrigger *trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:3 repeats:NO];
NSString *identifier = @"MyNotification";
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:identifier content:content trigger:trigger];
[self.center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
if (error != nil) {
NSLog(@"Something went wrong: %@",error);
}
}];
}10. For completeness this is applicationdidfinishloadingwithoptions
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];
[[SharedDataModel sharedDataModel] loadFromJSONFiles];
self.center = [UNUserNotificationCenter currentNotificationCenter];
self.center.delegate = self;
[self registerForNotifications];
[self configureNotifications];
[[SharedDataModel sharedDataModel] setUserDefaults];
[[NSNotificationCenter defaultCenter]
addObserver:self selector:@selector(receiveEvent:) name:@"REMOTE_JSON_UPDATE" object:nil];
return YES;
}For AFNetworking, I have a singleton that subclasses AFHTTPSessionManager. It is created and configured thusly,
+ (SharedHttpClient*) sharedHttpClient {
static SharedHttpClient *_sharedHttpClient = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSURLSessionConfiguration* sessionConfig = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"myBackgroundSessionConfig"];
_sharedHttpClient = [[SharedHttpClient alloc] initWithBaseURL:[NSURL URLWithString:kBaseURLString] sessionConfiguration:sessionConfig];
_sharedHttpClient = [[SharedHttpClient alloc] initWithBaseURL:[NSURL URLWithString:kBaseURLString]];
});
return _sharedHttpClient;
}
- (instancetype)initWithBaseURL:(NSURL *)url sessionConfiguration:(nullable NSURLSessionConfiguration *)configuration{
self = [super initWithBaseURL:url sessionConfiguration:configuration];
if (self) {
self.responseSerializer = [AFJSONResponseSerializer serializer];
self.requestSerializer = [AFJSONRequestSerializer serializer];
self.requestSerializer.timeoutInterval = 30;
}
return self;
}And this is the method making the https request. It is located in the aforementioned singleton:
- (void) checkRemoteFilesToSeeIfUpdated{
NSArray* jsonFilesToCheck = [SharedHttpClient jsonFilesToCheckForNewDrops];
__block BOOL notificationAlreadyDispatched = NO;
for (NSString* filename in jsonFilesToCheck) {
[self HEAD:filename parameters:nil success:^(NSURLSessionDataTask * _Nonnull task) {
if ([task.response isKindOfClass:[NSHTTPURLResponse class]]) {
NSHTTPURLResponse* response = (NSHTTPURLResponse *)task.response;
NSDictionary* headers = response.allHeaderFields;
NSString* lastModified = headers[@"Last-Modified"];
NSLog(@"Last Modified: %@", lastModified);
BOOL isNewer = [[SharedDataModel sharedDataModel] isRemoteFileNewer:filename usingDateStr:lastModified];
isNewer = YES;//set this to YES for testing purposes so as to force a notification
self.thereAreNewFiles = isNewer;
NSMutableDictionary* d = [NSMutableDictionary dictionary];
[d setObject:[NSNumber numberWithBool:isNewer] forKey:@"update_status"];
if(isNewer && !notificationAlreadyDispatched){/
[[NSNotificationCenter defaultCenter] postNotificationName:@"REMOTE_JSON_UPDATE"
object:self
userInfo:d];
notificationAlreadyDispatched = YES;
}
}
else{
NSLog(@"isKindOfClass check failed");
}
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
NSLog(@"Failed in checkRemoteFilesToSeeIfUpdated");
NSLog(@"%@", error.debugDescription);
}];
}
}I have turned on Push Notifications as well as Background Modes (Background Fetch)
The expected flow was that periodically,
1. performFetchWithCompletionHandler would be called by the system.
2. It in turn would call checkRemoteFilesToSeeIfUpdated which in turn would post a notifcation if any files were new.
3. If there are, the observer posts a notification to its handler
4. Handler calls createNotificationAndDispatch
5. Local notification dispatched!
I will be EXTREMELY grateful if anyone here can help me figure out what is going on here as I am already behind schedule :-(
Thank you very much!
So, as it turns out... this actually IS working.
I let the app run for over 24 hours and during that time, I did in fact get a couple of notifications.
Subsequent days increased somewhat with notification frequencies.
I've realized, however, that this form of non-scheduled notification is not the right choice for me so I am pivoting to using AWS Lambda along with SNS to be able to give timely notifications.
I'm going to try and close this thread if I can.