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

iOS 17中SwiftUI的MapPolyline性能卡顿问题求助

解决iOS 17 MapKit折线重绘导致的性能问题

核心问题分析

卡顿源于SwiftUI的视图更新机制:每次services(车辆位置)更新时,整个Map视图会重新计算并渲染所有内容,包括数据量庞大的MapPolyline。而折线数据仅在视图加载时初始化,完全不需要随车辆位置更新,因此需要将静态折线与动态车辆标注隔离,避免不必要的重绘。


方案1:拆分静态折线为独立子视图

将折线部分封装成不依赖动态状态的独立子视图,利用SwiftUI的视图依赖追踪特性,确保只有当折线数据变化时才重绘该子视图,车辆位置更新不会影响它。

步骤1:创建折线子视图

@available(iOS 17, *)
struct StaticPolylineLayer: View {
    let coordinates: [[CLLocationCoordinate2D]]
    let strokeColor: Color
    
    var body: some View {
        MapContent {
            ForEach(coordinates, id: \.self) { coords in
                MapPolyline(coordinates: coords)
                    .stroke(strokeColor, lineWidth: 3)
            }
        }
    }
}

步骤2:改造主视图

在主Map中引入该子视图,确保折线数据仅在onAppear时加载一次,且不与动态状态绑定:

@available(iOS 17, *)
struct LineMapViewWithPolylines: View {
    @State private var pathsCoordinates: [[CLLocationCoordinate2D]] = []
    
    @Binding var mapRegion: RegionWrapper
    @Binding var services: [Service]
    @Binding var selectedService: Service?
    @Binding var userPositionBtnAppearance: String
    @Binding var showsUserLocation: Bool
    
    var line: Line
    
    var body: some View {
        Map(position: .constant(MapCameraPosition.region(mapRegion._region)), interactionModes: [.pan, .zoom]) {
            if showsUserLocation {
                UserAnnotation()
            }
            
            // 动态车辆标注
            ForEach(services) { service in
                Annotation("", coordinate: service.coordinates) {
                    Button {
                        withAnimation(.easeInOut(duration: 0.3)) {
                            selectedService = service
                        }
                        withAnimation(.spring(response: 0.5, dampingFraction: 1.0, blendDuration: 0.5)) {
                            self.mapRegion = RegionWrapper(latitude: service.latitude - (UIDevice.current.userInterfaceIdiom == .pad ? 0 : 0.015), longitude: service.longitude, latitudeDelta: 0.06, longitudeDelta: 0.06)
                        }
                    } label: {
                        VStack {
                            if(line.lineName == "Tram A" || line.lineName == "Tram B" || line.lineName == "Tram C" || line.lineName == "Tram D") {
                                TramIcon(orientation: service.orientation)
                            }
                            else if(line.lineName == "BatCUB") {
                                FerryIcon(orientation: service.orientation)
                            }
                            else {
                                BusIcon(orientation: service.orientation)
                            }
                            
                            Text(service.vehicle.parkId)
                                .foregroundColor(Color("ReversedDarkMode"))
                                .fontWeight(.bold)
                                .font(.system(size: 14))
                                .offset(y: service.orientation == nil ? -48 : -60)
                                .scaledToFill()
                        }
                    }
                }
            }
            
            // 静态折线层
            StaticPolylineLayer(coordinates: pathsCoordinates, strokeColor: Color(line.lineType))
        }
        .toolbarBackground(.hidden, for: .navigationBar)
        .gesture(
            DragGesture()
                .onChanged { _ in
                    userPositionBtnAppearance = "location"
                }
        )
        .onAppear {
            LinePaths.getOrderedPathsByLine(lineId: line.id, withCoordinates: true) { paths in
                paths.forEach { backAndForthPaths in
                    backAndForthPaths.forEach { path in
                        pathsCoordinates.append(contentsOf: path.coordinates.map { coordinates in
                            coordinates.map { CLLocationCoordinate2D(latitude: $0[1], longitude: $0[0]) }
                        })
                    }
                }
            }
        }
    }
}

方案2:改用UIKit MKMapView封装(更彻底的性能控制)

