SwiftUI Map直接使用Place作为注解触发运行时错误的原因排查
环境配置
我的应用使用带注解的SwiftUI Map,注解预期是Place类的实例,该类定义如下:
final class Place: NSManagedObject, UpdateTimestampable, Identifiable { @NSManaged var horizontalAccuracy: Double @NSManaged var latitude: Double @NSManaged var longitude: Double @NSManaged var name: String @NSManaged var namesOfItemsBoughtHere: Set<String>? @NSManaged var radius: Double @NSManaged var updatedAt: Date? var coordinate: CLLocationCoordinate2D { var coord = CLLocationCoordinate2D() if let context = self.managedObjectContext { context.performAndWait { coord = CLLocationCoordinate2D(latitude: latitude, longitude: longitude) } } else { fatalError("Place without context") } return coord } }
地图中用作注解的地点由MapViewModel提供:
let placesForBuyLocationNames = mapViewModel.placesForBuyLocationNames()
问题描述
直接将Place实例作为注解传入Map时,会触发运行时错误:
let annotations = placesForBuyLocationNames Map(coordinateRegion: $region, interactionModes: .all, showsUserLocation: false, annotationItems: annotations) { annotation in MapAnnotation(coordinate: annotation.coordinate, anchorPoint: CGPoint(x: 0.5, y: 1)) { AnnotationView(region: region, buyPlaceNameBeforeDragging: annotation.name, mapviewModel: mapViewModel) } }
错误信息:
Publishing changes from within view updates is not allowed, this will cause undefined behavior.
但如果把Place映射成一个辅助结构体Annotation再传入,就不会出现错误:
struct Annotation: Identifiable { let id = UUID() let name: String let coordinate: CLLocationCoordinate2D } // … let annotations = placesForBuyLocationNames.map { Annotation(name: $0.name, coordinate: $0.coordinate) }
错误原因与代码问题分析
NSManagedObject的KVO特性触发视图更新循环
Place是NSManagedObject子类,Core Data会为其属性自动开启键值观察(KVO)。当Map在视图更新过程中访问annotation.coordinate时,这个计算属性内部调用了managedObjectContext.performAndWait,读取latitude和longitude的操作会被Core Data的KVO机制误判为属性变更,触发状态变更通知。SwiftUI收到通知后会发起新的视图更新,形成视图更新过程中发布状态变更的非法操作,直接触发报错。计算属性的实现方式加剧冲突
coordinate计算属性每次访问都会通过performAndWait在Core Data上下文队列中读取值,这个跨队列操作会被SwiftUI的视图更新机制误识别为状态变化。而辅助结构体Annotation是值类型,初始化时直接拷贝Place的属性值,后续访问不会再触发Core Data的KVO或上下文操作,完全隔离了视图更新和Core Data操作的干扰。Identifiable实现的隐式风险
Place作为NSManagedObject,默认用objectID作为Identifiable的id。但在视图更新过程中,如果Core Data上下文发生变更(比如对象从临时ID转为永久ID),objectID可能会隐式变化,导致SwiftUI重新计算注解视图,间接加剧状态发布的冲突。而自定义Annotation用UUID作为稳定的id,彻底避免了这个问题。
内容的提问来源于stack exchange,提问作者Reinhard Männer

