How to configure Background App Refresh using Swift? - ios

I have following function to download JSON data in my SeachVC (UIViewController) which works perfect.
func downloadJSON(){
guard let url = URL(string: "myURL") else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
do {
let downloadedCurrencies = try JSONDecoder().decode([Currency].self, from: data)
// Adding downloaded data into Local Array
Currencies = downloadedCurrencies
} catch let jsonErr {
print("Here! Error serializing json", jsonErr)
}
}.resume()
}
To implement Background App Refresh, I added following functions into App Delegate;
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
// Background App Refresh Config
UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplicationBackgroundFetchIntervalMinimum)
return true
}
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
if let VC = window?.rootViewController as? SearchVC {
// Update JSON data
VC.downloadJSON()
completionHandler(.newData)
}
}
However, when I simulate Background App Refresh on the simulator, I get warning:
Warning: Application delegate received call to -application:performFetchWithCompletionHandler: but the completion handler was never called.
Where I am going to implement completion handler and how?
Thank you

You will need to move your downloading code from the view controller and into another class or at least modify you current background refresh method to instantiate the view controller if required. Background refresh can be triggered when your app hasn't been launched in the foreground, so the if let will fall through.
Consider the code in your question:
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
if let VC = window?.rootViewController as? SearchVC {
// Update JSON data
VC.downloadJSON()
completionHandler(.newData)
}
}
If the if let... doesn't pass then you exit from the function without calling the completionHandler, so you get the runtime warning that the completion handler was not called.
You could modify your code to include a call to the completionHandler in an else case, but in this case no fetch will have taken place:
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
if let VC = window?.rootViewController as? SearchVC {
// Update JSON data
VC.downloadJSON()
completionHandler(.newData)
} else {
completionHandler(.noData)
}
Or you could instantiate the view controller (or I would suggest another data fetching class) if required:
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
let vc = (window?.rootViewController as? SearchVC) ?? SearchVC()
// Update JSON data
vc.downloadJSON()
completionHandler(.newData)
}
You should also modify your downloadJSON function to include a completion handler argument, which you invoke when the JSON download is complete. This will let you call the background fetch completion handler once you have actually downloaded the data:
func downloadJSON(completion: ((Bool,Error?) -> Void )? = nil)) {
guard let url = URL(string: "myURL") else {
completion?(false, nil)
return
}
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard nil == err else {
completion?(false, err)
return
}
guard let data = data else {
completion?(false, nil)
return
}
do {
let downloadedCurrencies = try JSONDecoder().decode([Currency].self, from: data)
// Adding downloaded data into Local Array
Currencies = downloadedCurrencies
completion(true,nil)
} catch let jsonErr {
print("Here! Error serializing json", jsonErr)
completion?(false,jsonErr)
}
}.resume()
}
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
let vc = (window?.rootViewController as? SearchVC) ?? SearchVC()
// Update JSON data
vc.downloadJSON() { (newData,error) in
if let err = error {
NSLog("Background fetch error: \(err.localizedDescription)")
completionHandler(.fail)
} else {
completionHandler(newData ? .newData:.noData)
}
}
}
Update September 2019
Note that iOS 13 introduces new background fetch and processing functionality. Refer to this WWDC session for more details

It's propably because you don't call the completionHandler at the else-case (which will never happen but the compiler doesn't know)
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
if let VC = window?.rootViewController as? SearchVC {
// Update JSON data
VC.downloadJSON()
completionHandler(.newData)
} else {
completionHandler(.failed)
}
}

Related

Swift / iOS: issue HTTP GET request without following redirects

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)
}
}

Swift Warning: Application delegate received call to -application:performFetchWithCompletionHandler: but the completion handler was never called

