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?
- Does scheduling the task each time scenePhase == .active lead to excess schedules?
- 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()
}
})
}
}
}