如果方案1仍有性能问题,可以直接封装UIKit的MKMapView,手动控制折线和标注的更新时机,完全避免SwiftUI自动重绘带来的开销。

实现自定义Map视图

@available(iOS 17, *)
struct CustomBusMapView: UIViewRepresentable {
    @Binding var services: [Service]
    @Binding var mapRegion: RegionWrapper
    @Binding var showsUserLocation: Bool
    @Binding var selectedService: Service?
    
    let pathsCoordinates: [[CLLocationCoordinate2D]]
    let lineColor: UIColor
    let line: Line
    
    // 自定义Annotation类,关联Service数据
    class VehicleAnnotation: MKPointAnnotation {
        var service: Service
        init(service: Service) {
            self.service = service
            super.init()
            self.coordinate = service.coordinates
        }
        
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    }
    
    func makeUIView(context: Context) -> MKMapView {
        let mapView = MKMapView()
        mapView.delegate = context.coordinator
        mapView.showsUserLocation = showsUserLocation
        // 初始化添加折线
        addPolylines(to: mapView)
        // 设置初始区域
        mapView.setRegion(mapRegion._region, animated: false)
        return mapView
    }
    
    func updateUIView(_ uiView: MKMapView, context: Context) {
        // 更新用户位置显示状态
        uiView.showsUserLocation = showsUserLocation
        // 更新地图区域(如果需要)
        if !uiView.region.isEqual(mapRegion._region) {
            uiView.setRegion(mapRegion._region, animated: true)
        }
        // 更新车辆标注
        updateVehicleAnnotations(in: uiView)
    }
    
    // 添加折线(仅初始化时执行)
    private func addPolylines(to mapView: MKMapView) {
        pathsCoordinates.forEach { coords in
            let polyline = MKPolyline(coordinates: coords, count: coords.count)
            mapView.addOverlay(polyline)
        }
    }
    
    // 更新车辆标注(仅当services变化时执行)
    private func updateVehicleAnnotations(in mapView: MKMapView) {
        // 移除旧的车辆标注
        let oldAnnotations = mapView.annotations.filter { $0 is VehicleAnnotation }
        mapView.removeAnnotations(oldAnnotations)
        
        // 添加新的车辆标注
        services.forEach { service in
            let annotation = VehicleAnnotation(service: service)
            mapView.addAnnotation(annotation)
        }
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    class Coordinator: NSObject, MKMapViewDelegate {
        var parent: CustomBusMapView
        
        init(_ parent: CustomBusMapView) {
            self.parent = parent
        }
        
        // 渲染折线
        func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
            guard let polyline = overlay as? MKPolyline else {
                return MKOverlayRenderer(overlay: overlay)
            }
            let renderer = MKPolylineRenderer(polyline: polyline)
            renderer.strokeColor = parent.lineColor
            renderer.lineWidth = 3
            return renderer
        }
        
        // 自定义车辆标注视图
        func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
            guard let vehicleAnnotation = annotation as? VehicleAnnotation else {
                // 返回用户位置默认视图
                return nil
            }
            
            let reuseId = "VehicleAnnotation"
            var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseId) as? MKAnnotationView
            
            if annotationView == nil {
                annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
                annotationView?.canShowCallout = false
                annotationView?.isUserInteractionEnabled = true
                // 添加点击手势
                let tapGesture = UITapGestureRecognizer(target: self, action: #selector(onAnnotationTapped(_:)))
                annotationView?.addGestureRecognizer(tapGesture)
            } else {
                annotationView?.annotation = annotation
            }
            
            // 设置对应图标
            let service = vehicleAnnotation.service
            var iconImage: UIImage?
            if parent.line.lineName == "Tram A" || parent.line.lineName == "Tram B" || parent.line.lineName == "Tram C" || parent.line.lineName == "Tram D" {
                // 转换SwiftUI的TramIcon为UIImage
                let icon = TramIcon(orientation: service.orientation)
                iconImage = icon.asUIImage()
            } else if parent.line.lineName == "BatCUB" {
                let icon = FerryIcon(orientation: service.orientation)
                iconImage = icon.asUIImage()
            } else {
                let icon = BusIcon(orientation: service.orientation)
                iconImage = icon.asUIImage()
            }
            annotationView?.image = iconImage
            
            // 添加车辆ID标签
            let label = UILabel()
            label.text = service.vehicle.parkId
            label.textColor = UIColor(named: "ReversedDarkMode")
            label.font = UIFont.systemFont(ofSize: 14, weight: .bold)
            label.frame = CGRect(x: 0, y: service.orientation == nil ? -48 : -60, width: 40, height: 20)
            annotationView?.addSubview(label)
            
            return annotationView
        }
        
