You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI周期背景任务问题:通讯录上传DB未重复执行排查

SwiftUI后台任务调度与实现问题

我在SwiftUI中配置了一个每2小时执行一次、将用户通讯录上传至数据库的后台任务,但该任务仅成功执行一次,后续两天未再次运行。现咨询以下问题:

  • 每次scenePhase变为.active时调度任务是否会导致过度调度?
  • uploadInBackground的实现是否合规?根据WWDC 2022相关内容,若任务未正确取消会降低后台任务优先级。

更新后的代码

@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)
                }
            }
        }
    }
}

我的代码

@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()
                }
        })
    }
}
}

问题解答

1. scenePhase切换时调度任务是否会过度调度?

你的代码是在scenePhase变为.background时调度任务,但即使如此,频繁提交仍会引发问题:

  • 重复任务堆积:每次进入后台都提交新任务,系统会自动丢弃旧请求保留最新的,但频繁操作会被判定为滥用后台任务,直接降低后续调度优先级。
  • 正确优化方式:
    1. 提交前先取消同标识符的已有任务,避免重复调度:
      static func scheduleContactsUpload() {
          // 取消已存在的同标识符任务
          BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: "uploadContacts")
          
          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 schedule contacts-upload task, error \(err)")
          }
      }
      
    2. 调整调度时机:仅在应用启动、任务执行完成后调度下一次,而非每次进入后台都触发。

2. uploadInBackground的实现是否合规?

当前实现存在多个不符合规范的点,会导致任务异常或优先级降低:

核心问题:

  1. 未响应任务取消:SwiftUI的backgroundTask闭包未处理系统取消信号,若任务被强制终止却无响应,会影响后续任务调度优先级。
  2. 嵌套Task导致等待失效:uploadInBackground中嵌套的Task会让外部await无法正确等待任务完成,可能任务仍在执行时,外层方法已结束,系统会误判任务完成并终止操作。
  3. 错误处理不完整:部分guard直接return未抛出错误,调用方无法感知失败,导致系统误判任务成功。

修改后的合规实现:

static func uploadContactsBackgroundTask() async {        
    guard store.permissionStatus == .authorized || store.permissionStatus == .restricted else {
        return
    }
    
    do {
        try await withTaskCancellationHandler {
            try await uploadInBackground()
        } onCancel: {
            // 取消正在进行的网络请求/文件操作
            API.storage.cancelCurrentRequests() // 需确保API支持取消请求
        }
    } catch {
        print("failed to finish uploading contacts: \(error)")
    }
}

static 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 {
        throw NSError(domain: "ContactsUpload", code: -1, userInfo: [NSLocalizedDescriptionKey: "用户ID未找到"])
    }
    let filename = user_id + ".csv"
    guard let postData = FileManager.default.contents(atPath: fileURL.relativePath) else {
        throw NSError(domain: "ContactsUpload", code: -2, userInfo: [NSLocalizedDescriptionKey: "CSV文件不存在"])
    }
    
    return try await withCheckedThrowingContinuation({ continuation in
        API.storage.createUrl(storagePath: "contacts", filename: filename)
            .onSuccess { createReq in
                let uploadTask = 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)
                    }
                }
                // 监听任务取消信号,及时终止上传
                Task {
                    if await Task.isCancelled {
                        uploadTask.cancel() // 需确保uploadUrl返回可取消的任务实例
                        continuation.resume(throwing: CancellationError())
                    }
                }
            }
            .onError({ err in
                continuation.resume(throwing: err)
            })
            .onCancellation {
                continuation.resume(throwing: CancellationError())
            }
    })
}

额外注意事项:

  • 任务类型选择:通讯录上传属于非即时性网络任务,使用BGProcessingTaskRequest比BGAppRefreshTaskRequest更合适,前者允许更长执行时间,系统会在充电、联网等合适时机调度。
  • 权限配置:必须在Info.plist中添加BGTaskSchedulerPermittedIdentifiers数组,包含任务标识符uploadContacts,否则任务无法被系统调度。
  • 测试方法:使用Xcode的Debug -> Simulate Background Refresh功能测试后台任务,真实环境中系统会根据应用活跃度调整调度频率,无法保证严格每2小时执行。

内容的提问来源于stack exchange,提问作者itamarg

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 07:47:03