I am calling this function below in the simulator to simulate background fetch.
Then I get this warning in the log:
Swift Warning: Application delegate received call to -application:performFetchWithCompletionHandler: but the completion handler was never called.
I have seen other Stack Iverflow answers say I just need to add completionhandler(). I've tried this and it says I need to add a parameter and that's where I am lost.
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler:#escaping (UIBackgroundFetchResult) -> Void) {
let db = Firestore.firestore()
guard let uid = Auth.auth().currentUser?.uid else { return }
//check if user online
let docRef = db.collection("Users").document(uid)
docRef.getDocument { (document, error) in
if let document = document {
if document.exists {
guard let dictionary = document.data() else { return }
guard let onlineOfflineStatus = dictionary["Online Offline Status"] as? String else { return }
// if online create value to set offline an alert
if onlineOfflineStatus == "Online" {
print("user is Online and inactive, will set value to trigger notification asking if they would like to go offline")
db.collection("sendGoOffline").document(uid).setData(["OfflineAlert" : 1200], completion: { (error) in
if let error = error {
print("there was an error", error)
}
})
}
}
}
if let error = error {
print("failed to fetch user", error)
}
}
}
The warning is telling you to add this method:
completionHandler(argument)
where the argument is one of the following:
UIBackgroundFetchResult.noData
UIBackgroundFetchResult.newData
UIBackgroundFetchResult.failed
The purpose is to tell the system that you are done.
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler:#escaping (UIBackgroundFetchResult) -> Void) {
// do backgound data fetch
// process it
// finished
completionHandler(UIBackgroundFetchResult.newData)
}
Read more here:
documentation
old but good article
UIBackgroundFetchResult
Remote notification related articles
How to handle remote notification with background mode enabled
Multiple scenarios with Push Notification in iOS
application(_:didReceiveRemoteNotification:fetchCompletionHandler:)

invoke lambda function in iOS appDelegate didReceiveRemoteNotification when app is in background

I am trying to fetch data by invoking a lambda function in response to a remote push notification while my app is in the background. My notifications are configured correctly and the didReceiveRemoteNotification is called while the app is in the background.
I have the following code in that method:
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
let lambdaInvoker = AWSLambdaInvoker.default()
lambdaInvoker.invokeFunction("lambdaFunctionName", jsonObject: jsonObject).continueWith(block: {(task:AWSTask<AnyObject>) -> Any? in
if let error = task.error as NSError? {
print(task.error!.localizedDescription)
print(task.error!)
DispatchQueue.main.async(execute: {
if (error.domain == AWSLambdaInvokerErrorDomain) && (AWSLambdaInvokerErrorType.functionError == AWSLambdaInvokerErrorType(rawValue: error.code)) {
print("Function error: \(String(describing: error.userInfo[AWSLambdaInvokerFunctionErrorKey]))")
} else {
print("Error: \(error)")
}
})
return nil
}
// Handle response in task.result
DispatchQueue.main.async(execute: {
if let jsonArray = task.result as? NSArray {
// do stuff
}
})
return nil
})
}
However the block is not executed in the lambda function. I have not used background fetch before and not sure how to make this work with a lambda function.
What I was missing was the completion handler. After adding that, the code and block executes as expected:
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
let lambdaInvoker = AWSLambdaInvoker.default()
lambdaInvoker.invokeFunction("lambdaFunctionName", jsonObject: jsonObject).continueWith(block: {(task:AWSTask<AnyObject>) -> Any? in
if let error = task.error as NSError? {
print(task.error!.localizedDescription)
print(task.error!)
DispatchQueue.main.async(execute: {
if (error.domain == AWSLambdaInvokerErrorDomain) && (AWSLambdaInvokerErrorType.functionError == AWSLambdaInvokerErrorType(rawValue: error.code)) {
print("Function error: \(String(describing: error.userInfo[AWSLambdaInvokerFunctionErrorKey]))")
completionHandler(UIBackgroundFetchResult.newData)
} else {
print("Error: \(error)")
completionHandler(UIBackgroundFetchResult.newData)
}
})
return nil
}
// Handle response in task.result
DispatchQueue.main.async(execute: {
if let jsonArray = task.result as? NSArray {
// do stuff
completionHandler(UIBackgroundFetchResult.newData)
}
})
return nil
})
}

