1

I am trying to implement a basic func here which will be called when my app is backgrounded or suspended.

In reality, we aim to send about 5 a day so Apple should not throttle our utilisation.

I've put together the following which uses firebase and userNotifications, for now, it is in my app delegate.

import Firebase
import FirebaseMessaging
import UserNotifications

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    var backgroundSessionCompletionHandler: (() -> Void)?


    lazy var downloadsSession: Foundation.URLSession = {
        let configuration = URLSessionConfiguration.background(withIdentifier: "bgSessionConfiguration")
        configuration.timeoutIntervalForRequest = 30.0
        let session = Foundation.URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
        return session
    }()



    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        FIRApp.configure()
        if #available(iOS 10.0, *) {
            let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
            UNUserNotificationCenter.current().requestAuthorization(
                options: authOptions,
                completionHandler: {_, _ in })

            // For iOS 10 display notification (sent via APNS)
            UNUserNotificationCenter.current().delegate = self
            // For iOS 10 data message (sent via FCM)
            FIRMessaging.messaging().remoteMessageDelegate = self

        } else {
            let settings: UIUserNotificationSettings =
                UIUserNotificationSettings(types: [.alert, .badge, .sound], categories: nil)
            application.registerUserNotificationSettings(settings)
        }

        application.registerForRemoteNotifications()
        let token = FIRInstanceID.instanceID().token()!
        print("token is \(token) < ")

        return true
    }



    func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void){
           print("in handleEventsForBackgroundURLSession")
           _ = self.downloadsSession
           self.backgroundSessionCompletionHandler = completionHandler
    }

    //MARK: SyncFunc

    func startDownload() {
        NSLog("in startDownload func")

        let todoEndpoint: String = "https://jsonplaceholder.typicode.com/todos/1"
        guard let url = URL(string: todoEndpoint) else {
            print("Error: cannot create URL")
            return
        }

        // make the request
        let task = downloadsSession.downloadTask(with: url)
        task.resume()
        NSLog(" ")
        NSLog(" ")

    }

    func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession){
        DispatchQueue.main.async(execute: {
            self.backgroundSessionCompletionHandler?()
            self.backgroundSessionCompletionHandler = nil
        })
    }

    func application(_ application: UIApplication,  didReceiveRemoteNotification userInfo: [NSObject : AnyObject],  fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {

        NSLog("in didReceiveRemoteNotification")
        NSLog("%@", userInfo)
        startDownload()

        DispatchQueue.main.async {
            completionHandler(UIBackgroundFetchResult.newData)
        }
    }

}

@available(iOS 10, *)
extension AppDelegate : UNUserNotificationCenterDelegate {

    // Receive displayed notifications for iOS 10 devices.
    /*
   func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        let userInfo = notification.request.content.userInfo
        // Print message ID.
        //print("Message ID: \(userInfo["gcm.message_id"]!)")

        // Print full message.
        print("%@", userInfo)
        startDownload()  

             DispatchQueue.main.async {
                completionHandler(UNNotificationPresentationOptions.alert)
             }          
    }
    */
}

extension AppDelegate : FIRMessagingDelegate {
    // Receive data message on iOS 10 devices.
    func applicationReceivedRemoteMessage(_ remoteMessage: FIRMessagingRemoteMessage) {
        print("%@", remoteMessage.appData)
    }
}

extension AppDelegate: URLSessionDownloadDelegate {
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL){
        NSLog("finished downloading")
    }
}

The results are as follows:

When the app is in the foreground:

  1. I get the log "in startDownload func"

  2. I get the log "finished downloading".

When the app is in the background:

  1. I get the log "in startDownload func"

  2. I do not get the log "finished downloading".

  3. The silencer isn't working i.e. when the app is backgrounded, I am still getting the notification in the tray.

I am using Postman to send the request and tried the following payload, which results in the console error 'FIRMessaging receiving notification in invalid state 2':

{
    "to" : "Server_Key", 
    "content_available" : true,
    "notification": {
    "body": "Firebase Cloud Message29- BG CA1"
  }
}

