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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 02:12:37