I have tried to implement background fetch, to hopefully can wake the app from time to time.
I have done these:
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")
}
I have also enable background fetch capabilities already. That's all i have done. And then i run the app. the function never called even after an hour (the phone slept).
What other things i have to do to make the function get called?
You have done many of the necessary steps:
Turned on "background fetch" the the "Capabilities" tab of your project;
Implemented application(_:performFetchWithCompletionHandler:);
Called setMinimumBackgroundFetchInterval(_:) in application(_:didFinishLaunchingWithOptions:).
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()
}
}
Background Fetch is automatically initiated by the system at appropriate intervals.
A very important and cool feature of the Background Fetch is its
ability to learn the times that should allow an app to be launched to
the background and get updated. Let’s suppose for example that a user
uses a news app every morning about 8:30 am (read some news along with
some hot coffee). After a few times of usage, the system learns that
it’s quite possible that the next time the app will run will be around
the same time, so it takes care to let it go live and get updated
before the usual launch time (it could be around 8:00 am). That way,
when the user opens the app the new and refreshed content is there
awaiting for him, and not the opposite! This feature is called usage
prediction.
For testing whether the code you wrote works properly or not, you can refer to Raywenderlich's tutorial on Background Fetch.
Tutorial: https://www.raywenderlich.com/143128/background-modes-tutorial-getting-started
(Search for: Testing Background Fetch)
Related
I'm making an app for Apple Watch that needs to wake the iPhone's counterpart app which loads a site via a WKWebView, takes a snapshot, and sends the image back.
It works perfectly when the iPhone app is on-screen, intermittently when it's running in the background, but not at all when the app is completely closed.
Is there any way to get the iPhone app to wake up in the background with WCSession's sendMessage? I've read that it's meant to but I haven't been able to get it working. Is it because the iPhone app doesn't send a reply to the initial message sent by the watch (the file that the iPhone sends back has to wait for the WKWebView to finish loading, so it can't be sent back in replyHandler)? Is there a plist setting I forgot to toggle?
The current workflow of this code is as follows:
On the Apple Watch, the user taps a button which triggers the already activated WCSession's sendMessage function in ExtensionDelegate.
The iPhone app receives it using the WCSession that it activated in AppDelegate.
In didRecieve, the iPhone app feeds a URL into a WKWebView and starts loading it.
In WKWebView's didFinish function, it takes a snapshot of the site and sends it back to the watch with transferFile.
The watch receives the snapshot and passes it back to the right ViewController.
All of these steps have been tested and verified to work while both apps are on-screen, but as soon as the iPhone enters the background or has its counterpart app closed, this workflow becomes very unstable.
The relevant code is below:
After the user presses the button, the ViewController fires a notification to ExtensionDelegate with the information to transmit over WCSession.
ExtensionDelegate (sending the message):
#objc func transmit(_ notification: Notification) {
// The paired iPhone has to be connected via Bluetooth.
if let session = session, session.isReachable {
session.sendMessage(["SWTransmission": notification.userInfo as Any],
replyHandler: { replyData in
// handle reply from iPhone app here
print(replyData)
}, errorHandler: { error in
// catch any errors here
print(error)
})
} else {
// when the iPhone is not connected via Bluetooth
}
}
The iPhone app (should, but doesn't) wakes up and activates the WCSession:
fileprivate let session: WCSession? = WCSession.isSupported() ? WCSession.default : nil
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
session?.delegate = self
session?.activate()
webView.navigationDelegate = self
webView.scrollView.contentInsetAdjustmentBehavior = .never
return true
}
The iPhone app receives the message in AppDelegate, and activates the WKWebView. Note that there isn't a configured reply. Could this be the cause of my issue?
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: #escaping ([String : Any]) -> Void) {
DispatchQueue.main.async { [self] in
let dictionary = message["SWTransmission"] as! [String: Any]
let link = URL(string: dictionary["URL"] as! String)!
let request = URLRequest(url: link)
webView.frame = CGRect(x: 0, y: 0, width: Int(((dictionary["width"] as! Double) * 1.5)), height: dictionary["height"] as! Int)
webView.load(request)
}
}
[Still in AppDelegate] After the site is loaded, didFinish (should) gets activated, where it takes a snapshot and sends the file back to the watch via transferFile.
func getDocumentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return paths[0]
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.takeSnapshot(with: nil) { [self] (image, error) in
let filename = getDocumentsDirectory().appendingPathComponent("webImage.jpg")
if let data = image!.jpegData(compressionQuality: 0.8) {
try? data.write(to: filename)
}
self.session?.transferFile(filename, metadata: nil)
}
}
The Apple Watch receives the file in ExtensionDelegate and sends it back to the relevant ViewController:
func session(_ session: WCSession, didReceive file: WCSessionFile) {
DispatchQueue.main.async { [self] in
do {
NotificationCenter.default.post(name: NSNotification.Name("openSite"), object: nil, userInfo: ["imageURL": file.fileURL] as [String: Any])
} catch {
print(error)
}
}
}
Thank you very much for your help!
I do not have experience working on WatchOS, but I took a look at the documentation for WCSession and it seems that what you are observing exactly matches the expected behaviour.
You mention
"It works perfectly when the iPhone app is on-screen, intermittently when it's running in the background, but not at all when the app is completely closed"
The Apple documentation for WCSession states
When both session objects are active, the two processes can communicate immediately by sending messages back and forth. When only one session is active, the active session may still send updates and transfer files, but those transfers happen opportunistically in the background.
These two align perfectly.
When the app is on-screen, both session objects are apparently active, and as per your observation under this scenario you see the communication happening everytime.
When the app is in background, the session object on the app side appears to be not active, and the transfer would take place 'opportunistically', which is in-line with you observation that the communication occurs intermittently.
When the the app is completely closed, it cannot be launched by the system under any circumstance as far as I know, and this is also in-line with your observation that in-this situation the communication never happens.
Unless you've already read through the WCSession documentation, I would suggest that you go through it. I see that you are checking for WCSession's isReachable property, however, another important property that is mentioned on the documentation page is activationState. It may be worth checking the value of this property before initiating the communication.
I need to update orders from my app while app is in background.
Ok, I am using OneSignal, I can get message on didReceiveRemoteNotification and inside it, I call Alamofire to check on my api what I need to update.
The problem is when the code get to the point: Alamofire.request(url).responseJSON {(response) in it doesnt go inside, just when I open the app I can get the result.
I would like it to get the new data on background and notify users after updating, so they can click on the notification to see whats is new.
I read that Alamofire runs on a background thread by default, but the network request goes on Main thread.
So, I tried: this and this, both don't work.
I tried URLSessionConfiguration but I got Error code -999 cancelled.
So, I added sessionManager.session.finishTasksAndInvalidate() in the end of my response. The error stops, but the code still don't go inside Alamofire request.
Some of my code - didReceiveRemoteNotification on my AppDelegate:
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
if let custom = userInfo["custom"] as? NSDictionary {
if let a = custom["a"] as? NSDictionary {
if let update = a["update"] as? NSString {
if update.isEqual(to: "Pedido") {
let strdataPedido = PedidoDAO().getMostRecentDtPedido()
if self.defaults.getDownloadInicialPedido(){
if strdataPedido != "" {
//let task = self.beginBackgroundTask()
self.loadOrdersFromLastSyncByApi(strdataPedido)
//self.endBackgroundTask(task)
}
}
}
}
}
}
loadOrdersFromLastSyncByApi function on my AppDelegate:
func loadOrdersFromLastSyncByApi(_ lastSync: String) {
let parceiroId = defaults.getParceiroId()
PedidoAPI().loadOrdersForLastSync(parceiroId, lastSync){ (dados) in
if let dadosPedidoModel = dados as? [PedidoModel] {
//do what needs to do to save new data
}
PedidoAPI().loadOrdersForLastSync function:
func loadOrdersForLastSync(_ parceiroId: String, _ lastSync: String, _ onComplete: #escaping(Any?) -> Void) {
let url = Test.basePath + "/api/orders/parceiro/\(parceiroId)/\(lastSync)"
//let queue = DispatchQueue(label: "com.test.br", qos: .background, attributes: .concurrent)
let task = self.beginBackgroundTask()
let queue = DispatchQueue.global(qos: .background)
queue.async {
Alamofire.request(url).responseJSON {(response) in
//This result its fired just when I open my app, I would like it to make everything on background
switch (response.result) {
//do what needs to send new data
}
self.endBackgroundTask(task)
Any help please?
Thanks for your question. So to clarify, I understood your question as the following:
You want to update users when there has been a change in their orders while the app is in background.
Follow-up Question:
Is there a reason you want to make the request from the client? Also, what data is coming in from OneSignal that you couldn't just handle on your server?
Answer:
You should handle any requests to a third party (Alamofire) on the server and then use our API to send a notification to the user with new info from the request response. I think that would be the best approach.
I'm using Firebase Messaging (Notifications) to send push reminders to users on iOS. For my app, that is a todo app, I'm using Swift 3. When the user gets the push notification I want them to be able to complete the task right from the push notification.
Everything works almost great. The user gets the push. When they 3d-touch they see the "complete button". When the "complete button" is tapped the didReceive response method in the app is triggered in the background.
Now to the problem, in that method I'm using a closure and then a closure in that closure. For some reason the first part of the code runs in the background without the user opening the app but the last part is only running when the user opens the app again (see below). Why is that?
This is my code:
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: #escaping () -> Void) {
let userInfo = response.notification.request.content.userInfo
if response.actionIdentifier == notificationActionComplete, let actionKey = userInfo["actionKey"] as? String {
getAction(actionKey: actionKey, completion: { (action) in
action.complete {
}
})
}
completionHandler()
}
func getAction(actionKey: String, completion:#escaping (Action)->Void) {
Database.database().reference(withPath: "actions/\(actionKey)").observeSingleEvent(of: .value, with: { snapshot in
let action = Action(snapshot: snapshot)
completion(action)
})
}
In action class:
var ref: DatabaseReference?
init(snapshot: DataSnapshot) {
key = snapshot.key
ref = snapshot.ref
//Other inits here
}
func complete(completion:#escaping (Void) -> Void) {
//This code to remove the node is running fine in background
ref.removeValue { (error, ref) in
//The code in here is not running until the user opens the app next time
otherRef.updateChildValues(self.toAnyObject(), withCompletionBlock: { (error, ref) in
completion()
})
}
Your app is basically suspended after the runloop cycle where userNotificationCenter() is called, so if your completion handler is in response to asynchronous work, that work will never happen until your app resumes again. To get around this you will probably need to begin a background task inside that function, and then have your completion handler end the background task when it is finished. This tells the system you need to stay alive for a while in the background (although it is not guaranteed, if you take too long)
See "Executing Finite Limit Tasks" at this URL (sorry, it's Obj-C, but there should be a Swift way to do it too):
https://developer.apple.com/library/content/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/BackgroundExecution/BackgroundExecution.html
I'd like to add to my Watch app functionality which send to iPhone app a Local Notification (while iPhone app is on the background or iPhone is locked).
I know how to create Local Notification itself.
What Im asking for is way, how to trigger background process (which contains also Local Notification) on iPhone by (for example) tapping on button on Apple Watch.
WKInterfaceController.openParentApplication is the official way to communicate with the iPhone. Documentation.
You pass parameters in the userInfo dictionary and retrieve results via the reply block.
On the iPhone the request is handled by appDelegate's handleWatchKitExtensionRequest method. Documentation
Code in my InterfaceController.swift:
#IBAction func btn() {
sendMessageToParentApp("Button tapped")
}
// METHODS #2:
func sendMessageToParentApp (input:String) {
let dictionary = ["message":input]
WKInterfaceController.openParentApplication(dictionary, reply: { (replyDictionary, error) -> Void in
if let castedResponseDictionary = replyDictionary as? [String:String], responseMessage = castedResponseDictionary["message"] {
println(responseMessage)
self.lbl.setText(responseMessage)
}
})
}
Next i made new method in my AppDelegate.swift:
func application(application: UIApplication, handleWatchKitExtensionRequest userInfo: [NSObject : AnyObject]?, reply: (([NSObject : AnyObject]!) -> Void)!) {
if let infoDictionary = userInfo as? [String:String], message = infoDictionary["message"] {
let response = "iPhone has seen this message." // odešle se string obsahující message (tedy ten String)
let responseDictionary = ["message":response] // tohle zase vyrobí slovník "message":String
NSNotificationCenter.defaultCenter().postNotificationName(notificationWatch, object: nil)
reply(responseDictionary)
}
}
As you can see I use Notification to get iOS app know that button has been tapped. In ViewController.swift I have Notification Observer and function which is executed every time observer catch notification that user tapped on button on watch ("notificationWatch" is global variable with notification key). Hope this will help to anybody.
I am using Alamofire to download data
How to make alamofire run download in background with swift?
Thanks
The basic idea is as follows:
The key problem is that with background downloads, your app may actually be terminated while downloads are in progress (e.g. jettisoned due to memory pressure). Fortunately, your app is fired up again when background downloads are done, but any task-level closures you originally supplied are long gone. To get around this, when using background sessions, one should rely upon session-level closures used by the delegate methods.
import UIKit
import Alamofire
import UserNotifications
fileprivate let backgroundIdentifier = ...
fileprivate let notificationIdentifier = ...
final class BackgroundSession {
/// Shared singleton instance of BackgroundSession
static let shared = BackgroundSession()
/// AlamoFire `SessionManager`
///
/// This is `private` to keep this app loosely coupled with Alamofire.
private let manager: SessionManager
/// Save background completion handler, supplied by app delegate
func saveBackgroundCompletionHandler(_ backgroundCompletionHandler: #escaping () -> Void) {
manager.backgroundCompletionHandler = backgroundCompletionHandler
}
/// Initialize background session
///
/// This is `private` to avoid accidentally instantiating separate instance of this singleton object.
private init() {
let configuration = URLSessionConfiguration.background(withIdentifier: backgroundIdentifier)
manager = SessionManager(configuration: configuration)
// specify what to do when download is done
manager.delegate.downloadTaskDidFinishDownloadingToURL = { _, task, location in
do {
let destination = try FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
.appendingPathComponent(task.originalRequest!.url!.lastPathComponent)
try FileManager.default.moveItem(at: location, to: destination)
} catch {
print("\(error)")
}
}
// specify what to do when background session finishes; i.e. make sure to call saved completion handler
// if you don't implement this, it will call the saved `backgroundCompletionHandler` for you
manager.delegate.sessionDidFinishEventsForBackgroundURLSession = { [weak self] _ in
self?.manager.backgroundCompletionHandler?()
self?.manager.backgroundCompletionHandler = nil
// if you want, tell the user that the downloads are done
let content = UNMutableNotificationContent()
content.title = "All downloads done"
content.body = "Whoo, hoo!"
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
let notification = UNNotificationRequest(identifier: notificationIdentifier, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(notification)
}
// specify what to do upon error
manager.delegate.taskDidComplete = { _, task, error in
let filename = task.originalRequest!.url!.lastPathComponent
if let error = error {
print("\(filename) error: \(error)")
} else {
print("\(filename) done!")
}
// I might want to post some event to `NotificationCenter`
// so app UI can be updated, if it's in foreground
}
}
func download(_ url: URL) {
manager.download(url)
}
}
Then I can just initiate those downloads. Note, I do not specify any task-specific closure when I initiate the download, but rather merely use the above session-level closures that use the details of the URLSessionTask to identify what to do:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// request permission to post notification if download finishes while this is running in background
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, error in
if let error = error, !granted {
print("\(error)")
}
}
}
#IBAction func didTapButton(_ sender: Any) {
let urlStrings = [
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/s72-55482.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo10/hires/as10-34-5162.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo-soyuz/apollo-soyuz/hires/s75-33375.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/as17-134-20380.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/as17-140-21497.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/as17-148-22727.jpg"
]
let urls = urlStrings.flatMap { URL(string: $0) }
for url in urls {
BackgroundSession.shared.download(url)
}
}
}
If your app isn't running when the downloads finish, iOS needs to know that, after it restarted your app, when you're all done and that it can safely suspend your app. So, in handleEventsForBackgroundURLSession you capture that closure:
class AppDelegate: UIResponder, UIApplicationDelegate {
...
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: #escaping () -> Void) {
BackgroundSession.shared.saveBackgroundCompletionHandler(completionHandler)
}
}
That is used by sessionDidFinishEventsForBackgroundURLSession, in step 1.
Two observations:
This is only called if your app was not running when the downloads finish.
If doing background sessions, though, you must capture this closure and call it when you're all done processing the background session delegate methods.
So, to recap, the basic limitations of background sessions are:
You can only use download and upload tasks while the app is in background;
You can only rely upon session-level delegates because the app may have been terminated since the requests were initiated; and
In iOS, you must implement handleEventsForBackgroundURLSession, capture that completion handler, and call it when your background process is done.
I must also point out that while Alamofire is a wonderful library, it's not actually adding a lot value (above and beyond what is provided by URLSession to this background download process). If you're doing simple uploads/downloads only, then you might consider just using URLSession directly. But if you are using Alamofire in your project already or if your requests consist of more complicated application/x-www-form-urlencoded requests (or whatever) which merit the advantages of Alamofire, then the above outlines the key moving parts involved in the process.