14

我试图实现后台获取,希望可以不时唤醒应用程序。

我做了这些:

      func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
application.setMinimumBackgroundFetchInterval(UIApplicationBackgroundFetchIntervalMinimum)
        return true
      }

func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
    debugPrint("performFetchWithCompletionHandler")
    getData()
    completionHandler(UIBackgroundFetchResult.newData)
  }

  func getData(){
    debugPrint("getData")
  }

我也已经启用了后台获取功能。这就是我所做的一切。然后我运行应用程序。即使一个小时后该功能也从未调用过(手机睡着了)。

我还需要做什么才能使函数被调用?

4

2 回答 2

17

您已经完成了许多必要的步骤:

话虽如此,但有几点意见:

  1. 我会在“设置”»“常规”»“后台应用刷新”中检查应用的权限。这可确保您不仅成功地在 plist 中请求后台获取,而且通常启用它,特别是为您的应用程序启用。

  2. 确保您没有杀死应用程序(即双击主页按钮并向上滑动您的应用程序以强制应用程序终止)。如果应用程序被终止,它将阻止后台获取正常工作。

  3. 您正在使用debugPrint,但这仅在从 Xcode 运行时才有效。但是您应该在物理设备上执行此操作,而不是从 Xcode 运行它。即使不通过 Xcode 运行应用程序,您也需要使用记录系统来显示您的活动。

    os_log从控制台使用并观看它(参见 WWDC 2016 Unified Logging and Activity Tracing)或使用通过UserNotifications 框架发布通知(参见 WWDC 2016 Introduction to Notifications),因此当应用程序在后台执行某些显着操作时我会收到通知。或者我已经创建了我自己的外部日志系统(例如写入一些文本文件或 plist)。但是您需要某种方式来观察print/之外的活动,debugPrint因为您想在不独立于 Xcode 运行它的同时对其进行测试。运行连接到调试器的应用程序时,任何与背景相关的行为都会发生变化。

  4. 正如PGDev 所说,您无法控制后台提取何时发生。它考虑了许多记录不充分的因素(wifi 连接、连接到电源、用户的应用程序使用频率、其他应用程序何时启动等)。

    话虽如此,当我启用后台提取,从设备(不是 Xcode)运行应用程序并将其连接到 wifi 和电源时,在暂停应用程序的 10 分钟内,第一次调用的后台提取出现在我的 iPhone 7+ 上。

  5. 您的代码当前没有执行任何获取请求。这引发了两个担忧:

    • 确保测试应用程序URLSession在您运行它时(即当您正常运行应用程序时,而不是通过后台获取时)在某些时候实际发出请求。如果您有一个不发出任何请求的测试应用程序,它似乎没有启用后台提取功能。(或者至少,它会严重影响后台获取请求的频率。)

    • 据报道,如果先前的后台获取调用实际上并未导致发出网络请求,操作系统将停止向您的应用发出后续的后台获取调用。(这可能是对前一点的一种排列;它并不完全清楚。)我怀疑 Apple 正试图阻止开发人员使用后台获取机制来处理没有真正获取任何内容的任务。

  6. 请注意,您的应用没有太多时间来执行请求,因此如果您正在发出请求,您可能只想查询是否有可用数据,而不是尝试下载所有数据本身。然后,您可以启动后台会话以开始耗时的下载。显然,如果正在检索的数据量可以忽略不计,那么这不太可能成为问题,但请确保您完成您的请求相当快地调用后台完成(30 秒,IIRC)。如果您不在该时间范围内调用它,它将影响是否/何时尝试后续后台获取请求。

  7. 如果应用程序没有处理后台请求,我可能会建议从设备中删除应用程序并重新安装。我遇到过这样的情况,在测试后台获取时请求停止工作(可能是由于在测试应用程序的前一次迭代时后台获取请求失败的结果)。我发现删除并重新安装它是重置后台获取过程的好方法。


为了便于说明,这里是一个成功执行后台提取的示例。我还添加了 UserNotifications 框架和os_log调用,以提供一种在未连接到 Xcode 时监控进度的方法(即在哪里printdebugPrint不再有用):

// AppDelegate.swift

import UIKit
import UserNotifications
import os.log

@UIApplicationMain
class AppDelegate: UIResponder {

    var window: UIWindow?

    /// The URLRequest for seeing if there is data to fetch.

    fileprivate var fetchRequest: URLRequest {
        // create this however appropriate for your app
        var request: URLRequest = ...
        return request
    }

    /// A `OSLog` with my subsystem, so I can focus on my log statements and not those triggered 
    /// by iOS internal subsystems. This isn't necessary (you can omit the `log` parameter to `os_log`,
    /// but it just becomes harder to filter Console for only those log statements this app issued).

    fileprivate let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "log")

}

// MARK: - UIApplicationDelegate

extension AppDelegate: UIApplicationDelegate {

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

        // turn on background fetch

