You have done many of the necessary steps:
That having been said, a couple of observations:
I'd check the permissions for the app in "Settings" » "General" » "Background App Refresh". This ensures that not only did you successfully request background fetch in your plist, but that it's enabled in general, as well as for your app in particular.
Make sure you're not killing the app (i.e. by double tapping on the home button and swiping up on your app for force the app to terminate). If the app is killed, it will prevent background fetch from working correctly.
You're using debugPrint
, but that only works when running it from Xcode. But you should be doing this on a physical device, not running it from Xcode. You need to employ a logging system that shows you activity even when not running the app through Xcode.
I use os_log
and watch it from the Console (see WWDC 2016 Unified Logging and Activity Tracing) or use post a notification via the UserNotifications framework (see WWDC 2016 Introduction to Notifications) so I'm notified when app does something notable in the background. Or I've created my own external logging systems (e.g. writing to some text file or plist). But you need some way of observing the activity outside of print
/debugPrint
because you want to test this while not running it independently of Xcode. Any background-related behaviors change while running an app connected to the debugger.
As PGDev said, you don't have control over when the background fetch takes place. It considers many poorly documented factors (wifi connectivity, connected to power, user's app usage frequency, when other apps might be spinning up, etc.).
That having been said, when I enabled background fetch, ran the app from the device (not Xcode), and had it connected to wifi and power, the first background fetch called appeared on my iPhone 7+ within 10 minutes of suspending the app.
Your code isn't currently doing any fetch request. That raises two concerns:
Make sure that the test app actually issues URLSession
request at some point its normal course of action when you run it (i.e. when you run the app normally, not via background fetch). If you have a test app that doesn't issue any requests, it doesn't appear to enable the background fetch feature. (Or at the very least, it severely affects the frequency of the background fetch requests.)
Reportedly, the OS will stop issuing subsequent background fetch calls to your app if prior background fetch calls didn't actually result in a network request being issued. (This may be a permutation of the prior point; it's not entirely clear.) I suspect Apple is trying to prevent developers using background fetch mechanism for tasks that aren't really fetching anything.
Note, your app doesn't have much time to perform the request, so if you are issuing a request, you might want to inquire solely whether there is data available, but not try to download all the data itself. You can then initiate a background session to start the time consuming downloads. Obviously, if the amount of data being retrieved is negligible, then this is unlikely to be a concern, but make sure you finish your request call the background completion reasonably quickly (30 seconds, IIRC). If you don't call it within that timeframe, it will affect if/when subsequent background fetch requests are attempted.
If the app is not processing background requests, I might suggest removing the app from the device and reinstalling. I've had situation where, when testing background fetch where the requests stopped working (possibly as a result of a failed background fetch request when testing a previous iteration of the app). I find that removing and re-installing it is a good way to reset the background fetch process.
For sake of illustration, here is an example that performs background fetches successfully. I've also added UserNotifications framework and os_log
calls to provide a way of monitoring the progress when not connected to Xcode (i.e. where print
and debugPrint
no longer are useful):
// 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
}
}
And the view controller merely requests permission for user notifications and issues some random request:
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()
}
}