重要前提
安装AI Skills的关键前提是:必须科学上网,且开启TUN模式,这一点至关重要,直接决定安装能否顺利完成,在此郑重提醒三遍:科学上网,科学上网,科学上网。查看完整安装教程 →
axiom-mapkit-ref by charleswiltgen/axiom
npx skills add https://github.com/charleswiltgen/axiom --skill axiom-mapkit-ref适用于 iOS 开发的完整 MapKit API 参考。涵盖 SwiftUI Map (iOS 17+) 和 MKMapView (UIKit)。
axiom-mapkit — 决策树、反模式、压力场景axiom-mapkit-diag — 基于症状的故障排除| 功能 | SwiftUI Map (iOS 17+) | MKMapView |
|---|---|---|
| 声明 | Map(position:) { content } | MKMapView() |
| 相机控制 | MapCameraPosition 绑定 |
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
setRegion(_:animated:) |
| 标注 | content 中的 Marker, Annotation | addAnnotation(_:) + 委托 |
| 覆盖层 | MapCircle, MapPolyline, MapPolygon | addOverlay(_:) + 渲染器委托 |
| 用户位置 | UserAnnotation() | showsUserLocation = true |
| 选择 | .mapSelection($selection) | 委托 didSelect |
| 控件 | .mapControls { } | showsCompass, showsScale |
| 交互模式 | .mapInteractionModes([]) | 委托方法 |
| 聚类 | 通过 .mapItemClusteringIdentifier 内置 | MKClusterAnnotation |
@State private var cameraPosition: MapCameraPosition = .automatic
Map(position: $cameraPosition) {
Marker("Home", coordinate: homeCoord)
Annotation("Custom", coordinate: coord) {
Image(systemName: "star.fill")
.foregroundStyle(.yellow)
.padding(4)
.background(.blue, in: Circle())
}
UserAnnotation()
MapCircle(center: coord, radius: 500)
.foregroundStyle(.blue.opacity(0.3))
MapPolyline(coordinates: routeCoords)
.stroke(.blue, lineWidth: 3)
}
.mapStyle(.standard(elevation: .realistic))
.mapControls {
MapUserLocationButton()
MapCompass()
MapScaleView()
}
控制相机位置:
// 系统管理相机以显示所有内容
.automatic
// 特定区域
.region(MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194),
span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
))
// 具有俯仰角和朝向的特定相机
.camera(MapCamera(
centerCoordinate: coordinate,
distance: 1000, // 距中心的米数
heading: 90, // 偏离北方的度数
pitch: 60 // 偏离垂直方向的度数 (0 = 俯视)
))
// 跟随用户位置
.userLocation(followsHeading: true, fallback: .automatic)
// 显示特定项目
.item(mapItem)
// 显示特定矩形区域
.rect(MKMapRect(...))
// 动画过渡到新位置
withAnimation {
cameraPosition = .region(newRegion)
}
// 关键帧动画 (iOS 17+)
Map(position: $cameraPosition)
.mapCameraKeyframeAnimator(trigger: flyToTrigger) { initialCamera in
KeyframeTrack(\.centerCoordinate) {
LinearKeyframe(destination, duration: 2.0)
}
KeyframeTrack(\.distance) {
CubicKeyframe(5000, duration: 1.0)
CubicKeyframe(1000, duration: 1.0)
}
}
@State private var selectedItem: MKMapItem?
Map(position: $cameraPosition, selection: $selectedItem) {
ForEach(mapItems, id: \.self) { item in
Marker(item: item)
}
}
.onChange(of: selectedItem) { _, newItem in
if let newItem {
// 处理选择
}
}
Map(position: $cameraPosition) { ... }
.onMapCameraChange { context in
// context.region — 可见的 MKCoordinateRegion
// context.camera — 当前的 MapCamera
// context.rect — 可见的 MKMapRect
fetchAnnotations(in: context.region)
}
.onMapCameraChange(frequency: .continuous) { context in
// 在交互过程中调用(不仅仅是结束时)
}
.mapStyle(.standard) // 默认
.mapStyle(.standard(elevation: .realistic)) // 3D 建筑
.mapStyle(.standard(emphasis: .muted)) // 柔和色彩
.mapStyle(.standard(pointsOfInterest: .including([.restaurant, .cafe])))
.mapStyle(.imagery) // 卫星
.mapStyle(.imagery(elevation: .realistic)) // 3D 卫星
.mapStyle(.hybrid) // 卫星 + 标签
.mapStyle(.hybrid(elevation: .realistic)) // 3D 混合
// 允许所有交互(默认)
.mapInteractionModes(.all)
// 只读地图(无交互)
.mapInteractionModes([])
// 仅平移,无缩放
.mapInteractionModes([.pan])
// 平移和缩放,无旋转/俯仰
.mapInteractionModes([.pan, .zoom])
系统样式的带标注气泡的地图标记:
// 基础标记
Marker("Coffee Shop", coordinate: coord)
// 带系统图像
Marker("Coffee Shop", systemImage: "cup.and.saucer.fill", coordinate: coord)
// 带字母组合(最多 2 个字符)
Marker("Coffee Shop", monogram: Text("CS"), coordinate: coord)
// 颜色
Marker("Coffee Shop", coordinate: coord)
.tint(.brown)
// 从 MKMapItem 创建
Marker(item: mapItem)
在坐标处完全自定义的视图:
Annotation("Custom Pin", coordinate: coord) {
VStack {
Image(systemName: "mappin.circle.fill")
.font(.title)
.foregroundStyle(.red)
Text("Here")
.font(.caption)
}
}
// 锚点(默认为底部中心)
Annotation("Pin", coordinate: coord, anchor: .center) {
Circle()
.fill(.blue)
.frame(width: 20, height: 20)
}
当前用户位置指示器:
UserAnnotation()
// 自定义外观
UserAnnotation(anchor: .center) {
Image(systemName: "location.circle.fill")
.foregroundStyle(.blue)
}
// 圆形
MapCircle(center: coord, radius: 1000) // 半径(米)
.foregroundStyle(.blue.opacity(0.2))
.stroke(.blue, lineWidth: 2)
// 多边形
MapPolygon(coordinates: polygonCoords)
.foregroundStyle(.green.opacity(0.3))
.stroke(.green, lineWidth: 2)
// 折线
MapPolyline(coordinates: routeCoords)
.stroke(.blue, lineWidth: 4)
// 从 MKRoute 创建
MapPolyline(route.polyline)
.stroke(.blue, lineWidth: 5)
ForEach(locations) { location in
Marker(location.name, coordinate: location.coordinate)
.tag(location.id)
}
.mapItemClusteringIdentifier("locations")
struct MapViewWrapper: UIViewRepresentable {
@Binding var region: MKCoordinateRegion
let annotations: [MKAnnotation]
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.delegate = context.coordinator
mapView.showsUserLocation = true
mapView.register(
MKMarkerAnnotationView.self,
forAnnotationViewWithReuseIdentifier: "marker"
)
return mapView
}
func updateUIView(_ mapView: MKMapView, context: Context) {
// 防止无限循环
if !regionsAreEqual(mapView.region, region) {
mapView.setRegion(region, animated: true)
}
// 差异更新标注,而不是移除所有
let current = Set(mapView.annotations.compactMap { $0 as? MyAnnotation })
let desired = Set(annotations.compactMap { $0 as? MyAnnotation })
let toAdd = desired.subtracting(current)
let toRemove = current.subtracting(desired)
mapView.addAnnotations(Array(toAdd))
mapView.removeAnnotations(Array(toRemove))
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
static func dismantleUIView(_ mapView: MKMapView, coordinator: Coordinator) {
mapView.removeAnnotations(mapView.annotations)
mapView.removeOverlays(mapView.overlays)
}
}
class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapViewWrapper
init(_ parent: MapViewWrapper) {
self.parent = parent
}
// 标注视图自定义
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard !(annotation is MKUserLocation) else { return nil } // 用户位置使用默认视图
let view = mapView.dequeueReusableAnnotationView(
withIdentifier: "marker",
for: annotation
) as! MKMarkerAnnotationView
view.markerTintColor = .systemRed
view.glyphImage = UIImage(systemName: "mappin")
view.clusteringIdentifier = "poi"
view.canShowCallout = true
return view
}
// 覆盖层渲染
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if let circle = overlay as? MKCircle {
let renderer = MKCircleRenderer(circle: circle)
renderer.fillColor = UIColor.systemBlue.withAlphaComponent(0.2)
renderer.strokeColor = .systemBlue
renderer.lineWidth = 2
return renderer
}
if let polyline = overlay as? MKPolyline {
let renderer = MKPolylineRenderer(polyline: polyline)
renderer.strokeColor = .systemBlue
renderer.lineWidth = 4
return renderer
}
if let polygon = overlay as? MKPolygon {
let renderer = MKPolygonRenderer(polygon: polygon)
renderer.fillColor = UIColor.systemGreen.withAlphaComponent(0.3)
renderer.strokeColor = .systemGreen
renderer.lineWidth = 2
return renderer
}
return MKOverlayRenderer(overlay: overlay)
}
// 区域更改跟踪
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
parent.region = mapView.region
}
// 标注选择
func mapView(_ mapView: MKMapView, didSelect annotation: MKAnnotation) {
// 处理点击
}
// 聚类标注
func mapView(
_ mapView: MKMapView,
clusterAnnotationForMemberAnnotations memberAnnotations: [MKAnnotation]
) -> MKClusterAnnotation {
MKClusterAnnotation(memberAnnotations: memberAnnotations)
}
}
带符号的气球形状标记:
let view = MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "marker")
view.markerTintColor = .systemPurple
view.glyphImage = UIImage(systemName: "star.fill")
view.glyphText = "A" // 文本符号(覆盖图像)
view.displayPriority = .required // 始终可见
view.clusteringIdentifier = "category" // 启用聚类
view.canShowCallout = true
view.titleVisibility = .adaptive // 根据空间显示标题
view.subtitleVisibility = .hidden
完全自定义的标注视图:
let view = MKAnnotationView(annotation: annotation, reuseIdentifier: "custom")
view.image = UIImage(named: "custom-pin")
view.centerOffset = CGPoint(x: 0, y: -view.image!.size.height / 2)
view.canShowCallout = true
view.leftCalloutAccessoryView = UIImageView(image: thumbnail)
view.rightCalloutAccessoryView = UIButton(type: .detailDisclosure)
func mapView(
_ mapView: MKMapView,
annotationView view: MKAnnotationView,
calloutAccessoryControlTapped control: UIControl
) {
guard let annotation = view.annotation as? MyAnnotation else { return }
// 导航到详情视图
}
始终使用 dequeueReusableAnnotationView(withIdentifier:for:):
// 在 makeUIView 中注册(一次)
mapView.register(MKMarkerAnnotationView.self, forAnnotationViewWithReuseIdentifier: "marker")
// 在委托中出队(每次)
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
let view = mapView.dequeueReusableAnnotationView(withIdentifier: "marker", for: annotation)
// 配置...
return view
}
无重用:1000 个标注 = 内存中 1000 个视图。有重用:用户滚动时约 20-30 个视图被回收利用。
let completer = MKLocalSearchCompleter()
completer.delegate = self
completer.resultTypes = [.pointOfInterest, .address]
completer.region = visibleMapRegion // 将结果偏向可见区域
// 每次按键时更新
completer.queryFragment = "coffee"
// 委托接收结果
func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
let results = completer.results // [MKLocalSearchCompletion]
for result in results {
// result.title — "Starbucks"
// result.subtitle — "123 Main St, San Francisco, CA"
// result.titleHighlightRanges — 匹配查询的范围
}
}
func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
// 网络错误、速率限制等
}
// 从自动补全结果创建
let request = MKLocalSearch.Request(completion: selectedCompletion)
// 从自然语言创建
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = "coffee shops"
request.region = mapRegion // 结果偏向
request.resultTypes = .pointOfInterest // 过滤类型
request.pointOfInterestFilter = MKPointOfInterestFilter(
including: [.cafe, .restaurant]
)
let search = MKLocalSearch(request: request)
let response = try await search.start()
for item in response.mapItems {
// item.name — "Starbucks"
// item.placemark — 包含地址的 MKPlacemark
// item.placemark.coordinate — CLLocationCoordinate2D
// item.phoneNumber — 可选电话
// item.url — 可选网站
// item.pointOfInterestCategory — .cafe, .restaurant 等
}
// 过滤要返回的结果类型
request.resultTypes = .address // 仅街道地址
request.resultTypes = .pointOfInterest // 商业、地标
request.resultTypes = .physicalFeature // 山脉、湖泊、公园
request.resultTypes = .query // 建议的搜索查询 (iOS 18+)
request.resultTypes = [.pointOfInterest, .address] // 多种类型
MKLocalSearchCompleter 自行处理节流 — 每次按键调用是安全的MKLocalSearch — Apple 会进行速率限制;不要超过每秒约 1 次调用MKLocalSearchCompleter 实例 — 不要为每个查询创建新的let request = MKDirections.Request()
request.source = MKMapItem.forCurrentLocation()
request.destination = destinationMapItem
request.transportType = .automobile
request.requestsAlternateRoutes = true // 获取多条路线
let directions = MKDirections(request: request)
let response = try await directions.calculate()
for route in response.routes {
route.polyline // MKPolyline — 在地图上显示
route.expectedTravelTime // TimeInterval(秒)
route.distance // CLLocationDistance(米)
route.name // "I-280 S" — 路线名称
route.advisoryNotices // [String] — 警告
route.steps // [MKRoute.Step] — 逐向导航
}
.automobile // 驾车路线
.walking // 步行路线
.transit // 公共交通(在可用时)
.any // 所有模式
let directions = MKDirections(request: request)
let eta = try await directions.calculateETA()
eta.expectedTravelTime // TimeInterval
eta.distance // CLLocationDistance
eta.expectedArrivalDate // Date
eta.expectedDepartureDate // Date
eta.transportType // MKDirectionsTransportType
for step in route.steps {
step.instructions // "Turn right onto Main St"
step.distance // CLLocationDistance(米)
step.polyline // 此步骤段的 MKPolyline
step.transportType // 对于公交路线可能会改变
step.notice // 可选建议
}
let request = MKLookAroundSceneRequest(coordinate: coordinate)
do {
let scene = try await request.scene
// scene 非 nil — 此坐标处环视可用
} catch {
// 此处环视不可用
}
@State private var lookAroundScene: MKLookAroundScene?
LookAroundPreview(scene: $lookAroundScene)
.frame(height: 200)
// 加载场景
func loadLookAround(for coordinate: CLLocationCoordinate2D) async {
let request = MKLookAroundSceneRequest(coordinate: coordinate)
lookAroundScene = try? await request.scene
}
let controller = MKLookAroundViewController(scene: scene)
// 模态呈现或作为子视图控制器嵌入
let snapshotter = MKLookAroundSnapshotter(scene: scene, options: .init())
let snapshot = try await snapshotter.snapshot
let image = snapshot.image // UIImage
// 圆形
let circle = MKCircle(center: coordinate, radius: 1000)
mapView.addOverlay(circle)
// 多边形
let polygon = MKPolygon(coordinates: &coords, count: coords.count)
mapView.addOverlay(polygon)
// 折线
let polyline = MKPolyline(coordinates: &coords, count: coords.count)
mapView.addOverlay(polyline, level: .aboveRoads)
// 自定义瓦片覆盖层
let template = "https://tile.example.com/{z}/{x}/{y}.png"
let tileOverlay = MKTileOverlay(urlTemplate: template)
tileOverlay.canReplaceMapContent = true // 隐藏 Apple 地图基础图层
mapView.addOverlay(tileOverlay, level: .aboveLabels)
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
switch overlay {
case let circle as MKCircle:
let renderer = MKCircleRenderer(circle: circle)
renderer.fillColor = UIColor.systemBlue.withAlphaComponent(0.2)
renderer.strokeColor = .systemBlue
renderer.lineWidth = 2
return renderer
case let polyline as MKPolyline:
let renderer = MKPolylineRenderer(polyline: polyline)
renderer.strokeColor = .systemBlue
renderer.lineWidth = 4
return renderer
case let polygon as MKPolygon:
let renderer = MKPolygonRenderer(polygon: polygon)
renderer.fillColor = UIColor.systemGreen.withAlphaComponent(0.3)
renderer.strokeColor = .systemGreen
renderer.lineWidth = 2
return renderer
case let tile as MKTileOverlay:
return MKTileOverlayRenderer(tileOverlay: tile)
default:
return MKOverlayRenderer(overlay: overlay)
}
}
mapView.addOverlay(overlay, level: .aboveRoads) // 道路上方,标签下方
mapView.addOverlay(overlay, level: .aboveLabels) // 所有内容上方
let renderer = MKGradientPolylineRenderer(polyline: polyline)
renderer.setColors([.green, .yellow, .red], locations: [0.0, 0.5, 1.0])
renderer.lineWidth = 6
为分享、缩略图或离线显示生成静态地图图像:
let options = MKMapSnapshotter.Options()
options.region = MKCoordinateRegion(
center: coordinate,
span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
)
options.size = CGSize(width: 300, height: 200)
options.scale = UIScreen.main.scale // Retina 支持
options.mapType = .standard
options.showsBuildings = true
options.pointOfInterestFilter = .excludingAll // 简洁地图
let snapshotter = MKMapSnapshotter(options: options)
let snapshot = try await snapshotter.start()
let image = snapshot.image
// 在快照上绘制自定义标注
UIGraphicsBeginImageContextWithOptions(image.size, true, image.scale)
image.draw(at: .zero)
let pinImage = UIImage(systemName: "mappin.circle.fill")!
let point = snapshot.point(for: coordinate)
pinImage.draw(at: CGPoint(
x: point.x - pinImage.size.width / 2,
y: point.y - pinImage.size.height
))
let finalImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
// 将坐标转换为快照图像中的点
let point = snapshot.point(for: coordinate)
// 检查坐标是否在快照边界内
let isVisible = CGRect(origin: .zero, size: snapshot.image.size).contains(point)
| 功能 | iOS 版本 |
|---|---|
| MKMapView | 3.0+ |
| MKLocalSearch | 6.1+ |
| MKDirections | 7.0+ |
| MKMarkerAnnotationView | 11.0+ |
| MKMapSnapshotter | 7.0+ |
| MKLookAroundSceneRequest | 16.0+ |
| LookAroundPreview (SwiftUI) | 17.0+ |
| SwiftUI Map (内容构建器) | 17.0+ |
| MapCameraPosition | 17.0+ |
| .mapSelection | 17.0+ |
| .mapCameraKeyframeAnimator | 17.0+ |
| .onMapCameraChange | 17.0+ |
| MapUserLocationButton | 17.0+ |
| MapCompass | 17.0+ |
| MapScaleView | 17.0+ |
| .mapInteractionModes | 17.0+ |
| MKLocalSearch.ResultType.query | 18.0+ |
| GeoToolbox / PlaceDescriptor | 26.0+ |
| MKGeocodingRequest | 26.0+ |
| MKReverseGeocodingRequest | 26.0+ |
| MKAddress | 26.0+ |
GeoToolbox 提供了 PlaceDescriptor — 一种适用于 MapKit 和第三方地图服务的物理位置标准化表示。
import GeoToolbox
// 从地址创建
let fountain = PlaceDescriptor(
representations: [.address("121-122 James's St \n Dublin 8 \n D08 ET27 \n Ireland")],
commonName: "Obelisk Fountain"
)
// 从坐标创建
let tower = PlaceDescriptor(
representations: [.coordinate(CLLocationCoordinate2D(latitude: 48.8584, longitude: 2.2945))],
commonName: "Eiffel Tower"
)
// 多重表示
let statue = PlaceDescriptor(
representations: [
.coordinate(CLLocationCoordinate2D(latitude: 40.6892, longitude: -74.0445)),
.address("Liberty Island, New York, NY 10004, United States")
],
commonName: "Statue of Liberty"
)
// 从 MKMapItem 创建
let descriptor = PlaceDescriptor(item: mapItem) // 返回可选值
使用常见地图概念表示地点的枚举:
| 情况 | 用法 |
|---|---|
.coordinate(CLLocationCoordinate2D) | 纬度/经度 |
.address(String) | 完整地址字符串 |
PlaceDescriptor 上的便捷访问器:
descriptor.coordinate // CLLocationCoordinate2D?
descriptor.address // String?
descriptor.commonName // String?
来自不同地图服务的地点的专有标识符:
let place = PlaceDescriptor(
representations: [.coordinate(CLLocationCoordinate2D(latitude: 51.5074, longitude: -0.1278))],
commonName: "London Eye",
supportingRepresentations: [
.serviceIdentifiers([
"com.apple.maps": "AppleMapsID123",
"com.google.maps": "GoogleMapsID456"
])
]
)
// 检索特定服务标识符
let appleID = place.serviceIdentifier(for: "com.apple.maps")
将地址字符串转换为地图项目(地址到坐标):
guard let request = MKGeocodingRequest(addressString: "1 Apple Park Way, Cupertino, CA") else {
return
}
let mapItems = try await request.mapItems
将坐标转换为地图项目(坐标到地址):
let location = CLLocation(latitude: 37.3349, longitude: -122.0090)
guard let request = MKReverseGeocodingRequest(location: location) else {
return
}
let mapItems = try await request.mapItems
从 PlaceDescriptor 创建 MKMapItem 时使用的结构化地址类型:
if let coordinate = descriptor.coordinate {
let location = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
let address = MKAddress()
let mapItem = MKMapItem(location: location, address: address)
}
| 需求 | 使用 |
|---|---|
| 地址字符串转坐标 | MKGeocodingRequest |
| 坐标转地址 | MKReverseGeocodingRequest |
| 自然语言地点搜索 | MKLocalSearch |
| 自动补全建议 | MKLocalSearchCompleter |
| 跨服务地点标识符 | 带 SupportingPlaceRepresentation 的 PlaceDescriptor |
WWDC : 2023-10043, 2024-10094
文档 : /mapkit, /mapkit/map, /mapkit/mklocalsearch, /mapkit/mkdirections, /geotoolbox, /geotoolbox/placedescriptor, /mapkit/mkgeocodingrequest, /mapkit/mkreversegeocodingrequest, /mapkit/mkaddress
技能 : mapkit, mapkit-diag, core-location-ref
每周安装次数
64
仓库
GitHub 星标
690
首次出现
2026年2月27日
安全审计
安装于
gemini-cli62
github-copilot62
amp62
cline62
codex62
kimi-cli62
Complete MapKit API reference for iOS development. Covers both SwiftUI Map (iOS 17+) and MKMapView (UIKit).
axiom-mapkit — Decision trees, anti-patterns, pressure scenariosaxiom-mapkit-diag — Symptom-based troubleshooting| Feature | SwiftUI Map (iOS 17+) | MKMapView |
|---|---|---|
| Declaration | Map(position:) { content } | MKMapView() |
| Camera control | MapCameraPosition binding | setRegion(_:animated:) |
| Annotations | Marker, Annotation in content | addAnnotation(_:) + delegate |
| Overlays | MapCircle, MapPolyline, MapPolygon | addOverlay(_:) + renderer delegate |
| User location | UserAnnotation() | showsUserLocation = true |
| Selection | .mapSelection($selection) | delegate didSelect |
| Controls | .mapControls { } | showsCompass, showsScale |
| Interaction modes | .mapInteractionModes([]) | delegate methods |
| Clustering | Built-in via .mapItemClusteringIdentifier | MKClusterAnnotation |
@State private var cameraPosition: MapCameraPosition = .automatic
Map(position: $cameraPosition) {
Marker("Home", coordinate: homeCoord)
Annotation("Custom", coordinate: coord) {
Image(systemName: "star.fill")
.foregroundStyle(.yellow)
.padding(4)
.background(.blue, in: Circle())
}
UserAnnotation()
MapCircle(center: coord, radius: 500)
.foregroundStyle(.blue.opacity(0.3))
MapPolyline(coordinates: routeCoords)
.stroke(.blue, lineWidth: 3)
}
.mapStyle(.standard(elevation: .realistic))
.mapControls {
MapUserLocationButton()
MapCompass()
MapScaleView()
}
Controls where the camera is positioned:
// System manages camera to show all content
.automatic
// Specific region
.region(MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194),
span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
))
// Specific camera with pitch and heading
.camera(MapCamera(
centerCoordinate: coordinate,
distance: 1000, // meters from center
heading: 90, // degrees from north
pitch: 60 // degrees from vertical (0 = top-down)
))
// Follow user location
.userLocation(followsHeading: true, fallback: .automatic)
// Show specific item
.item(mapItem)
// Show specific rect
.rect(MKMapRect(...))
// Animate to new position
withAnimation {
cameraPosition = .region(newRegion)
}
// Keyframe animation (iOS 17+)
Map(position: $cameraPosition)
.mapCameraKeyframeAnimator(trigger: flyToTrigger) { initialCamera in
KeyframeTrack(\.centerCoordinate) {
LinearKeyframe(destination, duration: 2.0)
}
KeyframeTrack(\.distance) {
CubicKeyframe(5000, duration: 1.0)
CubicKeyframe(1000, duration: 1.0)
}
}
@State private var selectedItem: MKMapItem?
Map(position: $cameraPosition, selection: $selectedItem) {
ForEach(mapItems, id: \.self) { item in
Marker(item: item)
}
}
.onChange(of: selectedItem) { _, newItem in
if let newItem {
// Handle selection
}
}
Map(position: $cameraPosition) { ... }
.onMapCameraChange { context in
// context.region — visible MKCoordinateRegion
// context.camera — current MapCamera
// context.rect — visible MKMapRect
fetchAnnotations(in: context.region)
}
.onMapCameraChange(frequency: .continuous) { context in
// Called during gesture (not just at end)
}
.mapStyle(.standard) // Default
.mapStyle(.standard(elevation: .realistic)) // 3D buildings
.mapStyle(.standard(emphasis: .muted)) // Muted colors
.mapStyle(.standard(pointsOfInterest: .including([.restaurant, .cafe])))
.mapStyle(.imagery) // Satellite
.mapStyle(.imagery(elevation: .realistic)) // 3D satellite
.mapStyle(.hybrid) // Satellite + labels
.mapStyle(.hybrid(elevation: .realistic)) // 3D hybrid
// Allow all interactions (default)
.mapInteractionModes(.all)
// Read-only map (no interaction)
.mapInteractionModes([])
// Pan only, no zoom
.mapInteractionModes([.pan])
// Pan and zoom, no rotate/pitch
.mapInteractionModes([.pan, .zoom])
System-styled map marker with callout:
// Basic marker
Marker("Coffee Shop", coordinate: coord)
// With system image
Marker("Coffee Shop", systemImage: "cup.and.saucer.fill", coordinate: coord)
// With monogram (2 characters max)
Marker("Coffee Shop", monogram: Text("CS"), coordinate: coord)
// Color
Marker("Coffee Shop", coordinate: coord)
.tint(.brown)
// From MKMapItem
Marker(item: mapItem)
Fully custom view at a coordinate:
Annotation("Custom Pin", coordinate: coord) {
VStack {
Image(systemName: "mappin.circle.fill")
.font(.title)
.foregroundStyle(.red)
Text("Here")
.font(.caption)
}
}
// Anchor point (default is bottom center)
Annotation("Pin", coordinate: coord, anchor: .center) {
Circle()
.fill(.blue)
.frame(width: 20, height: 20)
}
Current user location indicator:
UserAnnotation()
// Custom appearance
UserAnnotation(anchor: .center) {
Image(systemName: "location.circle.fill")
.foregroundStyle(.blue)
}
// Circle
MapCircle(center: coord, radius: 1000) // radius in meters
.foregroundStyle(.blue.opacity(0.2))
.stroke(.blue, lineWidth: 2)
// Polygon
MapPolygon(coordinates: polygonCoords)
.foregroundStyle(.green.opacity(0.3))
.stroke(.green, lineWidth: 2)
// Polyline
MapPolyline(coordinates: routeCoords)
.stroke(.blue, lineWidth: 4)
// From MKRoute
MapPolyline(route.polyline)
.stroke(.blue, lineWidth: 5)
ForEach(locations) { location in
Marker(location.name, coordinate: location.coordinate)
.tag(location.id)
}
.mapItemClusteringIdentifier("locations")
struct MapViewWrapper: UIViewRepresentable {
@Binding var region: MKCoordinateRegion
let annotations: [MKAnnotation]
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.delegate = context.coordinator
mapView.showsUserLocation = true
mapView.register(
MKMarkerAnnotationView.self,
forAnnotationViewWithReuseIdentifier: "marker"
)
return mapView
}
func updateUIView(_ mapView: MKMapView, context: Context) {
// Guard against infinite loops
if !regionsAreEqual(mapView.region, region) {
mapView.setRegion(region, animated: true)
}
// Diff annotations instead of removing all
let current = Set(mapView.annotations.compactMap { $0 as? MyAnnotation })
let desired = Set(annotations.compactMap { $0 as? MyAnnotation })
let toAdd = desired.subtracting(current)
let toRemove = current.subtracting(desired)
mapView.addAnnotations(Array(toAdd))
mapView.removeAnnotations(Array(toRemove))
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
static func dismantleUIView(_ mapView: MKMapView, coordinator: Coordinator) {
mapView.removeAnnotations(mapView.annotations)
mapView.removeOverlays(mapView.overlays)
}
}
class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapViewWrapper
init(_ parent: MapViewWrapper) {
self.parent = parent
}
// Annotation view customization
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard !(annotation is MKUserLocation) else { return nil } // Use default for user
let view = mapView.dequeueReusableAnnotationView(
withIdentifier: "marker",
for: annotation
) as! MKMarkerAnnotationView
view.markerTintColor = .systemRed
view.glyphImage = UIImage(systemName: "mappin")
view.clusteringIdentifier = "poi"
view.canShowCallout = true
return view
}
// Overlay rendering
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if let circle = overlay as? MKCircle {
let renderer = MKCircleRenderer(circle: circle)
renderer.fillColor = UIColor.systemBlue.withAlphaComponent(0.2)
renderer.strokeColor = .systemBlue
renderer.lineWidth = 2
return renderer
}
if let polyline = overlay as? MKPolyline {
let renderer = MKPolylineRenderer(polyline: polyline)
renderer.strokeColor = .systemBlue
renderer.lineWidth = 4
return renderer
}
if let polygon = overlay as? MKPolygon {
let renderer = MKPolygonRenderer(polygon: polygon)
renderer.fillColor = UIColor.systemGreen.withAlphaComponent(0.3)
renderer.strokeColor = .systemGreen
renderer.lineWidth = 2
return renderer
}
return MKOverlayRenderer(overlay: overlay)
}
// Region change tracking
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
parent.region = mapView.region
}
// Annotation selection
func mapView(_ mapView: MKMapView, didSelect annotation: MKAnnotation) {
// Handle tap
}
// Cluster annotation
func mapView(
_ mapView: MKMapView,
clusterAnnotationForMemberAnnotations memberAnnotations: [MKAnnotation]
) -> MKClusterAnnotation {
MKClusterAnnotation(memberAnnotations: memberAnnotations)
}
}
Balloon-shaped marker with glyph:
let view = MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "marker")
view.markerTintColor = .systemPurple
view.glyphImage = UIImage(systemName: "star.fill")
view.glyphText = "A" // Text glyph (overrides image)
view.displayPriority = .required // Always visible
view.clusteringIdentifier = "category" // Enable clustering
view.canShowCallout = true
view.titleVisibility = .adaptive // Show title based on space
view.subtitleVisibility = .hidden
Fully custom annotation view:
let view = MKAnnotationView(annotation: annotation, reuseIdentifier: "custom")
view.image = UIImage(named: "custom-pin")
view.centerOffset = CGPoint(x: 0, y: -view.image!.size.height / 2)
view.canShowCallout = true
view.leftCalloutAccessoryView = UIImageView(image: thumbnail)
view.rightCalloutAccessoryView = UIButton(type: .detailDisclosure)
func mapView(
_ mapView: MKMapView,
annotationView view: MKAnnotationView,
calloutAccessoryControlTapped control: UIControl
) {
guard let annotation = view.annotation as? MyAnnotation else { return }
// Navigate to detail view
}
Always use dequeueReusableAnnotationView(withIdentifier:for:):
// Register in makeUIView (once)
mapView.register(MKMarkerAnnotationView.self, forAnnotationViewWithReuseIdentifier: "marker")
// Dequeue in delegate (every time)
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
let view = mapView.dequeueReusableAnnotationView(withIdentifier: "marker", for: annotation)
// Configure...
return view
}
Without reuse: 1000 annotations = 1000 views in memory. With reuse: ~20-30 views recycled as user scrolls.
let completer = MKLocalSearchCompleter()
completer.delegate = self
completer.resultTypes = [.pointOfInterest, .address]
completer.region = visibleMapRegion // Bias results to visible area
// Update on each keystroke
completer.queryFragment = "coffee"
// Delegate receives results
func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
let results = completer.results // [MKLocalSearchCompletion]
for result in results {
// result.title — "Starbucks"
// result.subtitle — "123 Main St, San Francisco, CA"
// result.titleHighlightRanges — Ranges matching query
}
}
func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
// Network error, rate limit, etc.
}
// From autocomplete completion
let request = MKLocalSearch.Request(completion: selectedCompletion)
// From natural language
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = "coffee shops"
request.region = mapRegion // Bias results
request.resultTypes = .pointOfInterest // Filter type
request.pointOfInterestFilter = MKPointOfInterestFilter(
including: [.cafe, .restaurant]
)
let search = MKLocalSearch(request: request)
let response = try await search.start()
for item in response.mapItems {
// item.name — "Starbucks"
// item.placemark — MKPlacemark with address
// item.placemark.coordinate — CLLocationCoordinate2D
// item.phoneNumber — optional phone
// item.url — optional website
// item.pointOfInterestCategory — .cafe, .restaurant, etc.
}
// Filter what kind of results to return
request.resultTypes = .address // Street addresses only
request.resultTypes = .pointOfInterest // Businesses, landmarks
request.resultTypes = .physicalFeature // Mountains, lakes, parks
request.resultTypes = .query // Suggested search queries (iOS 18+)
request.resultTypes = [.pointOfInterest, .address] // Multiple types
MKLocalSearchCompleter handles its own throttling — safe to call on every keystrokeMKLocalSearch — Apple rate-limits these; don't fire more than ~1/secondMKLocalSearchCompleter instances — don't create new ones per querylet request = MKDirections.Request()
request.source = MKMapItem.forCurrentLocation()
request.destination = destinationMapItem
request.transportType = .automobile
request.requestsAlternateRoutes = true // Get multiple routes
let directions = MKDirections(request: request)
let response = try await directions.calculate()
for route in response.routes {
route.polyline // MKPolyline — display on map
route.expectedTravelTime // TimeInterval in seconds
route.distance // CLLocationDistance in meters
route.name // "I-280 S" — route name
route.advisoryNotices // [String] — warnings
route.steps // [MKRoute.Step] — turn-by-turn
}
.automobile // Driving directions
.walking // Pedestrian directions
.transit // Public transit (where available)
.any // All modes
let directions = MKDirections(request: request)
let eta = try await directions.calculateETA()
eta.expectedTravelTime // TimeInterval
eta.distance // CLLocationDistance
eta.expectedArrivalDate // Date
eta.expectedDepartureDate // Date
eta.transportType // MKDirectionsTransportType
for step in route.steps {
step.instructions // "Turn right onto Main St"
step.distance // CLLocationDistance in meters
step.polyline // MKPolyline for this step's segment
step.transportType // May change for transit routes
step.notice // Optional advisory
}
let request = MKLookAroundSceneRequest(coordinate: coordinate)
do {
let scene = try await request.scene
// scene is non-nil — Look Around available at this coordinate
} catch {
// Look Around not available here
}
@State private var lookAroundScene: MKLookAroundScene?
LookAroundPreview(scene: $lookAroundScene)
.frame(height: 200)
// Load scene
func loadLookAround(for coordinate: CLLocationCoordinate2D) async {
let request = MKLookAroundSceneRequest(coordinate: coordinate)
lookAroundScene = try? await request.scene
}
let controller = MKLookAroundViewController(scene: scene)
// Present modally or embed as child view controller
let snapshotter = MKLookAroundSnapshotter(scene: scene, options: .init())
let snapshot = try await snapshotter.snapshot
let image = snapshot.image // UIImage
// Circle
let circle = MKCircle(center: coordinate, radius: 1000)
mapView.addOverlay(circle)
// Polygon
let polygon = MKPolygon(coordinates: &coords, count: coords.count)
mapView.addOverlay(polygon)
// Polyline
let polyline = MKPolyline(coordinates: &coords, count: coords.count)
mapView.addOverlay(polyline, level: .aboveRoads)
// Custom tile overlay
let template = "https://tile.example.com/{z}/{x}/{y}.png"
let tileOverlay = MKTileOverlay(urlTemplate: template)
tileOverlay.canReplaceMapContent = true // Hides Apple Maps base layer
mapView.addOverlay(tileOverlay, level: .aboveLabels)
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
switch overlay {
case let circle as MKCircle:
let renderer = MKCircleRenderer(circle: circle)
renderer.fillColor = UIColor.systemBlue.withAlphaComponent(0.2)
renderer.strokeColor = .systemBlue
renderer.lineWidth = 2
return renderer
case let polyline as MKPolyline:
let renderer = MKPolylineRenderer(polyline: polyline)
renderer.strokeColor = .systemBlue
renderer.lineWidth = 4
return renderer
case let polygon as MKPolygon:
let renderer = MKPolygonRenderer(polygon: polygon)
renderer.fillColor = UIColor.systemGreen.withAlphaComponent(0.3)
renderer.strokeColor = .systemGreen
renderer.lineWidth = 2
return renderer
case let tile as MKTileOverlay:
return MKTileOverlayRenderer(tileOverlay: tile)
default:
return MKOverlayRenderer(overlay: overlay)
}
}
mapView.addOverlay(overlay, level: .aboveRoads) // Above roads, below labels
mapView.addOverlay(overlay, level: .aboveLabels) // Above everything
let renderer = MKGradientPolylineRenderer(polyline: polyline)
renderer.setColors([.green, .yellow, .red], locations: [0.0, 0.5, 1.0])
renderer.lineWidth = 6
Generate static map images for sharing, thumbnails, or offline display:
let options = MKMapSnapshotter.Options()
options.region = MKCoordinateRegion(
center: coordinate,
span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
)
options.size = CGSize(width: 300, height: 200)
options.scale = UIScreen.main.scale // Retina support
options.mapType = .standard
options.showsBuildings = true
options.pointOfInterestFilter = .excludingAll // Clean map
let snapshotter = MKMapSnapshotter(options: options)
let snapshot = try await snapshotter.start()
let image = snapshot.image
// Draw custom annotations on snapshot
UIGraphicsBeginImageContextWithOptions(image.size, true, image.scale)
image.draw(at: .zero)
let pinImage = UIImage(systemName: "mappin.circle.fill")!
let point = snapshot.point(for: coordinate)
pinImage.draw(at: CGPoint(
x: point.x - pinImage.size.width / 2,
y: point.y - pinImage.size.height
))
let finalImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
// Convert coordinate to point in snapshot image
let point = snapshot.point(for: coordinate)
// Check if coordinate is in snapshot bounds
let isVisible = CGRect(origin: .zero, size: snapshot.image.size).contains(point)
| Feature | iOS Version |
|---|---|
| MKMapView | 3.0+ |
| MKLocalSearch | 6.1+ |
| MKDirections | 7.0+ |
| MKMarkerAnnotationView | 11.0+ |
| MKMapSnapshotter | 7.0+ |
| MKLookAroundSceneRequest | 16.0+ |
| LookAroundPreview (SwiftUI) | 17.0+ |
| SwiftUI Map (content builder) | 17.0+ |
| MapCameraPosition | 17.0+ |
| .mapSelection | 17.0+ |
| .mapCameraKeyframeAnimator | 17.0+ |
| .onMapCameraChange | 17.0+ |
| MapUserLocationButton | 17.0+ |
| MapCompass | 17.0+ |
| MapScaleView |
GeoToolbox provides PlaceDescriptor — a standardized representation of physical locations that works across MapKit and third-party mapping services.
import GeoToolbox
// From address
let fountain = PlaceDescriptor(
representations: [.address("121-122 James's St \n Dublin 8 \n D08 ET27 \n Ireland")],
commonName: "Obelisk Fountain"
)
// From coordinates
let tower = PlaceDescriptor(
representations: [.coordinate(CLLocationCoordinate2D(latitude: 48.8584, longitude: 2.2945))],
commonName: "Eiffel Tower"
)
// Multiple representations
let statue = PlaceDescriptor(
representations: [
.coordinate(CLLocationCoordinate2D(latitude: 40.6892, longitude: -74.0445)),
.address("Liberty Island, New York, NY 10004, United States")
],
commonName: "Statue of Liberty"
)
// From MKMapItem
let descriptor = PlaceDescriptor(item: mapItem) // Returns optional
Enum representing a place using common mapping concepts:
| Case | Usage |
|---|---|
.coordinate(CLLocationCoordinate2D) | Latitude/longitude |
.address(String) | Full address string |
Convenience accessors on PlaceDescriptor:
descriptor.coordinate // CLLocationCoordinate2D?
descriptor.address // String?
descriptor.commonName // String?
Proprietary identifiers for places from different mapping services:
let place = PlaceDescriptor(
representations: [.coordinate(CLLocationCoordinate2D(latitude: 51.5074, longitude: -0.1278))],
commonName: "London Eye",
supportingRepresentations: [
.serviceIdentifiers([
"com.apple.maps": "AppleMapsID123",
"com.google.maps": "GoogleMapsID456"
])
]
)
// Retrieve a specific service identifier
let appleID = place.serviceIdentifier(for: "com.apple.maps")
Convert an address string to map items (address to coordinates):
guard let request = MKGeocodingRequest(addressString: "1 Apple Park Way, Cupertino, CA") else {
return
}
let mapItems = try await request.mapItems
Convert coordinates to map items (coordinates to address):
let location = CLLocation(latitude: 37.3349, longitude: -122.0090)
guard let request = MKReverseGeocodingRequest(location: location) else {
return
}
let mapItems = try await request.mapItems
Structured address type used when creating MKMapItem from a PlaceDescriptor:
if let coordinate = descriptor.coordinate {
let location = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
let address = MKAddress()
let mapItem = MKMapItem(location: location, address: address)
}
| Need | Use |
|---|---|
| Address string to coordinates | MKGeocodingRequest |
| Coordinates to address | MKReverseGeocodingRequest |
| Natural language place search | MKLocalSearch |
| Autocomplete suggestions | MKLocalSearchCompleter |
| Cross-service place identifiers | PlaceDescriptor with SupportingPlaceRepresentation |
WWDC : 2023-10043, 2024-10094
Docs : /mapkit, /mapkit/map, /mapkit/mklocalsearch, /mapkit/mkdirections, /geotoolbox, /geotoolbox/placedescriptor, /mapkit/mkgeocodingrequest, /mapkit/mkreversegeocodingrequest, /mapkit/mkaddress
Skills : mapkit, mapkit-diag, core-location-ref
Weekly Installs
64
Repository
GitHub Stars
690
First Seen
Feb 27, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykWarn
Installed on
gemini-cli62
github-copilot62
amp62
cline62
codex62
kimi-cli62
lark-cli 共享规则:飞书资源操作指南与权限配置详解
41,800 周安装
| 17.0+ |
| .mapInteractionModes | 17.0+ |
| MKLocalSearch.ResultType.query | 18.0+ |
| GeoToolbox / PlaceDescriptor | 26.0+ |
| MKGeocodingRequest | 26.0+ |
| MKReverseGeocodingRequest | 26.0+ |
| MKAddress | 26.0+ |