- 修复iOS后台任务注册时机问题,确保任务能正常触发 - 添加后台任务调试辅助工具和完整测试指南 - 优化断食通知系统,增加防抖机制避免频繁重调度 - 改进断食自动续订逻辑,使用固定时间而非相对时间计算 - 优化统计页面布局,添加身体指标section标题 - 增强饮水详情页面视觉效果,改进卡片样式和配色 - 添加用户反馈入口到个人设置页面 - 完善锻炼摘要卡片条件渲染逻辑 - 增强日志记录和错误处理机制 这些改进显著提升了应用的稳定性、性能和用户体验,特别是在iOS后台任务执行和断食功能方面。
190 lines
6.7 KiB
Swift
190 lines
6.7 KiB
Swift
import Expo
|
||
import React
|
||
import ReactAppDependencyProvider
|
||
import BackgroundTasks
|
||
|
||
@UIApplicationMain
|
||
public class AppDelegate: ExpoAppDelegate {
|
||
var window: UIWindow?
|
||
|
||
var reactNativeDelegate: ExpoReactNativeFactoryDelegate?
|
||
var reactNativeFactory: RCTReactNativeFactory?
|
||
|
||
public override func application(
|
||
_ application: UIApplication,
|
||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
|
||
) -> Bool {
|
||
// 注意:必须在 super.application() 之前注册后台任务
|
||
// 这是 BGTaskScheduler 的硬性要求,否则任务永远不会被触发
|
||
if #available(iOS 13.0, *) {
|
||
registerBackgroundTasks()
|
||
}
|
||
|
||
let delegate = ReactNativeDelegate()
|
||
let factory = ExpoReactNativeFactory(delegate: delegate)
|
||
delegate.dependencyProvider = RCTAppDependencyProvider()
|
||
|
||
reactNativeDelegate = delegate
|
||
reactNativeFactory = factory
|
||
bindReactNativeFactory(factory)
|
||
|
||
#if os(iOS) || os(tvOS)
|
||
window = UIWindow(frame: UIScreen.main.bounds)
|
||
factory.startReactNative(
|
||
withModuleName: "main",
|
||
in: window,
|
||
launchOptions: launchOptions)
|
||
#endif
|
||
|
||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||
}
|
||
|
||
// MARK: - Application Lifecycle
|
||
|
||
public override func applicationDidEnterBackground(_ application: UIApplication) {
|
||
super.applicationDidEnterBackground(application)
|
||
|
||
// 后台任务调度现在由 BackgroundTaskBridge 统一管理
|
||
// 这里只需要确保应用有足够的时间完成后台任务
|
||
if #available(iOS 13.0, *) {
|
||
NSLog("[AppDelegate] 应用进入后台,后台任务由 BackgroundTaskBridge 管理")
|
||
}
|
||
}
|
||
|
||
// 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) {
|
||
NSLog("[AppDelegate] ====== 后台任务被触发 ======")
|
||
NSLog("[AppDelegate] 任务标识符: \(identifier)")
|
||
NSLog("[AppDelegate] 任务类型: \(type(of: task))")
|
||
|
||
// 设置任务过期处理器(iOS 给的执行时间有限,通常30秒)
|
||
task.expirationHandler = {
|
||
NSLog("[AppDelegate] ⚠️ 后台任务即将过期,强制完成")
|
||
task.setTaskCompleted(success: false)
|
||
}
|
||
|
||
// 尝试获取 BackgroundTaskBridge 实例来处理任务
|
||
// 如果 React Native bridge 还未初始化,我们仍然可以执行一些基本的后台工作
|
||
guard let bridge = reactNativeFactory?.bridge,
|
||
bridge.isValid else {
|
||
NSLog("[AppDelegate] React Native bridge 未就绪")
|
||
NSLog("[AppDelegate] 执行基本的后台任务并调度下一次")
|
||
|
||
// 即使 JS 层不可用,也执行基本的后台维护
|
||
self.executeBasicBackgroundMaintenance(task: task, identifier: identifier)
|
||
return
|
||
}
|
||
|
||
NSLog("[AppDelegate] React Native bridge 已就绪,尝试获取 BackgroundTaskBridge 模块")
|
||
|
||
// 通过 bridge 查找 BackgroundTaskBridge 模块
|
||
DispatchQueue.main.async {
|
||
if let module = bridge.module(for: BackgroundTaskBridge.self) as? BackgroundTaskBridge {
|
||
NSLog("[AppDelegate] ✅ 找到 BackgroundTaskBridge 模块,发送任务通知")
|
||
// 通知 BackgroundTaskBridge 处理任务
|
||
NotificationCenter.default.post(
|
||
name: NSNotification.Name("BackgroundTaskBridge.handleTask"),
|
||
object: nil,
|
||
userInfo: ["task": task, "identifier": identifier]
|
||
)
|
||
} else {
|
||
NSLog("[AppDelegate] ❌ BackgroundTaskBridge 模块未找到")
|
||
NSLog("[AppDelegate] 执行基本的后台任务并调度下一次")
|
||
self.executeBasicBackgroundMaintenance(task: task, identifier: identifier)
|
||
}
|
||
}
|
||
}
|
||
|
||
@available(iOS 13.0, *)
|
||
private func executeBasicBackgroundMaintenance(task: BGTask, identifier: String) {
|
||
NSLog("[AppDelegate] 执行基本后台维护任务")
|
||
|
||
// 在后台线程执行基本维护
|
||
DispatchQueue.global(qos: .background).async {
|
||
// 执行一些基本的后台工作
|
||
// 例如:清理缓存、检查应用状态等
|
||
|
||
// 模拟一些工作
|
||
Thread.sleep(forTimeInterval: 2.0)
|
||
|
||
NSLog("[AppDelegate] 基本后台维护完成")
|
||
|
||
// 标记任务完成
|
||
task.setTaskCompleted(success: true)
|
||
|
||
// 调度下一次后台任务
|
||
self.scheduleNextBackgroundTask(identifier: identifier)
|
||
}
|
||
}
|
||
|
||
@available(iOS 13.0, *)
|
||
private func scheduleNextBackgroundTask(identifier: String) {
|
||
let request = BGAppRefreshTaskRequest(identifier: identifier)
|
||
// 设置最早开始时间(15分钟后)
|
||
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
|
||
|
||
do {
|
||
try BGTaskScheduler.shared.submit(request)
|
||
NSLog("[AppDelegate] ✅ 成功调度下一次后台任务,15分钟后")
|
||
} catch {
|
||
NSLog("[AppDelegate] ❌ 调度下一次后台任务失败: \(error.localizedDescription)")
|
||
}
|
||
}
|
||
|
||
// Linking API
|
||
public override func application(
|
||
_ app: UIApplication,
|
||
open url: URL,
|
||
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
|
||
) -> Bool {
|
||
return super.application(app, open: url, options: options) || RCTLinkingManager.application(app, open: url, options: options)
|
||
}
|
||
|
||
// Universal Links
|
||
public override func application(
|
||
_ application: UIApplication,
|
||
continue userActivity: NSUserActivity,
|
||
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
|
||
) -> Bool {
|
||
let result = RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler)
|
||
return super.application(application, continue: userActivity, restorationHandler: restorationHandler) || result
|
||
}
|
||
}
|
||
|
||
class ReactNativeDelegate: ExpoReactNativeFactoryDelegate {
|
||
// Extension point for config-plugins
|
||
|
||
override func sourceURL(for bridge: RCTBridge) -> URL? {
|
||
// needed to return the correct URL for expo-dev-client.
|
||
bridge.bundleURL ?? bundleURL()
|
||
}
|
||
|
||
override func bundleURL() -> URL? {
|
||
#if DEBUG
|
||
return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: ".expo/.virtual-metro-entry")
|
||
#else
|
||
return Bundle.main.url(forResource: "main", withExtension: "jsbundle")
|
||
#endif
|
||
}
|
||
}
|