        // 处理标注点击事件
        @objc private func onAnnotationTapped(_ gesture: UITapGestureRecognizer) {
            guard let annotationView = gesture.view as? MKAnnotationView,
                  let vehicleAnnotation = annotationView.annotation as? VehicleAnnotation else {
                return
            }
            let service = vehicleAnnotation.service
            // 触发选中逻辑
            withAnimation(.easeInOut(duration: 0.3)) {
                parent.selectedService = service
            }
            withAnimation(.spring(response: 0.5, dampingFraction: 1.0, blendDuration: 0.5)) {
                parent.mapRegion = RegionWrapper(latitude: service.latitude - (UIDevice.current.userInterfaceIdiom == .pad ? 0 : 0.015), longitude: service.longitude, latitudeDelta: 0.06, longitudeDelta: 0.06)
            }
        }
    }
}

// 辅助方法:将SwiftUI View转换为UIImage
extension View {
    func asUIImage() -> UIImage {
        let controller = UIHostingController(rootView: self)
        controller.view.frame = CGRect(x: 0, y: 0, width: 40, height: 40)
        let image = controller.view.asUIImage()
        return image
    }
}

extension UIView {
    func asUIImage() -> UIImage {
        let renderer = UIGraphicsImageRenderer(bounds: bounds)
        return renderer.image { rendererContext in
            layer.render(in: rendererContext.cgContext)
        }
    }
}

在主视图中使用

@available(iOS 17, *)
struct LineMapViewWithPolylines: View {
    @State private var pathsCoordinates: [[CLLocationCoordinate2D]] = []
    
    @Binding var mapRegion: RegionWrapper
    @Binding var services: [Service]
    @Binding var selectedService: Service?
    @Binding var userPositionBtnAppearance: String
    @Binding var showsUserLocation: Bool
    
    var line: Line
    
    var body: some View {
        CustomBusMapView(
            services: $services,
            mapRegion: $mapRegion,
            showsUserLocation: $showsUserLocation,
            selectedService: $selectedService,
            pathsCoordinates: pathsCoordinates,
            lineColor: UIColor(named: line.lineType) ?? .blue,
            line: line
        )
        .toolbarBackground(.hidden, for: .navigationBar)
        .gesture(
            DragGesture()
                .onChanged { _ in
                    userPositionBtnAppearance = "location"
                }
        )
        .onAppear {
            LinePaths.getOrderedPathsByLine(lineId: line.id, withCoordinates: true) { paths in
                paths.forEach { backAndForthPaths in
                    backAndForthPaths.forEach { path in
                        pathsCoordinates.append(contentsOf: path.coordinates.map { coordinates in
                            coordinates.map { CLLocationCoordinate2D(latitude: $0[1], longitude: $0[0]) }
                        })
                    }
                }
            }
        }
    }
}

额外优化建议

  • 确保pathsCoordinates的ForEach使用稳定的id:如果数组元素是固定的,尽量避免用id: \.self,可以给每个折线路径分配唯一标识符(比如从后端获取的路径ID),减少SwiftUI的视图识别开销。
  • 折线数据预处理:如果路径坐标点过多,可以考虑抽稀(移除冗余坐标),减少渲染压力。
  • 使用@StateObject管理折线数据:如果折线数据需要后期更新,用@StateObject封装数据模型,确保视图仅在数据真正变化时重绘。

内容的提问来源于stack exchange,提问作者Diplay

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 05:40:54