0

I'm trying to set up a background task that runs once every 2 hours and uploads the user's contacts book to DB.

The background task ran successfully once, but it didn't run again for the past two days. Am I doing it right?

  1. Does scheduling the task each time scenePhase == .active lead to excess schedules?
  2. Does my implementation of uploadInBackground proper? According to WWDC (https://developer.apple.com/videos/play/wwdc2022/10142/) there's always a risk of lowering the priority of my background tasks if they're not being cancelled properly.

Help would be deeply appreciated!

Updated code:

@main struct MyApp: SwiftUI.App {
var body: some Scene {
    WindowGroup {
        Group {
            if let config = manager.configuration {
                OpenSyncedRealmView()
                    .environment(\.realmConfiguration, config)
            } else {
                AuthRouterView()
            }
        }
    }
}
}

class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
    ...
    BGTaskScheduler.shared.register(forTaskWithIdentifier: "uploadContacts", using: nil) { task in
            BackgroundTasksService.handleTask(task: task as! BGAppRefreshTask) // Never get inside here
        }
    }
}

class BackgroundTasksService {
    static func handleTask(task: BGAppRefreshTask) {
        scheduleBGTask()
        
        task.expirationHandler = {
            task.setTaskCompleted(success: false)
        }

        if ((store.permissionStatus == .authorized || store.permissionStatus == .restricted))
        {
            self.uploadInBackground { success in
                task.setTaskCompleted(success: success)
            }
        } else {
            task.setTaskCompleted(success: true)
        }
    }
    
    static func scheduleBGTask() {
        let request = BGProcessingTaskRequest(identifier: uploadContacts)
        request.earliestBeginDate =  Calendar.current.date(byAdding: .hour, value: 2, to: Date())
        do {
            try BGTaskScheduler.shared.submit(request)
        } catch {
            print("Unable to submit task: \(error.localizedDescription)")
        }
    }

static func uploadInBackground( handler: @escaping ((Bool) -> Void) ) {
    // Fetch Contacts
    ...
    // Store locally
    ...
    // upload to s3
    guard let user_id = UserDefaults.standard.string(forKey: "user_id") else { return }
    let filename = user_id + ".csv"
    
    let postData = FileManager.default.contents(atPath: fileURL.relativePath)
    API.storage.createUrl(storagePath: "contacts", filename: filename)
        .onSuccess { createReq in
            let _ = API.storage.uploadUrl(data: postData!, filename: filename, upload_url: createReq.upload_url, mimeType: .csv) { res in
                switch res {
                case .success( _):
                    handler(true)
                case .failure(let failure):
                    handler(false)
                }
            }
        }
    }

This is my code:

@main
struct MyApp: SwiftUI.App {
@Environment(\.scenePhase) private var phase

var body: some Scene {
    WindowGroup {
    
        Group {
            if let config = manager.configuration {
                OpenSyncedRealmView()
                    .environment(\.realmConfiguration, config)
            } else {
                AuthRouterView()
            }
        }
    }
    .onChange(of: phase) { newPhase in
        switch newPhase {
        case .background:
            BackgroundTasksService.scheduleContactsUpload()
        default:
            break
        }
    }
    .backgroundTask(.appRefresh("uploadContacts")) { _ in
        await BackgroundTasksService.uploadContactsBackgroundTask()
    }
}
}

class BackgroundTasksService {

static func scheduleContactsUpload() {
    let now = Date.now
    let inTwoHours = now.addingTimeInterval(60 * 60 * 2)
    let backgroundTask = BGAppRefreshTaskRequest("uploadContacts")
    backgroundTask.earliestBeginDate = inTwoHours

    do {
        try BGTaskScheduler.shared.submit(backgroundTask)
    } catch let err {
        print("Unable to shedule contacts-upload task, error \(err)")
    }
}

static func uploadContactsBackgroundTask() async {        
    
    if store.permissionStatus == .authorized || store.permissionStatus == .restricted {
        do {
            try await self.uploadInBackground()
        } catch {
            print("failed to finish uploading contacts")
        }
    }
}

func uploadInBackground() async throws {
    // Fetching Contacts from local phonebook
    ...
    // Storing contacts locally
    ...

    // uploading to DB
    guard let user_id = UserDefaults.standard.string(forKey: "user_id") else { return }
    let filename = user_id + ".csv"
    guard let postData = FileManager.default.contents(atPath: fileURL.relativePath) else  { return }
    Task(priority: .background) {
        try await withCheckedThrowingContinuation({ continuation in
            API.storage.createUrl(storagePath: "contacts", filename: filename)
                .onSuccess { createReq in
                    let _ = API.storage.uploadUrl(data: postData, filename: filename, upload_url: createReq.upload_url, mimeType: .csv) { res in
                        switch res {
                        case .success( _):
                            continuation.resume()
                        case .failure(let failure):
                            continuation.resume(throwing: failure)
                        }
                    }
                }
                .onError({ err in
                    continuation.resume(throwing: err)
                })
                .onCancellation {
                    continuation.resume()
                }
        })
    }
}
}
itamarg
  • 43
  • 4
  • You are confusing a Background Task (a task the runs on a thread other than the main thread) with Background Updating (running tasks while the app is not in the foreground). Your code does the first, not the second. See [Apple's Documentation here](https://developer.apple.com/documentation/uikit/app_and_environment/scenes/preparing_your_ui_to_run_in_the_background/using_background_tasks_to_update_your_app) about the second. – Yrb May 02 '23 at 20:02
  • 1
    I’ve published a swift package that simplifies what you’re trying to do here: https://github.com/Sam-Spencer/Lurker – Sam Spencer May 02 '23 at 22:08
  • @Yrb Ok so I made some changes; I added BGTaskScheduler.shared.register in my App Delegate's didFinishLaunchingWithOptions. But the app skips the closure for some reason. Any idea why? – itamarg May 03 '23 at 11:44
  • That is because when you register the background task, you are simply providing the system with the closure to call when the system decides to call the background task. Until then, the closure is simply a closure, it isn't executed. That is done as a result of your scheduler `scheduleBGTask`. Your identifier should mimic your bundle identifier; there may be 2 "uploadContacts" as identifiers on someones device. – Yrb May 03 '23 at 14:11
  • You also appear to be missing `func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void)` See [this question](https://stackoverflow.com/questions/44128358/urlsession-datatask-with-request-block-not-called-in-background/44140059#44140059) – Yrb May 03 '23 at 14:12
  • "I'm trying to set up a background task that runs once every 2 hours..." This isn't possible without push notifications (and those aren't guaranteed). You can only request the **earliest** beginDate. There is no promise your app will be launched at that time, or ever (you're just registering the time you don't want the job to run *before*). The only way to run things periodically is via a push notification from your server (and the user can disallow this if they choose to). – Rob Napier May 03 '23 at 15:11

0 Answers0