How to use BGAppRefreshTask without server update?

Hello everyone,

I have a function in viewDidLoad() which checks whether the last access date by the user is within today's calendar date. If it is the same day, it proceeds to show the word for that day (retrieved from a JSON file). If it's a new day it displays a new word on the UI.

However, if the app is not manually opened by the user, the content does not update, no new data is stored in userdefaults and therefore the content of my local notification will also not send the user the new word picked for that day.

This is the code that runs in viewDidLoad():

Code Block
func isRefreshRequired() {
      // if it is first time opening app
      if userDefaults.bool(forKey: "First Launch") == false {
        //run code during first launch
        _ = updateWordOfTheDay()
        NotificationManager.askNotificationPermission()
        print("First time opening app")
        userDefaults.set(true, forKey: "First Launch")
      }
      else {
        //run code after first launch
        print("Not first time opening app")
        userDefaults.set(true, forKey: "First Launch")
        let lastAccessDate = userDefaults.object(forKey: "lastAccessDate") as? Date ?? Date()
        userDefaults.set(Date(), forKey: "lastAccessDate")
        // if it is the same day give the vocab picked for that day
        if calendar.isDateInToday(lastAccessDate) {
          readFromUserDefaults()
          print("No refresh required as it is the same day as lastAccessDate which was \(lastAccessDate)")
        }
        else {
          print("new day since lastAccessDate")
          _ = updateWordOfTheDay()
        }
      }
    }


Since this cannot be run unless the app is manually opened, I thought using BGAppRefreshTask would be perfect as it updates the app "with small bits of information". This way the app would be woken up and these conditions would be checked. But I have no idea how to actually implement this, especially in the "launch handler". All the tutorials online deal with data that is retrieved from a server, but my data is all internal.

I just want the app to occasionally run in the background to see if the last access date is different from the current day, and if it is, to send a notification using the new data.
Can someone please advise me?


Replies

It's true that the typical BG update involves a URL session to get web data, but it doesn't have to.

In the App Delegate, you need to set up your app. Here is the code I have in my didFinishLaunchingWithOptions in my app delegate:

(Note that self.logSB is my logging, similar to a Print command.)
Code Block
switch UIApplication.shared.backgroundRefreshStatus {
        case .available:
            self.logSB.debug("backgroundRefreshStatus is AVAILABLE")
        case .denied:
            self.logSB.debug("backgroundRefreshStatus is DENIED")
        case .restricted:
            self.logSB.debug("backgroundRefreshStatus is RESTRICTED")
        default:
            self.logSB.debug("backgroundRefreshStatus is unknown")
        }
        if #available(iOS 13.0, *) {
            self.logSB.verbose("iOS 13  Registering for Background duty")
            BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.WH.myApp.myIdentifier", using: nil) { task in
                self.handleAppRefresh(task: task as! BGAppRefreshTask)
                self.logSB.verbose("iOS 13  Refresh requested")
            }
        } else {
            self.logSB.verbose("iOS < 13  Registering for Background duty")
            UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplication.backgroundFetchIntervalMinimum)
            //                UIApplication.shared.setMinimumBackgroundFetchInterval(200)
        }

You can also see that I support the older background session method. That's what my app started with before iOS 13 came along. You could probably drop that.

Up at the AppDelegate class level:
Code Block     Swift
var backgroundSessionCompletionHandler: (() -> Void)?
    var backgroundSynchTask: UIBackgroundTaskIdentifier = .invalid


The pre-iOS 13 fetch:

Code Block Swift
    func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
        let dateFormatter = DateFormatter()
        dateFormatter.dateStyle = DateFormatter.Style.medium
        dateFormatter.timeStyle = DateFormatter.Style.long
        var convertedDate = dateFormatter.string(from: Date())
        if #available(iOS 13.0, *) {
            self.logSB.info("iOS 13 Ignoring Background Fetch called at \(convertedDate)")
            completionHandler(.newData)
        } else {
            self.logSB.info("Background Fetch called at \(convertedDate)")
            let myState = UIApplication.shared.applicationState
            switch myState {
            case .background :
                self.logSB.info("The app state was found to be Background at \(convertedDate)")
            case .inactive:
                self.logSB.info("The app state was found to be Inactive at \(convertedDate)")
            case .active:
                self.logSB.info("The app state was found to be Active at \(convertedDate)")
            default:
                self.logSB.info("The app state was found to be Unknown at \(convertedDate)")
            }
            Central().fetch {   
                convertedDate = dateFormatter.string(from: Date())
                self.logSB.info("Calling the Fetch completion handler at \(convertedDate)\n\n")
                completionHandler(.newData)
            }
}
    }

The Central().fetch function is what goes and gets current web data, but it doesn't have to. It could be anything.

And finally:

Code Block Swift
    func applicationDidEnterBackground(_ application: UIApplication) {
      let dateFormatter = DateFormatter()
        dateFormatter.dateStyle = DateFormatter.Style.medium
        dateFormatter.timeStyle = DateFormatter.Style.long
        let convertedDate = dateFormatter.string(from: Date())
        self.logSB.info("The app entered the background at \(convertedDate)")
        if #available(iOS 13.0, *) {
            scheduleAppRefresh()
        }
    }
    @available(iOS 13.0, *)
    func scheduleAppRefresh() {
        logSB.info("Scheduling the AppRefresh in iOS 13 !!")
        let request = BGAppRefreshTaskRequest(identifier: "com.WH.myApp.myIdentifier")
        request.earliestBeginDate = Date(timeIntervalSinceNow: 2 * 60)
        do {
            try BGTaskScheduler.shared.submit(request)
        } catch {
            logSB.error("Could not schedule app refresh: \(error)")
        }
    }
    @available(iOS 13.0, *)
    func handleAppRefresh(task: BGAppRefreshTask) {
        logSB.info("Handling the AppRefresh in iOS 13")
        scheduleAppRefresh()
        let queue = OperationQueue()
        queue.maxConcurrentOperationCount = 1
        let appRefreshOperation =  Central().fetch2
        queue.addOperation(appRefreshOperation)
        let lastOperation = queue.operations.last
        lastOperation?.completionBlock = {
            task.setTaskCompleted(success: !(lastOperation?.isCancelled ?? false))
        }
        task.expirationHandler = {
            queue.cancelAllOperations()
        }
      let dateFormatter = DateFormatter()
        dateFormatter.dateStyle = DateFormatter.Style.medium
        dateFormatter.timeStyle = DateFormatter.Style.long
        let convertedDate = dateFormatter.string(from: Date())
        self.logSB.info("Completed the iOS 13 App Refresh at \(convertedDate)")
    } 





Hey Wayne,

Thank you so much for sharing your code as an example. It is written so nicely and I only hope to be able to write like this by myself one day. I have been learning to code for three months now and at this point I am just learning from messing around with the code and reading the docs (which I find hard to understand) so apologies for the dumb questions.

I wanted to ask - since you have used Central().fetch2 to get current web data, I thought I might be able to use my isRefreshRequired() from my ViewController() class... something like

Code Block
        let appRefreshOperation =  ViewConroller().isRefreshRequired

obviously it gave me errors since it is incorrect, but I am not quite sure what to put here? Should I be making a separate struct that has .isRefreshRequired() as a method and use it that way?