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

@Published修饰的ObservableObject变更后SwiftUI视图未更新问题

地理围栏触发时View不响应@Published变量更新的问题

我知道有很多类似讨论,但三周来始终没法让这段代码正常运行,希望能找到明显错误。数据来自一个ObservableObject类,其中locationManager的代理函数会根据当前地理围栏更新@Published变量。初始状态下变量能正常传递给View,但当MapViewModel的函数修改该变量时,视图完全没反应——打印显示变量本身确实变了,只是视图不响应更新。

关键代码片段

MapViewModel核心部分

class MapViewModel : NSObject, ObservableObject, CLLocationManagerDelegate, MKMapViewDelegate {
    
    @Published var sceneAccordingToMap = 0
    
      func var1toggle() {
          sceneAccordingToMap = 1
      }
      
      func var2toggle() {
          sceneAccordingToMap = 2
          
      }

      func var3toggle() {
          sceneAccordingToMap = 3
      }

    // 其他地理围栏相关代码...

    // 进入安全区域
    func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
        print("Entered: \(region.identifier) ")

        if (region.identifier == "SafeArea"){
            print(sceneAccordingToMap)
            var1toggle()
            print(sceneAccordingToMap)
        }

        else if (region.identifier == "SafeArea2"){
            print(sceneAccordingToMap)
            var2toggle()
            print(sceneAccordingToMap)
        }
    }
    
    // 离开安全区域
    func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
        print("Left: \(region.identifier) ")
        if (region.identifier == "SafeArea"){
            print(sceneAccordingToMap)
            var1toggle()
            print(sceneAccordingToMap)
        }
        
        else if (region.identifier == "SafeArea2"){
            print(sceneAccordingToMap)
            var2toggle()
            print(sceneAccordingToMap)
        }
    }  
}

ContentView代码

struct ContentView: View {
    
    @StateObject var dataFromMap = MapViewModel()
    
    var body: some View {
        VStack {
            MapView().ignoresSafeArea(.all)
            Text("\(dataFromMap.sceneAccordingToMap)")
                .padding()
        }
        .environmentObject(dataFromMap)
    }
}

MapViewModel完整代码

import CoreLocation
import MapKit
import SwiftUI
import RealityKit
import Combine

// 地图默认配置
enum mapDefaults {
    static let initialLocation = CLLocationCoordinate2D(latitude: -37.81785423438109, longitude:  144.97973738630145)
    static let initialSpan = MKCoordinateSpan(latitudeDelta:0.01, longitudeDelta:0.01)
}

class MapViewModel : NSObject, ObservableObject, CLLocationManagerDelegate, MKMapViewDelegate {
    
    @Published var sceneAccordingToMap = 0
    
      func var1toggle() {
          sceneAccordingToMap = 1
      }
      
      func var2toggle() {
          sceneAccordingToMap = 2
          
      }
      
      func var3toggle() {
          sceneAccordingToMap = 3
      }

    @Published private(set) var authorizationStatus: UNAuthorizationStatus?
    
    @Published var region: MKCoordinateRegion = MKCoordinateRegion(
        center: mapDefaults.initialLocation,
        span: mapDefaults.initialSpan)
    
    var userLatitude = CLLocationCoordinate2D().latitude
    var userLongitude = CLLocationCoordinate2D().longitude
    
    var initialCoordinate : CLLocationCoordinate2D?
    var movedCoordinate : CLLocationCoordinate2D?
    
    @Published var distanceInMeter : Double = 0.0
    
    let locations = [
       Location(name: "Safearea", coordinate: CLLocationCoordinate2D(latitude: 37.331702, longitude:  -122.030785)),
       Location(name: "SafeArea2", coordinate: CLLocationCoordinate2D(latitude: 37.330709, longitude:  -122.030459))
    ]
    
    // 创建15米半径的地理围栏区域
    let geofenceRegion = CLCircularRegion(center:CLLocationCoordinate2D(latitude: 37.331702, longitude:  -122.030785),
                                          radius: 15,
                                          identifier: "SafeArea")
    
    let geofenceRegion2 = CLCircularRegion(center:  CLLocationCoordinate2D(latitude: 37.330709, longitude:  -122.030459),
                                          radius: 15,
                                          identifier: "SafeArea2")
    
    var circle = MKCircle(center: mapDefaults.initialLocation, radius: 10)
    
    var locationMangager: CLLocationManager?
    
    // 检查定位服务是否开启
    func checkLocationEnabled() {
        if CLLocationManager.locationServicesEnabled() {
            locationMangager = CLLocationManager()
            locationMangager!.delegate = self
            locationMangager?.startUpdatingLocation()
            locationMangager?.startMonitoring(for: geofenceRegion)
            locationMangager?.startMonitoring(for: geofenceRegion2)
            print("定位服务已开启")
        }
        else{
            print("定位服务未开启")
        }
    }
    
