基于Apple WeatherKit的天气应用启动优化与后台刷新需求
代码优化方案(解决崩溃+实现启动/后台刷新)
核心问题分析
- 启动崩溃根源:
- App初始化时直接调用
refresh(),此时CLLocationManager未完成权限校验,location为nil却强制解包(currentLocation!)导致崩溃。 - 重复创建
CLLocationManager实例,未统一管理权限请求流程。 - 启动屏固定5秒消失,未和数据加载状态绑定,可能出现数据未加载完就进入主界面的情况。
- App初始化时直接调用
- 功能缺失:
- 后台刷新间隔设为24小时,未满足30分钟刷新需求。
- 位置逆地理编码使用回调,和异步逻辑混合导致线程不安全。
优化后的完整代码
1. App主入口(PlaneWXApp.swift)
import SwiftUI import BackgroundTasks @main struct PlaneWXApp: App { @Environment(\.scenePhase) private var phase @StateObject private var weatherModel = WeatherModel() @StateObject private var launchScreenManager = LaunchScreenManager() @State private var selection = 3 init() { // 注册后台刷新任务 BGTaskScheduler.shared.register(forTaskWithIdentifier: "myapprefresh", using: nil) { task in handleAppRefreshTask(task: task as! BGAppRefreshTask) } } var body: some Scene { WindowGroup { ZStack { TabView(selection: $selection) { AlertView(weatherModel: weatherModel).tag(1) TodayView(weatherModel: weatherModel).tag(2) HomeView(weatherModel: weatherModel).tag(3) ForecastView(weatherModel: weatherModel).tag(4) } .tabViewStyle(PageTabViewStyle(indexDisplayMode: .always)) .onAppear { // 初始化页面样式 UIPageControl.appearance().currentPageIndicatorTintColor = .black UIPageControl.appearance().pageIndicatorTintColor = UIColor.black.withAlphaComponent(0.2) // 启动时触发数据刷新,完成后关闭启动屏 Task { await weatherModel.refresh() launchScreenManager.dismiss() } } if launchScreenManager.state != .completed { LaunchScreenView() } } .environmentObject(launchScreenManager) } .onChange(of: phase) { newPhase in switch newPhase { case .background: schedule30MinuteAppRefresh() case .active: // 回到前台时也可触发一次刷新 Task { await weatherModel.refresh() } default: break } } } private func schedule30MinuteAppRefresh() { let request = BGAppRefreshTaskRequest(identifier: "myapprefresh") // 设置30分钟后可刷新 request.earliestBeginDate = Date().addingTimeInterval(30 * 60) do { try BGTaskScheduler.shared.submit(request) } catch { print("提交后台刷新任务失败: \(error.localizedDescription)") } } private func handleAppRefreshTask(task: BGAppRefreshTask) { // 重新调度下一次刷新 schedule30MinuteAppRefresh() // 执行刷新任务 Task { await weatherModel.refresh() task.setTaskCompleted(success: true) } // 设置超时处理 task.expirationHandler = { task.setTaskCompleted(success: false) } } }
2. WeatherModel.swift
import Foundation import CoreLocation import WeatherKit @MainActor class WeatherModel: ObservableObject { private let locationManager = CLLocationManager() private let locationProvider = LocationProvider() private let weatherService = WeatherService() // 补充缺失的Published属性 @Published var cityName: String = "加载中..." @Published var temperature: String? @Published var minTemperature: String? @Published var maxTemperature: String? @Published var feelTemperature: String? @Published var feelTemperatureDescription: String? @Published var dewPoint: String? @Published var alerts: [WeatherAlertInfo] = [] @Published var wind: [Wind] = [] init() { // 配置位置管理器 locationManager.desiredAccuracy = kCLLocationAccuracyThreeKilometers locationManager.requestWhenInUseAuthorization() } func refresh() async { // 先确保获取到位置权限和当前位置 guard let currentLocation = await requestCurrentLocation() else { cityName = "无法获取位置" return } // 并行执行位置编码和天气获取,提升效率 async let addressTask = locationProvider.getPlace(for: currentLocation) async let weatherTask = weatherService.weather(for: currentLocation) // 处理结果 if let placemark = await addressTask { updateCityName(from: placemark) } else { cityName = "地址未知" } if let weather = await weatherTask { updateWeatherData(from: weather) } else { resetWeatherData() } } // 封装位置请求逻辑,避免重复创建CLLocationManager private func requestCurrentLocation() async -> CLLocation? { guard CLLocationManager.locationServicesEnabled() else { print("位置服务未开启") return nil } let status = locationManager.authorizationStatus guard status == .authorizedWhenInUse || status == .authorizedAlways else { print("位置权限未授权") return nil } // 异步获取当前位置 return await withCheckedContinuation { continuation in locationManager.requestLocation() locationManager.delegate = self self.locationContinuation = continuation } } private func updateCityName(from placemark: CLPlacemark) { if let city = placemark.locality, let state = placemark.administrativeArea { cityName = "\(city), \(state)" } else if let city = placemark.locality { cityName = city } else if let state = placemark.administrativeArea { cityName = state } else { cityName = "地址未知" } } private func updateWeatherData(from weather: Weather) { let todayForecast = weather.dailyForecast.first { Calendar.current.isDateInToday($0.date) } minTemperature = todayForecast?.lowTemperature.formatted() maxTemperature = todayForecast?.highTemperature.formatted() temperature = weather.currentWeather.temperature.formatted() feelTemperature = weather.currentWeather.apparentTemperature.formatted() dewPoint = weather.currentWeather.dewPoint.formatted() // 补充其他天气数据 alerts = weather.alerts.map { WeatherAlertInfo(from: $0) } wind = [WeatherModel.Wind(speed: weather.currentWeather.wind.speed.formatted(), direction: weather.currentWeather.wind.direction.description)] } private func resetWeatherData() { temperature = nil minTemperature = nil maxTemperature = nil feelTemperature = nil dewPoint = nil alerts = [] wind = [] } // 用于异步获取位置的Continuation private var locationContinuation: CheckedContinuation<CLLocation?, Never>? } // 扩展实现CLLocationManagerDelegate extension WeatherModel: CLLocationManagerDelegate { func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { locationContinuation?.resume(returning: locations.first) locationContinuation = nil manager.stopUpdatingLocation() } func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { print("获取位置失败: \(error.localizedDescription)") locationContinuation?.resume(returning: nil) locationContinuation = nil } } // 补充WeatherModel内部的Wind和WeatherAlertInfo定义(根据实际需求调整) extension WeatherModel { struct Wind { let speed: String let direction: String } struct WeatherAlertInfo { let title: String let summary: String init(from alert: WeatherAlert) { self.title = alert.title self.summary = alert.summary } } }
3. LocationProvider.swift
import CoreLocation class LocationProvider { // 将回调版本改为async/await版本,避免线程问题 func getPlace(for location: CLLocation) async -> CLPlacemark? { return await withCheckedContinuation { continuation in let geocoder = CLGeocoder() geocoder.reverseGeocodeLocation(location) { placemarks, error in if let error = error { print("逆地理编码失败: \(error.localizedDescription)") continuation.resume(returning: nil) return } continuation.resume(returning: placemarks?.first) } } } }
4. LaunchScreenManager.swift(补充完整)
import Foundation class LaunchScreenManager: ObservableObject { enum LaunchState { case initial, completed } @Published var state: LaunchState = .initial func dismiss() { state = .completed } }
关键优化点说明
启动崩溃修复:
- 移除App初始化时的
refresh()调用,改为在onAppear中异步执行,确保UI初始化完成。 - 封装位置请求逻辑,统一管理
CLLocationManager,处理权限校验和位置为空的情况,避免强制解包。 - 启动屏等待数据加载完成后再消失,避免空白或错误数据展示。
- 移除App初始化时的
启动刷新实现:
- 在App主界面
onAppear时触发weatherModel.refresh(),完成后关闭启动屏。 - 并行执行位置编码和天气获取,提升启动加载效率。
- 在App主界面
后台30分钟刷新实现:
- 将
earliestBeginDate改为30分钟(30*60秒)。 - 注册后台任务处理器,确保任务执行完成后标记成功,同时重新调度下一次刷新。
- 添加后台任务超时处理,避免系统强制终止。
- 将
代码健壮性提升:
- 将回调式的逆地理编码改为
async/await,统一异步逻辑风格,避免线程不安全。 - 补充所有缺失的属性定义,处理可选值的空情况,避免未定义变量导致的崩溃。
- 添加错误日志,方便调试问题。
- 将回调式的逆地理编码改为
内容的提问来源于stack exchange,提问作者L'ALLORET Pierre-Louis
相关产品推荐
相关产品推荐

