5

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

Vetuka
  • 1,523
  • 1
  • 24
  • 40

2 Answers2

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

Paulw11
  • 108,386
  • 14
  • 159
  • 186
  • 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! – Vetuka Oct 31 '17 at 22:20
  • last partial code gives an error: Argument passed to call that takes no arguments on "{" at the vc.downloadJSON() { (error) in – Vetuka Oct 31 '17 at 22:23
  • That is because you need to add a completion handler argument to your download function - see my edited answer – Paulw11 Oct 31 '17 at 22:25
  • Works perfect. No errors. I wish I can mark your question as answer as well. Thank you so much for your time! – Vetuka Oct 31 '17 at 22:43
  • 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. – Paulw11 Sep 11 '18 at 06:40
  • This is work upto iOS 11 but i need iOS 9 +. how can i used this with old iOS – Digvijaysinh Gida May 28 '19 at 07:16
  • What part doesn't work on iOS9? `performFetchWithCompletionHandler` was introduced in iOS 7. (and really, iOS9?) – Paulw11 May 28 '19 at 07:19
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)
    }
}
cldrr
  • 1,238
  • 14
  • 22
  • Warning disappeared. Thank you! – Vetuka Oct 31 '17 at 22:03
  • 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. – Paulw11 Oct 31 '17 at 22:19