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

Swift WidgetKit速度计组件问题:位置返回Null且更新不及时

速度计Widget无法持续更新速度值的问题解决

我希望速度计组件能尽快更新,但不知道如何设置类或getTimeline函数来实现。权限设置均正确,因为我偶尔能获取到速度值,但很快就会回到默认设置值。

原代码

import WidgetKit
import SwiftUI
import Intents
import CoreLocation
import Foundation

struct Provider: IntentTimelineProvider {
    class LocationManager: NSObject, CLLocationManagerDelegate {

        var locationManager = CLLocationManager()
        var location: CLLocation?

        override init() {
            super.init()
            locationManager.delegate = self
            locationManager.desiredAccuracy = kCLLocationAccuracyBest
            locationManager.requestWhenInUseAuthorization()
            locationManager.startUpdatingLocation()
            WidgetCenter.shared.reloadAllTimelines()

            let _ = print("Location Manager Init")
        }

        func requestUpdate(_ completion: @escaping () -> Void) {
            self.locationManager.requestWhenInUseAuthorization()
            self.locationManager.startUpdatingLocation()
            WidgetCenter.shared.reloadAllTimelines()
        }

        func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
            guard let location = locations.last else { return }
            self.location = location
        }

        func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
            print(error.localizedDescription)
        }
    }
    
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date(), configuration: ConfigurationIntent(), locManager: Provider.LocationManager.init())
    }

    func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(date: Date(), configuration: configuration, locManager: Provider.LocationManager.init())
        completion(entry)
    }
    
    func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        var entries: [SimpleEntry] = []

        let currentDate = Date()
        for offset in 0 ..< 20 {
            let entryDate = Calendar.current.date(byAdding: .second, value: offset, to: currentDate)!
            let locManager = Provider.LocationManager.init()
            let entry = SimpleEntry(date: entryDate, configuration: configuration, locManager: locManager)
            entries.append(entry)
        }

        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
}

struct SimpleEntry: TimelineEntry {
    let date: Date
    let configuration: ConfigurationIntent
    let locManager: Provider.LocationManager
}

struct SpeedometerEntryView : View {
    @Environment(\.widgetFamily) var widgetFamily
    
    var entry: Provider.Entry
    
    var displaySpeed: Text {
        var speed: Double = (entry.locManager.location?.speed ?? Double(30.8458)) * 2.237
        
        if speed < 0 {
            speed = speed * -1
        }
        
        let intSpeed: Int = Int(speed)
        return Text(intSpeed.formatted())
    }
    
    var gaugeValue: Double {
        var speed: Double = (entry.locManager.location?.speed ?? Double(30.8458)) * 2.237 / 100
        
        if speed > 1 {
            speed = 1
        }
        
        else if speed < 0 {
            speed = speed * -1
        }
        
        return speed
    }
        
    var body: some View {
        Gauge(
            value: gaugeValue,
            label: {Text("MPH")},
            currentValueLabel: {displaySpeed}
        )
        .gaugeStyle(.accessoryCircular)
    }
}

struct Speedometer: Widget {
    let kind: String = "Speedometer"
    
    var body: some WidgetConfiguration {
        IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
            SpeedometerEntryView(entry: entry)
        }
        .configurationDisplayName("Speed Widget")
        .description("Current Speed")
        .supportedFamilies([.accessoryCircular])
    }
}

问题分析与修复方案

原代码存在几个核心问题导致速度无法持续更新:

  • 每次生成SimpleEntry都初始化新的LocationManager,位置实例不共享,无法保留最新位置数据
  • 预生成20条间隔1秒的entry不符合Widget更新逻辑,应该由位置变化触发实时刷新
  • LocationManager初始化时直接刷新Widget,此时还未获取到有效位置,且未在位置更新时触发刷新
  • 未合理管理位置更新的启停,造成资源浪费

以下是修复后的代码:

1. 重构LocationManager为单例,确保全局唯一实例

class LocationManager: NSObject, CLLocationManagerDelegate {
    static let shared = LocationManager()
    
    private let locationManager = CLLocationManager()
    var currentSpeed: Double = 0.0
    private var onUpdate: (() -> Void)?
    
    private override init() {
        super.init()
        locationManager.delegate = self
        locationManager.desiredAccuracy = kCLLocationAccuracyBest
        locationManager.requestWhenInUseAuthorization()
    }
    
    func startUpdatingSpeed(completion: @escaping () -> Void) {
        onUpdate = completion
        locationManager.startUpdatingLocation()
    }
    
    func stopUpdatingSpeed() {
        locationManager.stopUpdatingLocation()
        onUpdate = nil
    }
    
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let location = locations.last else { return }
        // 处理速度值,转为MPH并确保非负
        let speedMPH = abs(location.speed * 2.237)
        currentSpeed = speedMPH
        // 获取到新位置后触发Widget刷新
        onUpdate?()
    }
    
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        print(error.localizedDescription)
    }
}

2. 修改Provider与Timeline逻辑

struct Provider: IntentTimelineProvider {
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date(), configuration: ConfigurationIntent(), speed: 0.0)
    }

    func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(date: Date(), configuration: ConfigurationIntent(), speed: LocationManager.shared.currentSpeed)
        completion(entry)
    }
    
    func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        // 获取当前速度生成entry
        let currentDate = Date()
        let entry = SimpleEntry(date: currentDate, configuration: configuration, speed: LocationManager.shared.currentSpeed)
        
        // 启动位置更新,获取新速度后刷新Timeline
        LocationManager.shared.startUpdatingSpeed {
            WidgetCenter.shared.reloadAllTimelines()
        }
        
        // 设置Timeline策略为手动刷新(由位置更新触发)
        let timeline = Timeline(entries: [entry], policy: .never)
        completion(timeline)
    }
}

3. 简化SimpleEntry,直接存储速度值

struct SimpleEntry: TimelineEntry {
    let date: Date
    let configuration: ConfigurationIntent
    let speed: Double
}

4. 更新视图逻辑,直接使用entry中的速度值

struct SpeedometerEntryView : View {
    @Environment(\.widgetFamily) var widgetFamily
    var entry: Provider.Entry
    
    var displaySpeed: Text {
        let intSpeed = Int(entry.speed)
        return Text(intSpeed.formatted())
    }
    
    var gaugeValue: Double {
        // 将速度值归一化到0-1范围(假设最大显示100MPH)
        min(entry.speed / 100, 1.0)
    }
        
    var body: some View {
        Gauge(
            value: gaugeValue,
            label: {Text("MPH")},
            currentValueLabel: {displaySpeed}
        )
        .gaugeStyle(.accessoryCircular)
    }
}

5. 保持Widget定义不变

struct Speedometer: Widget {
    let kind: String = "Speedometer"
    
    var body: some WidgetConfiguration {
        IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
            SpeedometerEntryView(entry: entry)
        }
        .configurationDisplayName("Speed Widget")
        .description("Current Speed")
        .supportedFamilies([.accessoryCircular])
    }
}

关键修改说明

  • 单例LocationManager:确保整个Widget生命周期内只有一个位置管理器实例,持续获取并保留最新速度值
  • 位置更新触发刷新:在获取到新位置数据后立即调用WidgetCenter.shared.reloadAllTimelines(),实现速度变化时的实时更新
  • 简化Timeline逻辑:只生成当前时间的entry,采用.never策略,避免预生成无效entry,完全由位置变化驱动更新
  • Entry存储具体数据:不再传递管理类,直接存储计算好的速度值,让视图逻辑更简洁

内容的提问来源于stack exchange,提问作者Connor Sabine

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 14:05:54