What's new in MapKit
Maps & Location 进阶 35m

MapKit 新特性

What's new in MapKit

2025年6月11日

在 Apple 官方观看视频

一句话判断

MapKit 终于从”给你一个地图,自己贴标签”进化成”你可以定义地图长什么样”,这对所有在应用里做品牌化地图体验的团队来说是质变。

这场 Session 讲了什么

MapKit 今年最大的变化不是某个单一功能,而是定位的转变。Apple 把 MapKit 从一个”展示系统地图+标注”的工具,升级成一个可以深度定制视觉语言的平台。自定义地图样式(Map Style)通过 JSON 配置就能改道路颜色、水域外观、建筑物高度,品牌化地图体验不再是 Google Maps 的专属能力。

声明式语法 MapContentBuilder 是另一个大动作。它用 Result Builder 的方式让你描述”地图上应该有什么”,而不是手动管理 MKAnnotationView 的生命周期。这和 SwiftUI 的设计哲学一脉相承——你描述状态,框架负责渲染差异。对于已经部分采用 SwiftUI 的项目,这是一个很强的迁移推力。

还有一个容易被忽略但实用价值很高的变化:MapItemDetail。过去用户在你的应用里点一个地点,要么看一个信息量极少的弹窗,要么被跳转到系统地图打断上下文。现在你可以在应用内直接展示完整的地点详情——评分、营业时间、照片、评论全都有。

值得深挖的点

MapContentBuilder 的真正意义

表面上看,MapContentBuilder 只是把命令式 API 包了一层声明式语法。但实际影响比这大得多。旧版 MapKit 里,你需要自己管理标注的添加、移除、复用、聚类,代码散落在 delegate 回调里,调试时要在三个方法之间反复跳转。一个标注的生命周期管理代码可能比业务逻辑还多。

MapContentBuilder 把这些全吃掉了。你用 Annotation、MapPolyline 这些声明式组件描述地图内容,框架自动做差异比较和视图更新。这不只是语法糖——它改变了你组织地图相关代码的方式。状态驱动渲染意味着你可以把地图当成一个普通的 SwiftUI 视图来测试和推理,而不是一个有自己生命周期的特殊野兽。

代价也很明确:它只在 SwiftUI 里可用。如果你的项目是纯 UIKit,你享受不到这个好处。而且对于需要高度定制标注外观的场景(比如自定义 callout 交互),声明式 API 的灵活性可能不如直接操作 MKAnnotationView。这是一个 trade-off:开发效率换定制自由度。大多数应用应该选前者。

自定义地图样式的商业逻辑

为什么这件事重要?因为在电商、外卖、旅游场景里,地图是产品体验的一部分,不是一个独立的功能模块。用户打开美团看到的是美团风格的地图,打开高德看到的是高德风格的地图——品牌一致性直接影响用户对你产品质量的感知。

过去用 MapKit 做到这一点几乎不可能,所有应用的系统地图看起来都一样。现在一个 JSON 文件就能改配色、隐藏无关 POI、调整建筑物渲染。JSON 配置方式借鉴了 Google Maps 的 Styling Wizard,但 Apple 的实现更注重隐私——所有样式处理都在本地完成。

需要注意的是,JSON 配置格式错误不会报错,会静默回退到默认样式。这意味着你在开发时可能发现不了问题,直到某个特定配置组合导致地图突然变成”原版”。建议用单元测试校验 JSON 结构的完整性。

代码片段

声明式构建地图内容

场景:一个门店地图页面,展示多个门店和导航路线。

struct StoreMapView: View {
    @State private var position: MapCameraPosition = .automatic

    var body: Map(position: $position) {
        Annotation("Apple Park", coordinate: .applePark) {
            Image(systemName: "building.2.fill")
                .foregroundStyle(.white)
                .padding(8)
                .background(.blue, in: RoundedRectangle(cornerRadius: 8))
        }

        MapPolyline(coordinates: routeCoordinates)
            .stroke(.blue, lineWidth: 4)

        UserAnnotation()
    }
    .mapStyle(.standard(
        elevation: .realistic,
        pointsOfInterest: .including([.restaurant, .cafe])
    ))
}

坑:.including() 过滤的是系统 POI 类别,你自己的自定义标注不受影响,但别忘了过滤掉不相关的系统 POI,否则地图会很吵。

地图相机动画

场景:旅游导览应用,用户点击景点后地图平滑飞入。

struct TourGuideView: View {
    @State private var cameraPosition: MapCameraPosition = .region(
        MKCoordinateRegion(
            center: CLLocationCoordinate2D(latitude: 39.9042, longitude: 116.4074),
            span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
        )
    )

    var body: some View {
        Map(position: $cameraPosition)
            .onAppear {
                withAnimation(.easeInOut(duration: 2.0)) {
                    cameraPosition = .camera(MapCamera(
                        centerCoordinate: .tiananmen,
                        distance: 500,
                        heading: 45,
                        pitch: 60
                    ))
                }
            }
    }
}

坑:pitch > 0 需要 .realistic 高程支持,否则 3D 效果不生效,地图会平得像纸。

增强的本地搜索

场景:外卖应用搜索附近餐厅,按类别和距离过滤。

func searchNearbyRestaurants() async throws -> [MKMapItem] {
    let request = MKLocalSearch.Request()
    request.naturalLanguageQuery = "咖啡"
    request.pointOfInterestFilter = MKPointOfInterestFilter(including: [.cafe])
    request.region = MKCoordinateRegion(
        center: .currentLocation,
        span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
    )

    let search = MKLocalSearch(request: request)
    let response = try await search.start()
    return response.mapItems
}

坑:LocalSearch 有频率限制,别在 ScrollView 的 onChange 里每次滑动都触发请求,否则会被限流。

最佳实践

先把现有的 MKMapView 包装成一个 SwiftUI View,用 UIViewRepresentable 桥接。这一步不需要等 MapKit 更新,现在就能做,而且为后续迁移铺路。

接下来,新页面直接用 MapContentBuilder。不要尝试在旧代码上改造——声明式和命令式混在一起会变成噩梦。新页面用新 API,旧页面保持不动,逐步替换。

自定义地图样式建议先做一个暗色主题的 JSON 配置,和应用的暗色模式联动。这个投入产出比最高——用户对暗色模式的感知很强烈,而且 JSON 配置一次做好就不需要再动。

MapItemDetail 优先用在外卖和旅游类应用里。房产应用也值得做,但优先级低一些——用户看房时更关心价格和户型,不是地图上的详情卡片。

最后,搜索结果一定要做本地缓存。LocalSearch 的频率限制是真实的,用户在地图上滑来滑去时不可能每次都发网络请求。把结果缓存 5-10 分钟,按 region 做 key。

还有什么值得关注

  • MapCamera 动画不要在短时间内连续触发,否则会导致动画堆叠和卡顿,需要自己做节流。
  • MapItemDetail 需要设备有地图数据,模拟器上的功能可能受限,真机测试是必须的。
  • 自定义样式的 JSON 放在 Bundle 里按环境区分(开发/生产),别把调试用的夸张配色带到线上。
MapKit 地图 SwiftUI 导航 定位