SwiftUI/MapKit 从MongoDB Realm填充MapAnnotation标注
SwiftUI 配合 Realm 灵活同步加载地图标注问题解决
问题背景
- 技术栈:SwiftUI、Realm 灵活同步(对接 MongoDB Atlas)、MapKit
- 需求:从 MongoDB Atlas 的
Centre集合读取点位数据,在地图上渲染位置标注
初始实现与问题
初始数据模型定义
首先定义了存储点位信息的Realm对象,其中经纬度存储在嵌入对象Coordinates中,规则为x存经度、y存纬度:
class Centre: Object, ObjectKeyIdentifiable { @Persisted var _id: ObjectId = ObjectId.generate() @Persisted var centreName = "" @Persisted var centreDesc = "" @Persisted var centreLocation: Coordinates? override static func primaryKey() -> String? { return "_id" } convenience init(centreName: String, centreDesc: String, centreLocation: Coordinates) { self.init() self.centreName = centreName self.centreDesc = centreDesc self.centreLocation = centreLocation } } class Coordinates: EmbeddedObject, ObjectKeyIdentifiable { @Persisted var x: Double? @Persisted var y: Double? }
为适配MapKit的MapAnnotation协议要求,额外定义了遵循Identifiable的标注结构体:
struct CustomAnnots: Identifiable { let id: UUID var nameCentreLoc: String var descCentreLoc: String let latitude: Double let longitude: Double var coordinate: CLLocationCoordinate2D { CLLocationCoordinate2D(latitude: latitude, longitude: longitude) } }
遇到的异常
- 语法错误+空值崩溃:第一版LocationView代码存在数组初始化语法错误,调整后无语法报错,但运行时触发
Force unwrapping nil value强制解包空值崩溃
// 存在问题的初始实现 struct LocationView: View { @Environment(\.realm) var realm @ObservedResults(Centre.self) var centres @ObservedRealmObject var centre: Centre @State private var nameCentreLoc = "" @State private var descCentreLoc = "" @State private var latitude = 0.0 @State private var longitude = 0.0 @State private var annots = [] @State private var region = MKCoordinateRegion( center: CLLocationCoordinate2D(latitude: 24.681_858, longitude: 81.811_623), span: MKCoordinateSpan(latitudeDelta: 10, longitudeDelta: 10) ) var body: some View { Map(coordinateRegion: $region, annotationItems: annots, annotationContent: { locations in MapPin(coordinate: locations.coordinate) }) .onAppear { setSubscription() initData() } } private func setSubscription() { let subscriptions = realm.subscriptions subscriptions.write { if let currentSubscription = subscriptions.first(named: "all_centres") { currentSubscription.update(toType: Centre.self) { centre in centre.centreName != "" } } else { subscriptions.append(QuerySubscription<Centre>(name: "all_centres") { centre in centre.centreName != "" }) } } } private func initData() { nameCentreLoc = centre.centreName descCentreLoc = centre.centreDesc latitude = (centre.centreLocation?.y)! longitude = (centre.centreLocation?.x)! // 此处定义的是局部变量annots,不会修改@State的annots属性 let annots = [for centre in centres { CustomAnnots(id: UUID(), nameCentreLoc: nameCentreLoc, descCentreLoc: descCentreLoc, latitude: latitude, longitude: longitude) }] } }
- 数据能打印但地图不显示标注:写遍历方法可以在控制台正常打印所有点位的经纬度、名称信息,但在视图层循环生成标注数组后,地图始终无法渲染标注
// 可正常打印数据的遍历方法 func getLoc() { for centre in centres { var annots = [CustomAnnots.init(id: UUID(), nameCentreLoc: centre.centreName, descCentreLoc: centre.centreDesc, latitude: (centre.centreLocation?.y)!, longitude: (centre.centreLocation?.x)!)] print(annots) } }
控制台打印输出正常:
[ACCv5.CustomAnnots(id: 67E9DADA-0BCC-4D30-8136-8B666881E82D, nameCentreLoc: "HO", descCentreLoc: "Head Office Artemis Cardiac Care Gurgaon", latitude: 28.438694893842058, longitude: 77.10845294294181)] [ACCv5.CustomAnnots(id: 26DC0C63-5A17-49C7-B4BF-FD3AA1ABF65E, nameCentreLoc: "Panipat", descCentreLoc: "Artemis Heart Centre at Ravindra Hospital", latitude: 29.388306713854682, longitude: 76.95889693063663)] [ACCv5.CustomAnnots(id: D3A70E58-6B65-4F5D-A398-3394B7FB04DF, nameCentreLoc: "Ranchi", descCentreLoc: "Artemis Heart Centre at Raj Hospital", latitude: 23.35731237118492, longitude: 85.32288933068195)]
但以下视图代码无法显示标注:
var body: some View { // ForEach未放在布局容器内,内部修改的是局部变量,不会更新@State的annots ForEach (centres) { centre in var annots = [CustomAnnots.init(id: UUID(), nameCentreLoc: centre.centreName, descCentreLoc: centre.centreDesc, latitude: (centre.centreLocation?.y)!, longitude: (centre.centreLocation?.x)!)] } Map(coordinateRegion: $region, annotationItems: annots, annotationContent: { locations in MapMarker(coordinate: locations.coordinate) }) }
最终可用方案
不需要额外定义CustomAnnots做数据转换,直接在Centre模型中添加coordinate计算属性,将@ObservedResults返回的centres集合直接作为Map的annotationItems数据源即可。这种方式省去了多余的数据转换逻辑,@ObservedResults本身是可观察集合,数据增删改会自动触发地图视图更新。
修改后的Centre模型
class Centre: Object, ObjectKeyIdentifiable { @Persisted var _id: ObjectId = ObjectId.generate() @Persisted var centreName = "" @Persisted var centreDesc = "" @Persisted var centreLocation: Coordinates? override static func primaryKey() -> String? { return "_id" } convenience init(centreName: String, centreDesc: String, x: Double, y: Double) { self.init() self.centreName = centreName self.centreDesc = centreDesc // 修复原构造器问题:先初始化Coordinates对象再赋值,避免可选链赋值失效 let location = Coordinates() location.x = x location.y = y self.centreLocation = location } // 直接给模型添加坐标计算属性,增加空值兜底避免崩溃 var coordinate: CLLocationCoordinate2D { guard let lat = centreLocation?.y, let lon = centreLocation?.x else { return CLLocationCoordinate2D(latitude: 0, longitude: 0) } return CLLocationCoordinate2D(latitude: lat, longitude: lon) } }
地图组件调用
Map(coordinateRegion: $region, annotationItems: centres) { centre in MapMarker(coordinate: centre.coordinate) }
注意:原参考实现中直接强制解包经纬度值,若Atlas中存在
centreLocation为空的脏数据会触发崩溃,生产环境建议保留空值兜底逻辑。
内容的提问来源于stack exchange,提问作者Manjinder Sandhu
相关产品推荐
相关产品推荐

