使用 URLSession 和后台获取以及使用 firebase 的远程通知

Posted

技术标签:

【中文标题】使用 URLSession 和后台获取以及使用 firebase 的远程通知【英文标题】:Using URLSession and background fetch together with remote notifications using firebase 【发布时间】:2017-05-20 05:30:52 【问题描述】:

我正在尝试在这里实现一个基本的函数,当我的应用程序被后台或暂停时将被调用。

实际上,我们的目标是每天发送大约 5 个,因此 Apple 不应限制我们的利用率。

我已经整理了以下使用 firebase 和 userNotifications 的内容,现在,它在我的应用委托中。

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")
    

结果如下:

当应用在前台时:

    我在“startDownload func”中得到日志

    我得到“下载完成”的日志。

当应用在后台时:

    我在“startDownload func”中得到日志

    我没有收到“下载完成”的日志。

    消音器不工作,即当应用程序在后台运行时,我仍然在托盘中收到通知。

我正在使用 Postman 发送请求并尝试了以下负载,导致控制台错误'FIRMessaging receiving notification in invalid state 2'


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

我设置了后台获取和远程通知的功能。该应用使用 swift 3 编写,并使用最新的 Firebase

编辑:更新 AppDelegate 以根据评论包含函数

【问题讨论】:

你的代码没有意义。您正在配置后台下载,但application(_:handleEventsForBackgroundURLSession identifier:completionHandler:)urlSessionDidFinishEvents(forBackgroundURLSession:) 的实现在哪里???您是否遗漏了一半的应用程序委托代码,或者您只是忘记编写最重要的部分? :) @matt 我漏掉了最重要的部分! :0 我认为 didFinishDownloadingTo 是完成时调用的方法,其中的代码将是完成处理程序。我会将这些函数放在我的 App 委托的扩展中:URLSessionDownloadDelegate 吗?我对如何拼凑起来有点困惑。也是在这个完成处理程序执行代码之后,我应该期望在上面的代码中看到“完成下载”打印吗?? @matt 最初我有一个完成处理程序作为任务定义的一部分,但是在运行它时,我在控制台中收到一个错误,指出在后台模式下不允许完成处理程序并使用委托而是 我注意到您正在使用 print 语句进行调试。我是否从中推断出您正在从 Xcode 调试器运行应用程序?当应用程序附加到调试器运行时,后台行为会发生很大变化。所以,很好,以这种方式进行初始测试,但你必须想出一些其他的调试机制才能真正完成后台下载。我通常使用NSLog(或者,我认为您也可以使用os_log)登录,然后查看我的应用程序的控制台。或者您可以向某个助手应用发送消息并通过该应用进行观看。 @Rob 您的推断是正确的,感谢您的提示!一旦我完成了这个初始部分,我将切换到 NSLog 和控制台 【参考方案1】:

一些观察:

    当您的应用程序由handleEventsForBackgroundURLSessionIdentifier 重新启动时,您不仅必须保存完成处理程序,而且实际上还必须启动会话。你似乎在做前者,但不是后者。

    此外,您必须实现 urlSessionDidFinishEvents(forBackgroundURLSession:) 并调用(并丢弃您的引用)已保存的完成处理程序。

    您似乎正在执行数据任务。但是如果你想要后台操作,它必须是下载或上传任务。 [您已编辑问题使其成为下载任务。]

    userNotificationCenter(_:willPresent:completionHandler:) 中,您永远不会调用传递给此方法的完成处理程序。因此,当 30 秒(或其他任何时间)到期时,由于您尚未调用它,您的应用将被立即终止,并且所有后台请求都将被取消。

    因此,willPresent 应该在启动请求后立即调用其完成处理程序。不要将此完成处理程序(您已完成对通知的处理)与稍后提供给 urlSessionDidFinishEvents 的单独完成处理程序(您已完成对后台 URLSession 事件的处理)混淆。

    您保存的后台会话完成处理程序不正确。我建议:

    var backgroundSessionCompletionHandler: (() -> Void)?
    

    保存的时候是:

    backgroundSessionCompletionHandler = completionHandler   // note, no ()
    

    当你在urlSessionDidFinishEvents 中调用它时,它是:

    DispatchQueue.main.async 
        self.backgroundSessionCompletionHandler?()
        self.backgroundSessionCompletionHandler = nil
    
    

【讨论】:

感谢您的回复!我纠正了错误并更改了要下载的数据,我还添加了我认为调用完成处理程序的内容。请参阅已编辑的问题,但是我不确定如何开始会话? startDownload 函数包括 task.resume() 我在 urlSessionDidFinishEvents 中添加并再次尝试调用完成处理程序,是否需要在此处和 userNotificationCenter 中调用? 感谢您的澄清,我仍然不确定 usernotification 完成处理程序中应该做什么,以及是否应该在我的 startDownload func 之后调用它? 让我们continue this discussion in chat。

以上是关于使用 URLSession 和后台获取以及使用 firebase 的远程通知的主要内容,如果未能解决你的问题,请参考以下文章

URLSession downloadTask 在后台运行时的行为?

watchOS 上的后台 URLSession - 周期是啥?

如何使用 AFNetworking 2.x 管理许多 (>500) 后台 URLSession 下载?

后台应用刷新对后台 URLSession 有影响吗?

NSURLSessionDownloadTask 在后台发起URLSession completionHandler

测试后台 URLSession 的最佳方法是啥?