SwiftUI自定义MKMapView标注失败求助:UIHostingController无效果
修复SwiftUI自定义MKMapView标注视图不显示的问题
核心问题分析
你当前的代码存在几个关键问题导致自定义标注无法正常显示:
- 错误将SwiftUI视图设置为标注的弹出气泡附属视图(
detailCalloutAccessoryView),而非标注本身的视图 - 未保留
UIHostingController的引用,导致SwiftUI视图生命周期异常 - 标注视图未使用复用机制,性能差且可能引发渲染问题
GasStationAnnotationView中误用@State管理传入参数,导致初始值覆盖传入值
修复步骤及代码
1. 修正GasStationAnnotationView的变量定义
将传入参数的@State改为let,仅保留需要内部修改的状态(如按钮点击状态),同时给视图设置固定尺寸确保渲染正常:
import SwiftUI struct GasStationAnnotationView: View { // 仅保留内部状态变量 @State private var gasStationButtonTapped: Bool = false @State private var widthSize: CGFloat = .zero // 传入参数用let声明 let backgroundColor: UIColor let gasStationImage: String let idStation: Int let gasStationAnnotation: GasStationAnnotation? var body: some View { VStack(alignment: .center, spacing: .zero) { HStack(spacing: .zero) { Button { withAnimation(.easeInOut(duration: 0.2)) { gasStationButtonTapped.toggle() widthSize = gasStationButtonTapped ? 50 : .zero } } label: { Image(gasStationImage) .resizable() .scaledToFit() .frame(width: 30, height: 30) .cornerRadius(36) } .clipShape(Circle()) .padding(2) .background(Color(backgroundColor)) .cornerRadius(36) Button {} label: { Text("info") } .frame(width: widthSize, height: 20) } .background(Color(backgroundColor)) .cornerRadius(36) Image(systemName: "triangle.fill") .resizable() .scaledToFit() .foregroundColor(Color(backgroundColor)) .frame(width: 5, height: 10) .rotationEffect(.degrees(180)) .offset(y: -3.5) .padding(.bottom, 20) } // 固定视图尺寸,确保MKAnnotationView能正确计算大小 .frame(width: 80, height: 60) } } struct GasStationAnnotationView_Previews: PreviewProvider { static var previews: some View { GasStationAnnotationView( backgroundColor: .white, gasStationImage: "other", idStation: 0, gasStationAnnotation: nil ) } }
2. 修改Mapita的viewFor方法,正确嵌入SwiftUI视图到标注
使用复用机制创建MKAnnotationView,并将SwiftUI视图通过UIHostingController作为标注的子视图,同时保留控制器引用避免释放:
struct Mapita: UIViewRepresentable { @Binding var region: MKCoordinateRegion var gasAnnotations: [GasStationAnnotation] func makeUIView(context: Context) -> MKMapView { let mapView = MKMapView() mapView.delegate = context.coordinator // 注册可复用的标注视图 mapView.register(MKAnnotationView.self, forAnnotationViewWithReuseIdentifier: "GasStationAnnotation") return mapView } func updateUIView(_ mapView: MKMapView, context: Context) { mapView.region = region mapView.showsUserLocation = true // 优化:仅更新差异标注,避免频繁删除添加 let existingAnnotations = mapView.annotations.filter { $0 is GasStationAnnotation } let newAnnotations = gasAnnotations let toRemove = existingAnnotations.filter { !newAnnotations.contains($0 as! GasStationAnnotation) } let toAdd = newAnnotations.filter { !existingAnnotations.contains($0) } mapView.removeAnnotations(toRemove) mapView.addAnnotations(toAdd) } func makeCoordinator() -> MapitaCoordinator { MapitaCoordinator(self) } class MapitaCoordinator: NSObject, MKMapViewDelegate { var parent: Mapita // 保留UIHostingController的引用,避免被释放 private var hostingControllers: [MKAnnotation: UIHostingController<GasStationAnnotationView>] = [:] init(_ parent: Mapita) { self.parent = parent } func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) { parent.region = mapView.region } func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { guard let customAnnotation = annotation as? GasStationAnnotation else { // 返回默认的用户位置标注视图 if annotation is MKUserLocation { return nil } return nil } // 复用标注视图 let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "GasStationAnnotation", for: customAnnotation) annotationView.annotation = customAnnotation annotationView.canShowCallout = true // 如果需要点击弹出气泡可以保留 // 移除旧的子视图和控制器 if let oldController = hostingControllers[customAnnotation] { oldController.view.removeFromSuperview() oldController.removeFromParent() hostingControllers.removeValue(forKey: customAnnotation) } // 创建SwiftUI视图和Hosting控制器 let customView = GasStationAnnotationView( backgroundColor: .white, gasStationImage: customAnnotation.companyImage, idStation: customAnnotation.gasStationID, gasStationAnnotation: customAnnotation ) let hostingController = UIHostingController(rootView: customView) // 设置Hosting控制器的视图约束,使其填充标注视图 hostingController.view.translatesAutoresizingMaskIntoConstraints = false annotationView.addSubview(hostingController.view) NSLayoutConstraint.activate([ hostingController.view.topAnchor.constraint(equalTo: annotationView.topAnchor), hostingController.view.leadingAnchor.constraint(equalTo: annotationView.leadingAnchor), hostingController.view.trailingAnchor.constraint(equalTo: annotationView.trailingAnchor), hostingController.view.bottomAnchor.constraint(equalTo: annotationView.bottomAnchor) ]) // 保留控制器引用 hostingControllers[customAnnotation] = hostingController // 调整偏移量,让三角部分准确指向地图坐标点 annotationView.centerOffset = CGPoint(x: 0, y: -30) return annotationView } // 标注视图被回收时,清理Hosting控制器 func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) { guard let annotation = view.annotation else { return } if let controller = hostingControllers[annotation] { controller.view.removeFromSuperview() controller.removeFromParent() hostingControllers.removeValue(forKey: annotation) } } } }
3. 确保GasStationAnnotation实现正确
确保自定义Annotation类正确实现MKAnnotation协议,并添加Equatable用于差异比较:
class GasStationAnnotation: NSObject, MKAnnotation, Equatable { var coordinate: CLLocationCoordinate2D var companyImage: String var gasStationID: Int init(coordinate: CLLocationCoordinate2D, companyImage: String, gasStationID: Int) { self.coordinate = coordinate self.companyImage = companyImage self.gasStationID = gasStationID super.init() } // 实现Equatable,用于updateUIView中的差异判断 static func == (lhs: GasStationAnnotation, rhs: GasStationAnnotation) -> Bool { lhs.gasStationID == rhs.gasStationID } }
关键说明
- 保留UIHostingController引用:如果不保留控制器,系统会在视图添加后释放控制器,导致SwiftUI视图无法正常渲染或响应事件
- 标注视图复用:使用
dequeueReusableAnnotationView提升性能,避免频繁创建视图 - 尺寸约束:给SwiftUI视图设置固定尺寸或通过约束确保MKAnnotationView能正确计算大小
- centerOffset调整:通过
annotationView.centerOffset让标注的三角部分准确指向地图坐标点
内容的提问来源于stack exchange,提问作者coden0w
相关产品推荐
相关产品推荐