I have the capabilities set for background fetch and remote notifications. The app is written in swift 3 and uses the latest Firebase

EDIT: Updated AppDelegate to include funcs as per comment

user2363025
  • 6,365
  • 19
  • 48
  • 89
  • Your code makes no sense. You are configuring a background download but where are your implementations of `application(_:handleEventsForBackgroundURLSession identifier:completionHandler:)` and`urlSessionDidFinishEvents(forBackgroundURLSession:)`??? Did you leave out half of your app delegate code or did you just forget to write the most important part? :) – matt Jan 05 '17 at 18:41
  • @matt I left out the most important part! :0 I thought the didFinishDownloadingTo was the method called on completion and the code there in would be the completion handler. Would I place these funcs in my App delegate's extension : URLSessionDownloadDelegate? I'm a bit confused on how to piece this together. Also is it after this completion handler executes code that I should expect to see the "finished downloading" print in the code above?? – user2363025 Jan 06 '17 at 09:23
  • @matt initially I had a completion handler as part of the task definition, but when running this, I got an error in the console stating that completionHandlers aren't allowed in background mode and to use a delegate instead – user2363025 Jan 06 '17 at 09:59
  • 1
    I notice that you're doing debugging with `print` statements. Do I infer from this that you are running the app from the Xcode debugger? When apps are run attached to debugger, the background behavior changes a lot. So, fine, do your initial testing this way, but you'll have to come up with some other debugging mechanism to really put the background download through its paces. I usually log with `NSLog` (or, I think you can do `os_log`, too) and then watch the console for my app. Or you can send messages to some helper app and watch via that. – Rob Jan 06 '17 at 16:56
  • @Rob your inference is correct, thanks for the tip! I'll switch to NSLog and the console once I get past this initial part – user2363025 Jan 06 '17 at 17:01

1 Answers1

2

A few observations:

  1. When your app is restarted by handleEventsForBackgroundURLSessionIdentifier, you have to not only save the completion handler, but you actually have to start the session, too. You appear to be doing the former, but not the latter.

    Also, you have to implement urlSessionDidFinishEvents(forBackgroundURLSession:) and call (and discard your reference to) that saved completion handler.

  2. You appear to be doing a data task. But if you want background operation, it has to be download or upload task. [You have edited question to make it a download task.]

  3. In userNotificationCenter(_:willPresent:completionHandler:), you don't ever call the completion handler that was passed to this method. So, when the 30 seconds (or whatever it is) expires, because you haven't called it, your app will be summarily terminated and all background requests will be canceled.

    So, willPresent should call its completion handler as soon it done starting the requests. Don't confuse this completion handler (that you're done handling the notification) with the separate completion handler that is provided later to urlSessionDidFinishEvents (that you're done handling the the background URLSession events).

  4. Your saved background session completion handler is not right. I'd suggest:

    var backgroundSessionCompletionHandler: (() -> Void)?
    

    When you save it, it is:

    backgroundSessionCompletionHandler = completionHandler   // note, no ()
    

    And when you call it in urlSessionDidFinishEvents, it is:

    DispatchQueue.main.async {
        self.backgroundSessionCompletionHandler?()
        self.backgroundSessionCompletionHandler = nil
    }
    
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • thanks for the response! I corrected the mistake and changed data to download, I also added in what I think is calling the completionhandler also. See edited question, I'm unsure how to start the session however? the startDownload func includes task.resume() – user2363025 Jan 06 '17 at 16:52
  • I've added in the urlSessionDidFinishEvents and again tried to call the completion handler, does need to be called here and in userNotificationCenter? – user2363025 Jan 06 '17 at 16:57
  • thanks for clarification, I'm still unsure on what should go in the usernotification completion handler and if I should indeed be calling it after my startDownload func? – user2363025 Jan 06 '17 at 17:24
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/132521/discussion-between-user2363025-and-rob). – user2363025 Jan 06 '17 at 17:41