Files
digital-pilates/ios/OutLive/BackgroundTaskBridge.swift
richarjiang f80a1bae78 feat(background-task): 实现iOS原生后台任务V2系统并重构锻炼通知消息模板
- 新增iOS原生BackgroundTaskBridge桥接模块,支持后台任务注册、调度和完成
- 重构BackgroundTaskManager为V2版本,集成原生iOS后台任务能力
- 在AppDelegate中注册后台任务处理器,确保应用启动时正确初始化
- 重构锻炼通知消息生成逻辑,使用配置化模板提升可维护性
- 扩展健康数据类型映射,支持更多运动项目的中文显示
- 替换原有backgroundTaskManager引用为backgroundTaskManagerV2
2025-11-04 09:41:10 +08:00

421 lines
11 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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
]
)
}
}
}
}