        application.setMinimumBackgroundFetchInterval(UIApplicationBackgroundFetchIntervalMinimum)

        // issue log statement that app launched

        os_log("didFinishLaunching", log: log)

        // turn on user notifications if you want them

        UNUserNotificationCenter.current().delegate = self

        return true
    }

    func applicationWillEnterForeground(_ application: UIApplication) {
        os_log("applicationWillEnterForeground", log: log)
    }

    func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
        os_log("performFetchWithCompletionHandler", log: log)
        processRequest(completionHandler: completionHandler)
    }

}

// MARK: - UNUserNotificationCenterDelegate

extension AppDelegate: UNUserNotificationCenterDelegate {

    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        os_log("willPresent %{public}@", log: log, notification)
        completionHandler(.alert)
    }

    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        os_log("didReceive %{public}@", log: log, response)
        completionHandler()
    }
}

// MARK: - Various utility methods

extension AppDelegate {

    /// Issue and process request to see if data is available
    ///
    /// - Parameters:
    ///   - prefix: Some string prefix so I know where request came from (i.e. from ViewController or from background fetch; we'll use this solely for logging purposes.
    ///   - completionHandler: If background fetch, this is the handler passed to us by`performFetchWithCompletionHandler`.

    func processRequest(completionHandler: ((UIBackgroundFetchResult) -> Void)? = nil) {
        let task = URLSession.shared.dataTask(with: fetchRequest) { data, response, error in

            // since I have so many paths execution, I'll `defer` this so it captures all of them

            var result = UIBackgroundFetchResult.failed
            var message = "Unknown"

            defer {
                self.postNotification(message)
                completionHandler?(result)
            }

            // handle network errors

            guard let data = data, error == nil else {
                message = "Network error: \(error?.localizedDescription ?? "Unknown error")"
                return
            }

            // my web service returns JSON with key of `success` if there's data to fetch, so check for that

            guard
                let json = try? JSONSerialization.jsonObject(with: data),
                let dictionary = json as? [String: Any],
                let success = dictionary["success"] as? Bool else {
                    message = "JSON parsing failed"
                    return
            }

            // report back whether there is data to fetch or not

            if success {
                result = .newData
                message = "New Data"
            } else {
                result = .noData
                message = "No Data"
            }
        }
        task.resume()
    }

    /// Post notification if app is running in the background.
    ///
    /// - Parameters:
    ///
    ///   - message:           `String` message to be posted.

    func postNotification(_ message: String) {

        // if background fetch, let the user know that there's data for them

        let content = UNMutableNotificationContent()
        content.title = "MyApp"
        content.body = message
        let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
        let notification = UNNotificationRequest(identifier: "timer", content: content, trigger: trigger)
        UNUserNotificationCenter.current().add(notification)

        // for debugging purposes, log message to console

        os_log("%{public}@", log: self.log, message)  // need `public` for strings in order to see them in console ... don't log anything private here like user authentication details or the like
    }

}

并且视图控制器仅请求用户通知的权限并发出一些随机请求:

import UIKit
import UserNotifications

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        // request authorization to perform user notifications

        UNUserNotificationCenter.current().requestAuthorization(options: [.sound, .alert]) { granted, error in
            if !granted {
                DispatchQueue.main.async {
                    let alert = UIAlertController(title: nil, message: "Need notification", preferredStyle: .alert)
                    alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
                    self.present(alert, animated: true, completion: nil)
                }
            }
        }

        // you actually have to do some request at some point for background fetch to be turned on;
        // you'd do something meaningful here, but I'm just going to do some random request...

        let url = URL(string: "http://example.com")!
        let request = URLRequest(url: url)
        let task = URLSession.shared.dataTask(with: request) { data, response, error in
            DispatchQueue.main.async {
                let alert = UIAlertController(title: nil, message: error?.localizedDescription ?? "Sample request finished", preferredStyle: .alert)
                alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
                self.present(alert, animated: true)
            }
        }
        task.resume()
    }

}
于 2017-06-02T00:11:36.647 回答
6

后台提取由系统以适当的时间间隔自动启动。

Background Fetch 的一个非常重要且很酷的功能是它能够了解应该允许应用程序启动到后台并进行更新的时间。例如,假设用户每天早上 8:30 左右使用新闻应用程序(阅读一些新闻和一些热咖啡)。使用几次后,系统会发现应用程序下次运行的时间很可能会在同一时间左右,因此请注意让它在正常启动时间之前上线并更新(可能是上午 8:00 左右)。这样,当用户打开应用程序时,新的和刷新的内容就在那里等待着他,而不是相反!此功能称为使用预测

要测试您编写的代码是否正常工作,您可以参考Raywenderlich 的Background Fetch教程。

教程https ://www.raywenderlich.com/143128/background-modes-tutorial-getting-started (搜索:测试后台获取)

于 2017-06-01T17:19:55.597 回答