Search code examples
swiftbackgroundbackground-processbgtaskscheduler

Is it possible to execute function periodically even if app is terminated?


I am building a Country Tracker app. User installs the app, through several APIs app get user country and saves it in the Core Data. The problem is that BGTask is unreliable. For the last 3 days I didn't see it execute once. Is it possible to do that differently?

I want to add a third solution to my

BGTaskScheduler.shared.register(forTaskWithIdentifier: "///", using: nil) { task in
            self.handleLocationFetch(task: task as! BGAppRefreshTask) }


private func scheduleAppRefresh() {
        let request = BGAppRefreshTaskRequest(identifier: "///")
        request.earliestBeginDate = Date(timeIntervalSinceNow: 1800) // 30 minutes
        do {
            try BGTaskScheduler.shared.submit(request)
        } catch {
            print("Could not schedule app refresh: \(error)")
        }
    }

and

init() {
BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.pinborg.countryTracker.locationfetch", using: nil) { task in
            self.handleLocationFetch(task: task as! BGAppRefreshTask) }
}

, the one that will always work, no matter what. Thank you in advance.

EDIT:

    private func handleLocationFetch(task: BGAppRefreshTask) {
        scheduleAppRefresh()
        let locationManager = LocationManager()
        let entryStore = EntryStore()
        let openCageVM = OpenCageVM()
        
        task.expirationHandler = {
            locationManager.locationManager.stopMonitoringSignificantLocationChanges()
        }
        
        locationManager.locationManager.startMonitoringSignificantLocationChanges()
        
        // Fetch the "app storage" toggle setting from UserDefaults (or another storage)
        let allowTrackingDuringFlight = UserDefaults.standard.bool(forKey: "allowTrackingDuringFlight")
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
            guard let location = locationManager.locationManager.location else {
                print("Failed to fetch location.")
                task.setTaskCompleted(success: false)
                return
            }
            
            let altitude = location.altitude
            
            // If app storage is enabled, check the altitude; otherwise, ignore altitude
            if !allowTrackingDuringFlight && altitude > 5000 {
                print("Location altitude is too high. Task aborted.")
                task.setTaskCompleted(success: false)
                return
            }
            
            let lat = location.coordinate.latitude
            let lng = location.coordinate.longitude
            
            // Call the API to fetch location information
            openCageVM.fetchLocationInfo(lat: lat, lng: lng) {
                print("Location info fetched")
                
                self.sendLocationNotification(location: location)
                
                // Use fetched data to add an entry to the store
                entryStore.addNewEntryHistory(
                    date: Date(),
                    iso2: openCageVM.info?.results?.first?.components?.iso31661Alpha2 ?? "",
                    iso3: openCageVM.info?.results?.first?.components?.iso31661Alpha3 ?? "",
                    name: openCageVM.info?.results?.first?.components?.country ?? ""
                )
                
                task.setTaskCompleted(success: true)
            }
        })
        
        task.setTaskCompleted(success: true)
    }

Solution

  • The short answer to your question is NO. Apple specifically forbids apps from running indefinitely in the background, with a small number of exceptions, as mentioned by Rob Napier.

    Given that your use-case is to track a user's travels between countries, you should use Core Location's startMonitoringSignificantLocationChanges, again as per Rob Napier's suggestion in his comment.

    The GPS is extremely power-hungry, and keeping it "lit" would chew through the user's battery in short order. The significant location change API lets the system limit power use by only turning on the GPS occasionally, and only invoking your app's code when it detects that the user has moved by a meaningful distance. (The docs say you'll get notified when the user moves 500 meters from their previous location, and that you should not expect to be called more frequently than every 5 minutes. I'm not sure how to square those 2 things, since somebody in a car/bus/train would certainly travel more than 500 meters in 5 minutes.)