feat(background-task): 实现iOS原生后台任务V2系统并重构锻炼通知消息模板
- 新增iOS原生BackgroundTaskBridge桥接模块,支持后台任务注册、调度和完成 - 重构BackgroundTaskManager为V2版本,集成原生iOS后台任务能力 - 在AppDelegate中注册后台任务处理器,确保应用启动时正确初始化 - 重构锻炼通知消息生成逻辑,使用配置化模板提升可维护性 - 扩展健康数据类型映射,支持更多运动项目的中文显示 - 替换原有backgroundTaskManager引用为backgroundTaskManagerV2
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import Expo
|
||||
import React
|
||||
import ReactAppDependencyProvider
|
||||
import BackgroundTasks
|
||||
|
||||
@UIApplicationMain
|
||||
public class AppDelegate: ExpoAppDelegate {
|
||||
@@ -13,6 +14,11 @@ public class AppDelegate: ExpoAppDelegate {
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
|
||||
) -> Bool {
|
||||
// 在应用启动完成前注册后台任务
|
||||
if #available(iOS 13.0, *) {
|
||||
registerBackgroundTasks()
|
||||
}
|
||||
|
||||
let delegate = ReactNativeDelegate()
|
||||
let factory = ExpoReactNativeFactory(delegate: delegate)
|
||||
delegate.dependencyProvider = RCTAppDependencyProvider()
|
||||
@@ -31,6 +37,53 @@ public class AppDelegate: ExpoAppDelegate {
|
||||
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
|
||||
// MARK: - Background Task Registration
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
private func registerBackgroundTasks() {
|
||||
let identifier = "com.anonymous.digitalpilates.task"
|
||||
|
||||
// 注册后台任务处理器
|
||||
// 必须在应用启动完成前注册,否则会崩溃
|
||||
BGTaskScheduler.shared.register(
|
||||
forTaskWithIdentifier: identifier,
|
||||
using: nil
|
||||
) { [weak self] task in
|
||||
// 尝试通知 BackgroundTaskBridge 处理任务
|
||||
// 如果 bridge 不可用,标记任务完成
|
||||
self?.handleBackgroundTask(task, identifier: identifier)
|
||||
}
|
||||
|
||||
NSLog("[AppDelegate] 后台任务已在应用启动时注册: \(identifier)")
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
private func handleBackgroundTask(_ task: BGTask, identifier: String) {
|
||||
// 尝试获取 BackgroundTaskBridge 实例来处理任务
|
||||
// 如果 React Native bridge 还未初始化,则直接完成任务
|
||||
guard let bridge = reactNativeFactory?.bridge,
|
||||
bridge.isValid else {
|
||||
NSLog("[AppDelegate] React Native bridge 未就绪,直接完成后台任务")
|
||||
task.setTaskCompleted(success: false)
|
||||
return
|
||||
}
|
||||
|
||||
// 通过 bridge 查找 BackgroundTaskBridge 模块
|
||||
DispatchQueue.main.async {
|
||||
if let module = bridge.module(for: BackgroundTaskBridge.self) as? BackgroundTaskBridge {
|
||||
// 通知 BackgroundTaskBridge 处理任务
|
||||
NotificationCenter.default.post(
|
||||
name: NSNotification.Name("BackgroundTaskBridge.handleTask"),
|
||||
object: nil,
|
||||
userInfo: ["task": task, "identifier": identifier]
|
||||
)
|
||||
} else {
|
||||
NSLog("[AppDelegate] BackgroundTaskBridge 模块未找到,完成后台任务")
|
||||
task.setTaskCompleted(success: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Linking API
|
||||
public override func application(
|
||||
|
||||
31
ios/OutLive/BackgroundTaskBridge.m
Normal file
31
ios/OutLive/BackgroundTaskBridge.m
Normal file
@@ -0,0 +1,31 @@
|
||||
#import <React/RCTBridgeModule.h>
|
||||
#import <React/RCTEventEmitter.h>
|
||||
|
||||
@interface RCT_EXTERN_MODULE(BackgroundTaskBridge, RCTEventEmitter)
|
||||
|
||||
RCT_EXTERN_METHOD(configure:(NSDictionary *)options
|
||||
resolver:(RCTPromiseResolveBlock)resolver
|
||||
rejecter:(RCTPromiseRejectBlock)rejecter)
|
||||
|
||||
RCT_EXTERN_METHOD(schedule:(NSDictionary *)options
|
||||
resolver:(RCTPromiseResolveBlock)resolver
|
||||
rejecter:(RCTPromiseRejectBlock)rejecter)
|
||||
|
||||
RCT_EXTERN_METHOD(complete:(NSNumber *)success
|
||||
rescheduleAfter:(NSNumber *_Nullable)rescheduleAfter
|
||||
resolver:(RCTPromiseResolveBlock)resolver
|
||||
rejecter:(RCTPromiseRejectBlock)rejecter)
|
||||
|
||||
RCT_EXTERN_METHOD(cancelAll:(RCTPromiseResolveBlock)resolver
|
||||
rejecter:(RCTPromiseRejectBlock)rejecter)
|
||||
|
||||
RCT_EXTERN_METHOD(getPendingRequests:(RCTPromiseResolveBlock)resolver
|
||||
rejecter:(RCTPromiseRejectBlock)rejecter)
|
||||
|
||||
RCT_EXTERN_METHOD(backgroundRefreshStatus:(RCTPromiseResolveBlock)resolver
|
||||
rejecter:(RCTPromiseRejectBlock)rejecter)
|
||||
|
||||
RCT_EXTERN_METHOD(simulateLaunch:(RCTPromiseResolveBlock)resolver
|
||||
rejecter:(RCTPromiseRejectBlock)rejecter)
|
||||
|
||||
@end
|
||||
420
ios/OutLive/BackgroundTaskBridge.swift
Normal file
420
ios/OutLive/BackgroundTaskBridge.swift
Normal file
@@ -0,0 +1,420 @@
|
||||
//
|
||||
// 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
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user