How to configure Background App Refresh using Swift?
Asked Answered
T

2

6

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

Tortuosity answered 31/10, 2017 at 21:48 Comment(0)
Z
6

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

Zel answered 31/10, 2017 at 22:13 Comment(7)
Thank you for the great answer. I am newbie so please excuse my questions. How can I add completionHandler into my downloadJSON? Oh you added that too. Thank you so much! Going to implement your input! Really appreciated!Tortuosity
last partial code gives an error: Argument passed to call that takes no arguments on "{" at the vc.downloadJSON() { (error) inTortuosity
That is because you need to add a completion handler argument to your download function - see my edited answerZel
Works perfect. No errors. I wish I can mark your question as answer as well. Thank you so much for your time!Tortuosity
The completionHandler is provided by the system when it calls the delegate method. It is your responsibility to call it with the result of your fetch once it is complete. .newData indicates that new data was fetched. iOS uses the result to identify how often and when your app should be given an opportunity to perform a background fetch.Zel
This is work upto iOS 11 but i need iOS 9 +. how can i used this with old iOSIneffable
What part doesn't work on iOS9? performFetchWithCompletionHandler was introduced in iOS 7. (and really, iOS9?)Zel
R
3

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)
    }
}
Rosebud answered 31/10, 2017 at 21:51 Comment(2)
Warning disappeared. Thank you!Tortuosity
Actually, this is a run-time warning issued by iOS, not the compiler, because it did happen; the if test failed because the view controller wasn't available in the background. Adding a call to the completion handler in the else will cause the warning to go away since the completion handler was called, but no download has occurred.Zel

© 2022 - 2024 — McMap. All rights reserved.