I'm following a sample code to download several images and show them in cells. For that, I configured an URLSession like this:
let backgroundConfig = URLSessionConfiguration.background(withIdentifier: "com.myexample.images")
self.backgroundSession = URLSession(configuration: backgroundConfig, delegate: self, delegateQueue: nil)
Then, I perform the download of an image like this:
func downloadImage(imageUrl url: URL, imageId: Int, completion: ImageResult?) -> URLSessionDownloadTask? {
let request = URLRequest(url: url)
let task = backgroundSession.downloadTask(with: request)
// Code here to keep track of the completion handler for this task
task.resume()
return task
}
I also conform to URLSessionDownloadDelegate and implement its didCompleteWithError, didFinishDownloadingTo and urlSessionDidFinishEvents methods. For this last method, I have this implementation:
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
if let appDelegate = UIApplication.shared.delegate as? AppDelegate, let completionHandler = appDelegate.backgroundSessionCompletionHandler {
appDelegate.backgroundSessionCompletionHandler = nil
completionHandler()
}
}
And then in AppDelegate:
var backgroundSessionCompletionHandler: (() -> Void)?
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: #escaping () -> Void) {
backgroundSessionCompletionHandler = completionHandler
}
I don't know if this is actually working. I run the app in a simulator, I go to phone's Home to put the app in background state and I'm not able to see if neither urlSessionDidFinishEvents nor app delegate's handleEventsForBackgroundURLSession methods are called. Maybe because the downloads are too fast to see this.
How could I properly test this in a simulator? Am I missing something?
call this method downloadImage on BackgroundFetch of Appdelegate
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Swift.Void)
and then in debug mode, run app on device, then lock your phone and then from XCode - Debug Menu, select Simulate Background Fetch
Related
I have an IOS native app with Pushwoosh notifications.
When I put the app in background mode (Not killed), and then received a push, methon onMessageOpened called, even the user didn’t open the push.
My onMessageOpened method:
class ViewController: UIViewController, WKUIDelegate, WKNavigationDelegate, PWMessagingDelegate {
...
...
func pushwoosh(_ pushwoosh: Pushwoosh!, onMessageOpened message: PWMessage!) {
var url = message.payload["url"] as! String
let myURL = URL(string:url)
let myRequest = URLRequest(url: myURL!)
self.webView.load(myRequest)
}
}
This is in my AppDelegate:
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
Pushwoosh.sharedInstance()?.handlePushReceived(userInfo)
completionHandler(.noData)
}
In addition I have a difficult with debugging this issue. Because I received push only in production version. Not in debug mode.
EDIT:
After I add a log calls, I can see it called to onMessageOpened just after receiving a push. You can see the low latency between records.
This is my new code:
func pushwoosh(_ pushwoosh: Pushwoosh!, onMessageReceived message: PWMessage!) {
NSLog("PushWoosh: message recevied!")
}
func pushwoosh(_ pushwoosh: Pushwoosh!, onMessageOpened message: PWMessage!) {
NSLog("PushWoosh: message opened!")
...
}
And this is my logs after a few hours of device was untouched.
I want my Swift iOS application to fetch the URL it receives via the NSUserActivityTypeBrowsingWeb request.
My email service-provider "wraps" the links (like most of them do), returning a "302 redirect" message. I want to do the initial GET, to get "302 Found" and "Location" header, but I don't need iOS to follow the redirect for me.
I've added the following code:
// Follow the link to trigger click tracking
let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
guard let data = data else { return }
print(String(data: data, encoding: .utf8)!)
}
task.resume()
This works, but I don't need to follow the redirect and fetch all of the data (as it can be quite a large web page).
Does iOS provide a way of getting the initial HTTP(S) response without following the redirect? (A bit like omitting the --location flag on CURL).
You should implement your custom session with URLSessionTaskDelegate extension and then you can interrupt redirection inside willPerformHTTPRedirection method:
class Redirect : NSObject {
var session: URLSession?
override init() {
super.init()
session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
}
func makeRequest() {
let url = URL(string: "http://gmail.com")!
let task = session?.dataTask(with: url) {(data, response, error) in
guard let data = data else {
return
}
print(String(data: data, encoding: .utf8)!)
}
task?.resume()
}
}
extension Redirect: URLSessionDelegate, URLSessionTaskDelegate {
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: #escaping (URLRequest?) -> Void) {
// Stops the redirection, and returns (internally) the response body.
completionHandler(nil)
}
}
let r = Redirect()
r.makeRequest()
Outputs:
<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
<H1>301 Moved</H1>
The document has moved
here.
</BODY></HTML>
Thanks to iUrii and kleids I got this working, a few points to note:
A callback is needed to get data out of the async task
Updating UILabels can only happen from the main queue (!!) so the DispatchQueue.main.async {} is needed
//
// AppDelegate.swift
// testlinks
//
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
return true
}
func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: #escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
// First attempt at handling a universal link
print("Continue User Activity called: ")
if userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL {
//handle URL
let r = Redirect()
r.makeRequest(url: url, callback: { (location) in
guard let locationURL = location else {return}
print("locationURL", locationURL)
// Show this on our simple example app
DispatchQueue.main.async {
if let vc = self.window?.rootViewController as? ViewController {
vc.result.text = url.absoluteString
vc.originalURL.text = locationURL.absoluteString
}
}
})
}
return true
}
}
// More efficient click-tracking with HTTP GET to obtain the "302" response, but not follow the redirect through to the Location.
// The callback is used to return the Location header back from the async task = thanks #kleids
class Redirect : NSObject {
var session: URLSession?
override init() {
super.init()
session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
}
func makeRequest(url: URL, callback: #escaping (URL?) -> ()) {
let task = self.session?.dataTask(with: url) {(data, response, error) in
guard response != nil else {
return
}
if let response = response as? HTTPURLResponse {
if let l = response.value(forHTTPHeaderField: "Location") {
callback(URL(string: l))
}
}
}
task?.resume()
}
}
extension Redirect: URLSessionDelegate, URLSessionTaskDelegate {
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: #escaping (URLRequest?) -> Void) {
// Stops the redirection, and returns (internally) the response body.
completionHandler(nil)
}
}
I want to make an additional HTTP request after background download/upload in order to confirm that the application finished downloading/uploading. Let me show you a simple example.
First we need to create download/upload task.
let configuration = URLSessionConfiguration.background(withIdentifier: UUID().uuidString)
configuration.sessionSendsLaunchEvents = true
configuration.isDiscretionary = true
let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
session.downloadTask(with: largeFileURL).resume()
Then we need to fire some additional request after download/upload finishes. In order to prevent application from being suspended I'm using background task.
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(expirationHandler: { [weak self] in
finishBackgroundTask()
})
let task = URLSession.shared.dataTask(with: someURL) { data, response, error in
// Process response.
finishBackgroundTask()
}
task.resume()
}
private func finishBackgroundTask() {
UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
backgroundTaskIdentifier = .invalid
}
The last thing is to implement application delegate method:
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: #escaping () -> Void) {
}
Question
Is it a proper way to make some work after background transfer?
The best approach, if memory serves, is to start the new request before you call the completion block. Be aware, however, that no matter how you do it, if you repeatedly make short requests, the OS will rapidly increase the delay between when a background download finishes and when your app gets relaunched in the background to handle the session events.
I propose to create a completionHandler into your AppDelegate
var backgroundSessionCompletionHandler: (() -> Void)?
Then in the handleEventsForBackgroundURLSession UIApplicationDelegate's method you define you completion handler
func application(_ application: UIApplication, handleEventsForBackgroundURLSession
identifier: String, completionHandler: #escaping () -> Void) {
backgroundSessionCompletionHandler = {
// Execute your additional HTTP request
}
}
The last step is to call this completion handler when the download is finished
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
if let completionHandler = appDelegate.backgroundSessionCompletionHandler {
appDelegate.backgroundSessionCompletionHandler = nil
DispatchQueue.main.async(execute: {
completionHandler()
})
}
}
}
I hope this helps.
Alright, so I've been trying really hard to get the answer to this but I still can't get Alamofire to do its magic when I simulate a background refresh via Xcode.
This is what my said function in app delegate looks like
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
let vc = ViewController()
vc.fetchData()
vc.sendNoti(currentFlightRules: currentFlightRules)
completionHandler(.newData)
print("Background Fetch Complete! UP")
}
fetchData() is the one that has the API request via Alamofire and it doesn't run whereas the sendNoti() does. I know that this implementation is running as when I simulate the background refresh the print statement executes.
I'd really appreciate some help with this as I'm not familiar with how to do tasks in the background and especially networking tasks in the background. Thanks in advance.
Your problem is that fetchData will complete asynchronously. This means that sendNotification and the completionHandler will be called before the network operation has been completed.
You need your fetchData function to accept and invoke a completion handler when it has the data. You then invoke the notification method and the background fetch completionHandler from that closure:
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
let vc = ViewController()
vc.fetchData { (error, data) in
guard error == nil else {
print("There was an error \(error!.localizedDescription)")
completionHandler(.failed)
return
}
if let newData = data {
vc.sendNoti(currentFlightRules: newData)
completionHandler(.newData)
} else {
completionHandler(.noData)
}
}
}
I would also suggest that you refactor so that your fetch and notification operations are not included in a view controller; they should be separated out into their own classes.
I start a download in an action extension (ActionRequestHandler) like this:
private lazy var urlSession: URLSession = {
let config = URLSessionConfiguration.background(withIdentifier: "de.stefantrauth.Downloader")
config.sharedContainerIdentifier = "group.de.stefantrauth.Downloader"
return URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue.main)
}()
private func initiateDownloadOfFileFrom(url: URL) {
urlSession.downloadTask(with: url).resume()
completeRequest() // this tells the system the action extension is done with its work
}
Then the download is processed by iOS in the background.
I now want to handle the finished download in my main application AppDelegate, because that is what iOS calls when the download has finished.
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: #escaping () -> Void) {
print("handleEventsForBackgroundURLSession")
urlSessionBackgroundCompletionHandler = completionHandler
}
This method gets called in background after some time as expected.
My AppDelegate also implements URLSessionDelegate and URLSessionDownloadDelegate to process updates for the download.
Especially interesting are
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
DispatchQueue.main.async {
print("urlSessionDidFinishEvents")
self.urlSessionBackgroundCompletionHandler?()
self.urlSessionBackgroundCompletionHandler = nil
}
}
and
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
print("download finished to \(location.absoluteString)")
do {
let documentsURL = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
let savedURL = documentsURL.appendingPathComponent(location.lastPathComponent)
try FileManager.default.moveItem(at: location, to: savedURL)
print("moved file to: \(savedURL.absoluteString)")
} catch {
print ("file error: \(error)")
}
}
Both urlSessionDidFinishEvents and didFinishDownloadingTo are not being called after handleEventsForBackgroundURLSession got called in background. Only after relaunching the app into foreground the delegate methods get called.
Why are they not getting called and what can I do to fix that?
I tried creating the URLSession in handleEventsForBackgroundURLSessionlike this:
private func initUrlSessionWith(identifier: String) {
let config = URLSessionConfiguration.background(withIdentifier: identifier)
config.sharedContainerIdentifier = "group.de.stefantrauth.Downloader"
urlSession = URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue.main)
}
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: #escaping () -> Void) {
print("handleEventsForBackgroundURLSession")
initUrlSessionWith(identifier: identifier)
urlSessionBackgroundCompletionHandler = completionHandler
}
However this did not fix the problem.
Before you ask: Yes I am testing this on a real device because the simulator has problems with background task handling.
My code is actually correct. Just my way of debugging was the problem. After using console logging instead of debugging via Xcode it was working fine.
See https://forums.developer.apple.com/message/263807#263807 for more details on how to work with background url session debugging.