@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
相关产品推荐
相关产品推荐

