Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to configure Background App Refresh using Swift?

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

like image 429
Vetuka Avatar asked Oct 31 '17 21:10

Vetuka


2 Answers

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

like image 180
Paulw11 Avatar answered Nov 19 '22 20:11

Paulw11


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)
    }
}
like image 41
cldrr Avatar answered Nov 19 '22 21:11

cldrr