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

Swift实现带标注自定义地图:能否复刻DayZDB地图功能?

如何用Swift实现带标注的自定义地图

嘿,这个需求我刚好有实践经验,给你梳理两种靠谱的实现方案,完全能达到你想要的效果:


方案一:用UIScrollView实现自定义静态地图(最贴合你说的示例效果)

这个方案直接基于UIScrollView搭建,完美支持加载本地图片、捏合缩放、在指定位置放置标注,和你提到的示例功能几乎一致,实现起来也最简单。

核心步骤:

  1. 搭建缩放基础:把地图图片放到UIImageView里,再嵌套进UIScrollView,设置缩放范围,实现UIScrollViewDelegate的缩放代理方法。
  2. 添加标注/自定义视图:根据原始图片的坐标计算标注在滚动视图中的位置,把标注添加到合适的容器里(随图片缩放或固定大小)。

核心代码示例:

import UIKit

class CustomMapVC: UIViewController, UIScrollViewDelegate {
    private let scrollView = UIScrollView()
    private let mapImageView = UIImageView(image: UIImage(named: "your_custom_map"))
    // 原始图片上的标注位置(比如图片宽高1000x800,这里的坐标是基于原图的)
    private let annotationPoints = [CGPoint(x: 200, y: 300), CGPoint(x: 500, y: 600)]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupScrollView()
        addMapAnnotations()
    }
    
    private func setupScrollView() {
        view.addSubview(scrollView)
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            scrollView.topAnchor.constraint(equalTo: view.topAnchor),
            scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
        
        // 配置缩放参数
        scrollView.delegate = self
        scrollView.minimumZoomScale = 1.0
        scrollView.maximumZoomScale = 5.0
        scrollView.showsScrollIndicators = false
        
        // 添加地图图片
        scrollView.addSubview(mapImageView)
        mapImageView.translatesAutoresizingMaskIntoConstraints = false
        guard let mapSize = mapImageView.image?.size else { return }
        NSLayoutConstraint.activate([
            mapImageView.topAnchor.constraint(equalTo: scrollView.topAnchor),
            mapImageView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
            mapImageView.widthAnchor.constraint(equalToConstant: mapSize.width),
            mapImageView.heightAnchor.constraint(equalToConstant: mapSize.height)
        ])
    }
    
    private func addMapAnnotations() {
        guard let mapSize = mapImageView.image?.size else { return }
        
        for point in annotationPoints {
            // 自定义标注视图(这里用按钮示例,你可以换成任何UIView)
            let annotationView = UIButton(type: .custom)
            annotationView.setImage(UIImage(named: "annotation_icon"), for: .normal)
            annotationView.frame.size = CGSize(width: 32, height: 32)
            
            // 计算标注在图片上的位置(居中对齐)
            annotationView.frame.origin = CGPoint(
                x: point.x - annotationView.frame.width/2,
                y: point.y - annotationView.frame.height/2
            )
            
            // 👉 如果要让标注随图片一起缩放,就添加到mapImageView上
            mapImageView.addSubview(annotationView)
            // 👉 如果要让标注固定大小不缩放,就添加到scrollView或view上,然后在缩放时更新位置(下面有示例)
            // scrollView.addSubview(annotationView)
            // annotationView.tag = 100 // 给标注打标签方便后续识别
        }
    }
    
    // MARK: - UIScrollViewDelegate
    func viewForZooming(in scrollView: UIScrollView) -> UIView? {
        return mapImageView
    }
    
    // 👉 如果标注固定大小,需要实现这个方法更新位置
    /*
    func scrollViewDidZoom(_ scrollView: UIScrollView) {
        updateFixedSizeAnnotations()
    }
    
    private func updateFixedSizeAnnotations() {
        let zoomScale = scrollView.zoomScale
        let contentOffset = scrollView.contentOffset
        
        for subview in scrollView.subviews where subview.tag == 100 {
            guard let index = annotationPoints.firstIndex(where: { 
                // 匹配标注对应的原始坐标,这里可以用tag关联索引,更高效
                abs($0.x - (subview.frame.origin.x + subview.frame.width/2)/zoomScale) < 1 &&
                abs($0.y - (subview.frame.origin.y + subview.frame.height/2)/zoomScale) < 1
            }) else { continue }
            
            let originalPoint = annotationPoints[index]
            // 转换原始坐标到当前缩放后的scrollView坐标
            let newX = originalPoint.x * zoomScale - subview.frame.width/2 + contentOffset.x
            let newY = originalPoint.y * zoomScale - subview.frame.height/2 + contentOffset.y
            subview.frame.origin = CGPoint(x: newX, y: newY)
        }
    }
    */
}