I get a warning saying that the completion handler was never called When I simulate a Background fetch

I followed all the steps in order to set up the background fetch but I'm suspecting that I made a mistake when writing the function performFetchWithCompletionHandlerin the AppDelegate.
Here is the warning that I get as soon as I simulate a background fetch
Warning: Application delegate received call to - application:
performFetchWithCompletionHandler:but the completion handler was never called.
Here's my code :
func application(application: UIApplication, performFetchWithCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {
if let tabBarController = window?.rootViewController as? UITabBarController,
viewControllers = tabBarController.viewControllers as [UIViewController]! {
for viewController in viewControllers {
if let notificationViewController = viewController as? NotificationsViewController {
firstViewController.reloadData()
completionHandler(.NewData)
print("background fetch done")
}
}
}
}
How can I test if the background-fetchis working ?
If you don't enter the first if statement, the completion handler will never be called. Also, you could potentially not find the view controller you're looking for when you loop through the view controllers, which would mean the completion would never be called. Lastly, you should probably put a return after you call the completion handler.
func application(
application: UIApplication,
performFetchWithCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void
) {
guard let tabBarController = window?.rootViewController as? UITabBarController,
let viewControllers = tabBarController.viewControllers else {
completionHandler(.failed)
return
}
guard let notificationsViewController = viewControllers.first(where: { $0 is NotificationsViewController }) as? NotificationsViewController else {
completionHandler(.failed)
return
}
notificationViewController.reloadData()
completionHandler(.newData)
}

Wait for function to end in Swift

I have a function to get data with json and i append all the data to an array. I try to create semaphore and wait until sending a signal to semaphore to continue but it doesn't work(I'm not sure if i do it correct or not), then i saw a question in Stackoverflow, answer was creating a completion handler like that
func application(application: UIApplication!, performFetchWithCompletionHandler completionHandler: ((UIBackgroundFetchResult) -> Void)!) {
getUrunGrup(completionHandler)
}
so i changed my function like that
func getUrunGrup(completionHandler: ((UIBackgroundFetchResult) -> Void)!){
Alamofire.request(.GET, "http://213.136.86.160:27701/Thunder/DataService/GetUrunGrup")
.responseJSON {(request, response, jsonObj, error) in
if let jsonresult:NSDictionary = jsonObj as? NSDictionary{
if let result: AnyObject = jsonresult["Result"] {
let elementCount = result.count
for (var i = 0; i<elementCount; ++i){
if let name: AnyObject = result[i]["Adi"]!{
if let kod:AnyObject = result[i]["Kod"]!{
urunUstGrup.append(["Adi": "\(name)", "Kod": "\(kod)"])
println("getUrunGrup \(i)")
}
}
}
}
}
}
completionHandler(UIBackgroundFetchResult.NewData)
println("Background Fetch Complete")
}
But there is no answer for how should i call this function?
you have to pass your async function the handler to call later on,like this:
func application(application: UIApplication!, performFetchWithCompletionHandler completionHandler: ((UIBackgroundFetchResult) -> Void)!) {
loadShows(completionHandler)
}
func loadShows(completionHandler: ((UIBackgroundFetchResult) -> Void)!) {
//....
//DO IT
//....
completionHandler(UIBackgroundFetchResult.NewData)
println("Background Fetch Complete")
}
OR (cleaner way IMHO)
add an intermediate completionHandler
func application(application: UIApplication!, performFetchWithCompletionHandler completionHandler: ((UIBackgroundFetchResult) -> Void)!) {
loadShows() {
completionHandler(UIBackgroundFetchResult.NewData)
println("Background Fetch Complete")
}
}
func loadShows(completionHandler: (() -> Void)!) {
//....
//DO IT
//....
completionHandler()
}

Resources