您已经完成了许多必要的步骤:
话虽如此,但有几点意见:
我会在“设置”»“常规”»“后台应用刷新”中检查应用的权限。这可确保您不仅成功地在 plist 中请求后台获取,而且通常启用它,特别是为您的应用程序启用。
确保您没有杀死应用程序(即双击主页按钮并向上滑动您的应用程序以强制应用程序终止)。如果应用程序被终止,它将阻止后台获取正常工作。
您正在使用debugPrint
,但这仅在从 Xcode 运行时才有效。但是您应该在物理设备上执行此操作,而不是从 Xcode 运行它。即使不通过 Xcode 运行应用程序,您也需要使用记录系统来显示您的活动。
我os_log
从控制台使用并观看它(参见 WWDC 2016 Unified Logging and Activity Tracing)或使用通过UserNotifications 框架发布通知(参见 WWDC 2016 Introduction to Notifications),因此当应用程序在后台执行某些显着操作时我会收到通知。或者我已经创建了我自己的外部日志系统(例如写入一些文本文件或 plist)。但是您需要某种方式来观察print
/之外的活动,debugPrint
因为您想在不独立于 Xcode 运行它的同时对其进行测试。运行连接到调试器的应用程序时,任何与背景相关的行为都会发生变化。
正如PGDev 所说,您无法控制后台提取何时发生。它考虑了许多记录不充分的因素(wifi 连接、连接到电源、用户的应用程序使用频率、其他应用程序何时启动等)。
话虽如此,当我启用后台提取,从设备(不是 Xcode)运行应用程序并将其连接到 wifi 和电源时,在暂停应用程序的 10 分钟内,第一次调用的后台提取出现在我的 iPhone 7+ 上。
您的代码当前没有执行任何获取请求。这引发了两个担忧:
确保测试应用程序URLSession
在您运行它时(即当您正常运行应用程序时,而不是通过后台获取时)在某些时候实际发出请求。如果您有一个不发出任何请求的测试应用程序,它似乎没有启用后台提取功能。(或者至少,它会严重影响后台获取请求的频率。)
据报道,如果先前的后台获取调用实际上并未导致发出网络请求,操作系统将停止向您的应用发出后续的后台获取调用。(这可能是对前一点的一种排列;它并不完全清楚。)我怀疑 Apple 正试图阻止开发人员使用后台获取机制来处理没有真正获取任何内容的任务。
请注意,您的应用没有太多时间来执行请求,因此如果您正在发出请求,您可能只想查询是否有可用数据,而不是尝试下载所有数据本身。然后,您可以启动后台会话以开始耗时的下载。显然,如果正在检索的数据量可以忽略不计,那么这不太可能成为问题,但请确保您完成您的请求相当快地调用后台完成(30 秒,IIRC)。如果您不在该时间范围内调用它,它将影响是否/何时尝试后续后台获取请求。
如果应用程序没有处理后台请求,我可能会建议从设备中删除应用程序并重新安装。我遇到过这样的情况,在测试后台获取时请求停止工作(可能是由于在测试应用程序的前一次迭代时后台获取请求失败的结果)。我发现删除并重新安装它是重置后台获取过程的好方法。
为了便于说明,这里是一个成功执行后台提取的示例。我还添加了 UserNotifications 框架和os_log
调用,以提供一种在未连接到 Xcode 时监控进度的方法(即在哪里print
和debugPrint
不再有用):
// 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()
}
}