- 新增iOS原生BackgroundTaskBridge桥接模块,支持后台任务注册、调度和完成 - 重构BackgroundTaskManager为V2版本,集成原生iOS后台任务能力 - 在AppDelegate中注册后台任务处理器,确保应用启动时正确初始化 - 重构锻炼通知消息生成逻辑,使用配置化模板提升可维护性 - 扩展健康数据类型映射,支持更多运动项目的中文显示 - 替换原有backgroundTaskManager引用为backgroundTaskManagerV2
421 lines
11 KiB
Swift
421 lines
11 KiB
Swift
//
|
||
// BackgroundTaskBridge.swift
|
||
// OutLive
|
||
//
|
||
// Native bridge responsible for scheduling and relaying iOS background tasks to the JS runtime.
|
||
//
|
||
|
||
import BackgroundTasks
|
||
import Foundation
|
||
import React
|
||
import UIKit
|
||
|
||
@objc(BackgroundTaskBridge)
|
||
class BackgroundTaskBridge: RCTEventEmitter {
|
||
|
||
private enum TaskKind: String {
|
||
case processing
|
||
case refresh
|
||
}
|
||
|
||
private let queue = DispatchQueue(label: "com.anonymous.digitalpilates.background.bridge", qos: .utility)
|
||
|
||
private var hasListeners = false
|
||
private var identifier: String?
|
||
private var kind: TaskKind = .processing
|
||
private var requiresNetworkConnectivity = false
|
||
private var requiresExternalPower = false
|
||
private var defaultDelay: TimeInterval = 60 * 30 // 30 minutes
|
||
private var isRegistered = false
|
||
private var currentTask: BGTask?
|
||
|
||
override init() {
|
||
super.init()
|
||
// 监听来自 AppDelegate 的后台任务通知
|
||
NotificationCenter.default.addObserver(
|
||
self,
|
||
selector: #selector(handleTaskNotification(_:)),
|
||
name: NSNotification.Name("BackgroundTaskBridge.handleTask"),
|
||
object: nil
|
||
)
|
||
}
|
||
|
||
deinit {
|
||
NotificationCenter.default.removeObserver(self)
|
||
}
|
||
|
||
override static func requiresMainQueueSetup() -> Bool {
|
||
true
|
||
}
|
||
|
||
override func startObserving() {
|
||
hasListeners = true
|
||
}
|
||
|
||
override func stopObserving() {
|
||
hasListeners = false
|
||
}
|
||
|
||
@objc
|
||
private func handleTaskNotification(_ notification: Notification) {
|
||
guard #available(iOS 13.0, *),
|
||
let userInfo = notification.userInfo,
|
||
let task = userInfo["task"] as? BGTask else {
|
||
return
|
||
}
|
||
|
||
NSLog("[BackgroundTaskBridge] 收到来自 AppDelegate 的后台任务")
|
||
handle(task: task)
|
||
}
|
||
|
||
override func supportedEvents() -> [String]! {
|
||
[
|
||
"BackgroundTaskBridge.execute",
|
||
"BackgroundTaskBridge.expire"
|
||
]
|
||
}
|
||
|
||
@objc
|
||
func configure(
|
||
_ options: NSDictionary,
|
||
resolver: @escaping RCTPromiseResolveBlock,
|
||
rejecter: @escaping RCTPromiseRejectBlock
|
||
) {
|
||
guard #available(iOS 13.0, *) else {
|
||
rejecter(
|
||
"BACKGROUND_TASK_UNAVAILABLE",
|
||
"BGTaskScheduler requires iOS 13 or later.",
|
||
nil
|
||
)
|
||
return
|
||
}
|
||
|
||
guard let identifier = options["identifier"] as? String,
|
||
!identifier.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
||
rejecter("INVALID_IDENTIFIER", "A non-empty identifier is required.", nil)
|
||
return
|
||
}
|
||
|
||
let taskTypeRaw = (options["taskType"] as? String)?.lowercased()
|
||
let taskKind = TaskKind(rawValue: taskTypeRaw ?? "") ?? .processing
|
||
|
||
let requiresNetwork = (options["requiresNetworkConnectivity"] as? Bool) ?? false
|
||
let requiresPower = (options["requiresExternalPower"] as? Bool) ?? false
|
||
let defaultDelaySeconds = (options["defaultDelay"] as? NSNumber)?.doubleValue
|
||
|
||
queue.async { [weak self] in
|
||
guard let self else { return }
|
||
|
||
self.identifier = identifier
|
||
self.kind = taskKind
|
||
self.requiresNetworkConnectivity = requiresNetwork
|
||
self.requiresExternalPower = requiresPower
|
||
|
||
if let delay = defaultDelaySeconds, delay > 0 {
|
||
self.defaultDelay = delay
|
||
}
|
||
|
||
do {
|
||
try self.registerTaskIfNeeded(identifier: identifier)
|
||
try self.scheduleTask(after: self.defaultDelay)
|
||
resolver([
|
||
"identifier": identifier,
|
||
"scheduled": true,
|
||
"defaultDelay": self.defaultDelay
|
||
])
|
||
} catch {
|
||
rejecter("BACKGROUND_TASK_ERROR", error.localizedDescription, error)
|
||
}
|
||
}
|
||
}
|
||
|
||
@objc
|
||
func schedule(
|
||
_ options: NSDictionary,
|
||
resolver: @escaping RCTPromiseResolveBlock,
|
||
rejecter: @escaping RCTPromiseRejectBlock
|
||
) {
|
||
guard #available(iOS 13.0, *) else {
|
||
rejecter(
|
||
"BACKGROUND_TASK_UNAVAILABLE",
|
||
"BGTaskScheduler requires iOS 13 or later.",
|
||
nil
|
||
)
|
||
return
|
||
}
|
||
|
||
guard let identifier = identifier else {
|
||
rejecter("NOT_CONFIGURED", "Call configure() before scheduling.", nil)
|
||
return
|
||
}
|
||
|
||
let delay = (options["delay"] as? NSNumber)?.doubleValue
|
||
let effectiveDelay = (delay?.isFinite == true && delay! >= 0) ? delay! : defaultDelay
|
||
|
||
queue.async { [weak self] in
|
||
guard let self else { return }
|
||
|
||
do {
|
||
try self.registerTaskIfNeeded(identifier: identifier)
|
||
try self.scheduleTask(after: effectiveDelay)
|
||
resolver([
|
||
"identifier": identifier,
|
||
"scheduled": true,
|
||
"delay": effectiveDelay
|
||
])
|
||
} catch {
|
||
rejecter("BACKGROUND_TASK_ERROR", error.localizedDescription, error)
|
||
}
|
||
}
|
||
}
|
||
|
||
@objc
|
||
func complete(
|
||
_ success: NSNumber,
|
||
rescheduleAfter: NSNumber?,
|
||
resolver: @escaping RCTPromiseResolveBlock,
|
||
rejecter: @escaping RCTPromiseRejectBlock
|
||
) {
|
||
guard #available(iOS 13.0, *) else {
|
||
rejecter(
|
||
"BACKGROUND_TASK_UNAVAILABLE",
|
||
"BGTaskScheduler requires iOS 13 or later.",
|
||
nil
|
||
)
|
||
return
|
||
}
|
||
|
||
let shouldRescheduleAfter = rescheduleAfter?.doubleValue ?? defaultDelay
|
||
let completionSuccess = success.boolValue
|
||
|
||
queue.async { [weak self] in
|
||
guard let self else { return }
|
||
|
||
if let task = self.currentTask {
|
||
task.setTaskCompleted(success: completionSuccess)
|
||
self.currentTask = nil
|
||
}
|
||
|
||
do {
|
||
try self.scheduleTask(after: shouldRescheduleAfter)
|
||
resolver([
|
||
"rescheduled": true,
|
||
"delay": shouldRescheduleAfter,
|
||
"success": completionSuccess
|
||
])
|
||
} catch {
|
||
rejecter("BACKGROUND_TASK_ERROR", error.localizedDescription, error)
|
||
}
|
||
}
|
||
}
|
||
|
||
@objc
|
||
func cancelAll(
|
||
_ resolver: @escaping RCTPromiseResolveBlock,
|
||
rejecter: @escaping RCTPromiseRejectBlock
|
||
) {
|
||
guard #available(iOS 13.0, *) else {
|
||
rejecter(
|
||
"BACKGROUND_TASK_UNAVAILABLE",
|
||
"BGTaskScheduler requires iOS 13 or later.",
|
||
nil
|
||
)
|
||
return
|
||
}
|
||
|
||
queue.async {
|
||
BGTaskScheduler.shared.cancelAllTaskRequests()
|
||
resolver(["cancelled": true])
|
||
}
|
||
}
|
||
|
||
@objc
|
||
func getPendingRequests(
|
||
_ resolver: @escaping RCTPromiseResolveBlock,
|
||
rejecter: @escaping RCTPromiseRejectBlock
|
||
) {
|
||
guard #available(iOS 13.0, *) else {
|
||
rejecter(
|
||
"BACKGROUND_TASK_UNAVAILABLE",
|
||
"BGTaskScheduler requires iOS 13 or later.",
|
||
nil
|
||
)
|
||
return
|
||
}
|
||
|
||
BGTaskScheduler.shared.getPendingTaskRequests { [weak self] requests in
|
||
guard let self else { return }
|
||
|
||
let mapped = requests
|
||
.filter { request in
|
||
guard let identifier = self.identifier else {
|
||
return true
|
||
}
|
||
return request.identifier == identifier
|
||
}
|
||
.map { request -> [String: Any] in
|
||
var payload: [String: Any] = [
|
||
"identifier": request.identifier
|
||
]
|
||
|
||
if let date = request.earliestBeginDate {
|
||
payload["earliestBegin"] = date.timeIntervalSince1970 * 1000
|
||
} else {
|
||
payload["earliestBegin"] = NSNull()
|
||
}
|
||
|
||
if let processingRequest = request as? BGProcessingTaskRequest {
|
||
payload["requiresNetworkConnectivity"] = processingRequest.requiresNetworkConnectivity
|
||
payload["requiresExternalPower"] = processingRequest.requiresExternalPower
|
||
payload["type"] = "processing"
|
||
} else {
|
||
payload["type"] = "refresh"
|
||
}
|
||
|
||
return payload
|
||
}
|
||
|
||
resolver(mapped)
|
||
}
|
||
}
|
||
|
||
@objc
|
||
func backgroundRefreshStatus(
|
||
_ resolver: @escaping RCTPromiseResolveBlock,
|
||
rejecter: @escaping RCTPromiseRejectBlock
|
||
) {
|
||
guard Thread.isMainThread else {
|
||
DispatchQueue.main.async { [weak self] in
|
||
self?.backgroundRefreshStatus(resolver, rejecter: rejecter)
|
||
}
|
||
return
|
||
}
|
||
|
||
let status = UIApplication.shared.backgroundRefreshStatus
|
||
switch status {
|
||
case .available:
|
||
resolver("available")
|
||
case .denied:
|
||
resolver("denied")
|
||
case .restricted:
|
||
resolver("restricted")
|
||
@unknown default:
|
||
resolver("unknown")
|
||
}
|
||
}
|
||
|
||
@objc
|
||
func simulateLaunch(
|
||
_ resolver: @escaping RCTPromiseResolveBlock,
|
||
rejecter: @escaping RCTPromiseRejectBlock
|
||
) {
|
||
guard hasListeners else {
|
||
rejecter("NO_LISTENERS", "No JS listeners registered for background events.", nil)
|
||
return
|
||
}
|
||
|
||
DispatchQueue.main.async { [weak self] in
|
||
guard let self else { return }
|
||
|
||
self.sendEvent(
|
||
withName: "BackgroundTaskBridge.execute",
|
||
body: [
|
||
"identifier": self.identifier ?? "simulated",
|
||
"timestamp": Date().timeIntervalSince1970 * 1000,
|
||
"simulated": true
|
||
]
|
||
)
|
||
resolver(["simulated": true])
|
||
}
|
||
}
|
||
|
||
// MARK: - Private helpers
|
||
|
||
@available(iOS 13.0, *)
|
||
private func registerTaskIfNeeded(identifier: String) throws {
|
||
guard !isRegistered else { return }
|
||
|
||
// 注意:任务标识符已在 AppDelegate 中注册
|
||
// BGTaskScheduler 要求所有任务必须在应用启动完成前注册
|
||
// 这里只标记为已注册,实际的任务处理将通过 AppDelegate 中的注册生效
|
||
isRegistered = true
|
||
NSLog("[BackgroundTaskBridge] 使用 AppDelegate 中预注册的后台任务: \(identifier)")
|
||
}
|
||
|
||
@available(iOS 13.0, *)
|
||
private func scheduleTask(after delay: TimeInterval) throws {
|
||
guard let identifier else {
|
||
throw NSError(
|
||
domain: "BackgroundTaskBridge",
|
||
code: 1,
|
||
userInfo: [NSLocalizedDescriptionKey: "Identifier is missing. Configure the bridge first."]
|
||
)
|
||
}
|
||
|
||
let request: BGTaskRequest
|
||
|
||
switch kind {
|
||
case .processing:
|
||
let processing = BGProcessingTaskRequest(identifier: identifier)
|
||
processing.requiresNetworkConnectivity = requiresNetworkConnectivity
|
||
processing.requiresExternalPower = requiresExternalPower
|
||
processing.earliestBeginDate = Date(timeIntervalSinceNow: delay)
|
||
request = processing
|
||
case .refresh:
|
||
let refresh = BGAppRefreshTaskRequest(identifier: identifier)
|
||
refresh.earliestBeginDate = Date(timeIntervalSinceNow: delay)
|
||
request = refresh
|
||
}
|
||
|
||
do {
|
||
try BGTaskScheduler.shared.submit(request)
|
||
} catch {
|
||
throw error
|
||
}
|
||
}
|
||
|
||
@available(iOS 13.0, *)
|
||
private func handle(task: BGTask) {
|
||
queue.async { [weak self] in
|
||
guard let self else { return }
|
||
|
||
self.currentTask = task
|
||
|
||
task.expirationHandler = { [weak self] in
|
||
guard let self else { return }
|
||
self.queue.async {
|
||
self.currentTask = nil
|
||
}
|
||
|
||
guard self.hasListeners else { return }
|
||
|
||
DispatchQueue.main.async {
|
||
self.sendEvent(
|
||
withName: "BackgroundTaskBridge.expire",
|
||
body: [
|
||
"identifier": self.identifier ?? "",
|
||
"timestamp": Date().timeIntervalSince1970 * 1000
|
||
]
|
||
)
|
||
}
|
||
}
|
||
|
||
guard self.hasListeners else {
|
||
task.setTaskCompleted(success: false)
|
||
self.currentTask = nil
|
||
return
|
||
}
|
||
|
||
DispatchQueue.main.async {
|
||
self.sendEvent(
|
||
withName: "BackgroundTaskBridge.execute",
|
||
body: [
|
||
"identifier": self.identifier ?? "",
|
||
"timestamp": Date().timeIntervalSince1970 * 1000
|
||
]
|
||
)
|
||
}
|
||
}
|
||
}
|
||
}
|