You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Apple WeatherKit的天气应用启动优化与后台刷新需求

代码优化方案(解决崩溃+实现启动/后台刷新)

核心问题分析

  1. 启动崩溃根源:
    • App初始化时直接调用refresh(),此时CLLocationManager未完成权限校验,location为nil却强制解包(currentLocation!)导致崩溃。
    • 重复创建CLLocationManager实例,未统一管理权限请求流程。
    • 启动屏固定5秒消失,未和数据加载状态绑定,可能出现数据未加载完就进入主界面的情况。
  2. 功能缺失:
    • 后台刷新间隔设为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
    }
}

关键优化点说明

  1. 启动崩溃修复:

    • 移除App初始化时的refresh()调用,改为在onAppear中异步执行,确保UI初始化完成。
    • 封装位置请求逻辑,统一管理CLLocationManager,处理权限校验和位置为空的情况,避免强制解包。
    • 启动屏等待数据加载完成后再消失,避免空白或错误数据展示。
  2. 启动刷新实现:

    • 在App主界面onAppear时触发weatherModel.refresh(),完成后关闭启动屏。
    • 并行执行位置编码和天气获取,提升启动加载效率。
  3. 后台30分钟刷新实现:

    • 将earliestBeginDate改为30分钟(30*60秒)。
    • 注册后台任务处理器,确保任务执行完成后标记成功,同时重新调度下一次刷新。
    • 添加后台任务超时处理,避免系统强制终止。
  4. 代码健壮性提升:

    • 将回调式的逆地理编码改为async/await,统一异步逻辑风格,避免线程不安全。
    • 补充所有缺失的属性定义,处理可选值的空情况,避免未定义变量导致的崩溃。
    • 添加错误日志,方便调试问题。

内容的提问来源于stack exchange,提问作者L'ALLORET Pierre-Louis

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.03 23:11:14