    // 检查用户是否授权定位
    private func checkLocationAuth() {
        guard let locationMangager = locationMangager else {
            return
        }
        
        switch locationMangager.authorizationStatus{
            
        case .notDetermined:
            locationMangager.requestWhenInUseAuthorization()
        case .restricted:
            print("用户定位访问权限被限制")
        case .denied:
            print("用户拒绝定位访问权限")
        case .authorizedAlways, .authorizedWhenInUse:
            region = MKCoordinateRegion(center: locationMangager.location!.coordinate, span: mapDefaults.initialSpan)
        @unknown default:
            break
        }
    }
    
    // 用户修改定位权限后触发
    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        checkLocationAuth()
        requestAuthorization()
    }
    
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        let location = locations.last
        
        userLatitude = location!.coordinate.latitude
        userLongitude = location!.coordinate.longitude
        
        initialCoordinate = CLLocationCoordinate2D(latitude: userLatitude , longitude: userLongitude)
        movedCoordinate = CLLocationCoordinate2D(latitude: location!.coordinate.latitude , longitude: location!.coordinate.longitude)
        self.distanceInMeter = Double(movedCoordinate!.distance(to: initialCoordinate!))
        print(distanceInMeter)
    }
  
    // 进入安全区域
    func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
        print("Entered: \(region.identifier) ")

        if (region.identifier == "SafeArea"){
            print("进入SafeArea")
            print(sceneAccordingToMap)
            var1toggle()
            print(sceneAccordingToMap)
        }
        else if (region.identifier == "SafeArea2"){
            print("进入SafeArea2")
            print(sceneAccordingToMap)
            var2toggle()
            print(sceneAccordingToMap)
        }
    }
    
    // 离开安全区域
    func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
        print("Left: \(region.identifier) ")
        if (region.identifier == "SafeArea"){
            print("离开SafeArea")
            print(sceneAccordingToMap)
            var1toggle()
            print(sceneAccordingToMap)
        }
        else if (region.identifier == "SafeArea2"){
            print("离开SafeArea2")
            print(sceneAccordingToMap)
            var2toggle()
            print(sceneAccordingToMap)
        }
    }

    // 请求通知权限
    func requestAuthorization() {
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { isGranted, _ in
            DispatchQueue.main.async {
                self.authorizationStatus = isGranted ? .authorized : .denied
            }
        }
    }
    
    // 创建本地通知
    func triggerLocalNotification(subTitle: String, body: String){
        let content = UNMutableNotificationContent()
        content.title = "提醒!"
        content.subtitle = subTitle
        content.body = body
        content.sound = UNNotificationSound.default
        
        let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)
        let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
        UNUserNotificationCenter.current().add(request)
    }
    
    func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
         if let tileOverlay = overlay as? MKTileOverlay {
           let renderer = MKTileOverlayRenderer(overlay: tileOverlay)
           return renderer
         }
         return MKOverlayRenderer()
    }
}

// 计算两个坐标点之间的距离(米)
extension CLLocationCoordinate2D {
    func distance(to: CLLocationCoordinate2D) -> CLLocationDistance {
        MKMapPoint(self).distance(to: MKMapPoint(to))
    }
}

问题原因及解决方案

原因

CLLocationManager的代理方法(比如didEnterRegion、didExitRegion)是在后台线程执行的,而SwiftUI的@Published变量只有在主线程更新时,才能正确触发视图的刷新机制。当前代码中,var1toggle、var2toggle直接在后台线程更新sceneAccordingToMap,导致视图无法感知到变量变化。

解决方案

将更新@Published变量的代码切换到主线程执行,有两种方式:

方式1:修改toggle方法,包裹主线程

func var1toggle() {
    DispatchQueue.main.async {
        self.sceneAccordingToMap = 1
    }
}

func var2toggle() {
    DispatchQueue.main.async {
        self.sceneAccordingToMap = 2
    }
}

func var3toggle() {
    DispatchQueue.main.async {
        self.sceneAccordingToMap = 3
    }
}

方式2:在代理方法中调用toggle时切换主线程

// 进入安全区域
func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
    print("Entered: \(region.identifier) ")
    DispatchQueue.main.async {
        if (region.identifier == "SafeArea"){
            print("进入SafeArea")
            print(self.sceneAccordingToMap)
            self.var1toggle()
            print(self.sceneAccordingToMap)
        }
        else if (region.identifier == "SafeArea2"){
            print("进入SafeArea2")
            print(self.sceneAccordingToMap)
            self.var2toggle()
            print(self.sceneAccordingToMap)
        }
    }
}

// 离开安全区域同理
func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
    print("Left: \(region.identifier) ")
    DispatchQueue.main.async {
        if (region.identifier == "SafeArea"){
            print("离开SafeArea")
            print(self.sceneAccordingToMap)
            self.var1toggle()
            print(self.sceneAccordingToMap)
        }
        else if (region.identifier == "SafeArea2"){
            print("离开SafeArea2")
            print(self.sceneAccordingToMap)
            self.var2toggle()
            print(self.sceneAccordingToMap)
        }
    }
}

两种方式都能解决问题,推荐方式1,因为把线程切换的逻辑封装在toggle方法里,后续其他地方调用这些方法时也不需要再考虑线程问题。

内容的提问来源于stack exchange,提问作者Theuns Botha

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.17 01:30:47