方案二:用MapKit实现自定义地图(适合带地理坐标的场景)

如果你需要结合真实地理数据,或者地图是由多块瓦片拼接而成,MapKit会是更合适的选择。它自带成熟的地图交互(缩放、平移),还能轻松实现标注弹窗、拖拽等扩展功能。

核心步骤:

  1. 加载自定义底图:通过MKOverlay把自定义图片添加到MapKit中,设置对应的地理范围。
  2. 添加标注:用MKPointAnnotation定义标注的地理坐标,自定义MKAnnotationView展示样式。

核心代码示例:

import UIKit
import MapKit

class MapKitCustomMapVC: UIViewController, MKMapViewDelegate {
    private let mapView = MKMapView()
    // 自定义地图的地理范围(这里用MKMapRect模拟,你可以换成真实的经纬度范围)
    private let customMapBounds = MKMapRect(x: 0, y: 0, width: 1000, height: 800)
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupMapView()
        addCustomMapOverlay()
        addMapAnnotations()
    }
    
    private func setupMapView() {
        view.addSubview(mapView)
        mapView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            mapView.topAnchor.constraint(equalTo: view.topAnchor),
            mapView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            mapView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            mapView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
        mapView.delegate = self
        // 设置地图显示范围为自定义底图的范围
        mapView.setRegion(MKCoordinateRegion(customMapBounds), animated: false)
    }
    
    private func addCustomMapOverlay() {
        guard let mapImage = UIImage(named: "your_custom_map") else { return }
        // 创建自定义图片Overlay
        let imageOverlay = MKImageOverlay(image: mapImage, mapRect: customMapBounds)
        mapView.addOverlay(imageOverlay)
    }
    
    private func addMapAnnotations() {
        // 把原始图片坐标转换为地理坐标
        let annotation1 = MKPointAnnotation()
        annotation1.coordinate = customMapBounds.coordinate(at: CGPoint(x: 200, y: 300))
        annotation1.title = "资源点1"
        
        let annotation2 = MKPointAnnotation()
        annotation2.coordinate = customMapBounds.coordinate(at: CGPoint(x: 500, y: 600))
        annotation2.title = "资源点2"
        
        mapView.addAnnotations([annotation1, annotation2])
    }
    
    // MARK: - MKMapViewDelegate
    func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
        if let imageOverlay = overlay as? MKImageOverlay {
            return MKImageOverlayRenderer(overlay: imageOverlay)
        }
        return MKOverlayRenderer(overlay: overlay)
    }
    
    func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
        guard annotation is MKPointAnnotation else { return nil }
        let reuseID = "CustomAnnotation"
        var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseID) as? MKPinAnnotationView
        if annotationView == nil {
            annotationView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: reuseID)
            annotationView?.canShowCallout = true // 点击显示弹窗
            annotationView?.pinTintColor = .red
        } else {
            annotationView?.annotation = annotation
        }
        return annotationView
    }
}

// 扩展MKMapRect,方便图片坐标转地理坐标
extension MKMapRect {
    func coordinate(at point: CGPoint) -> CLLocationCoordinate2D {
        let longitude = origin.longitude + (point.x / width) * span.longitudeDelta
        let latitude = origin.latitude - (point.y / height) * span.latitudeDelta
        return CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
    }
    
    var span: MKCoordinateSpan {
        let topLeft = origin.coordinate
        let bottomRight = MKMapPoint(x: origin.x + width, y: origin.y + height).coordinate
        return MKCoordinateSpan(
            latitudeDelta: topLeft.latitude - bottomRight.latitude,
            longitudeDelta: bottomRight.longitude - topLeft.longitude
        )
    }
}

方案选择建议

  • 如果只是单张大图的静态地图,优先选UIScrollView方案,代码简单,性能好,完全满足你要的捏合缩放、标注功能。
  • 如果需要结合地理数据、瓦片地图,或者要利用MapKit的成熟交互(比如标注拖拽、路线规划),选MapKit方案更合适。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.21 04:33:25