Loading...
Loading...
Compare original and translation side by side
axiom-mapkit-refaxiom-mapkit-diagaxiom-core-locationaxiom-mapkit-refaxiom-mapkit-diagaxiom-core-location| Anti-Pattern | Time Cost | Fix |
|---|---|---|
| Using MKMapView when SwiftUI Map suffices | 2-4 hours UIViewRepresentable boilerplate | Use SwiftUI |
| Creating annotations in SwiftUI view body | UI freeze with 100+ items, view recreation on every update | Move annotations to model, use |
| No annotation view reuse (MKMapView) | Memory spikes, scroll lag with 500+ annotations | |
| Infinite loop — region change triggers update, update sets region | Guard with |
| Ignoring MapCameraPosition (SwiftUI) | Can't programmatically control camera, broken "center on user" | Bind |
| Synchronous geocoding on main thread | UI freeze for 1-3 seconds per geocode | Use |
| Not filtering annotations to visible region | Loading all 10K annotations at once | Use |
Ignoring | Irrelevant results, slow search | Set |
| 反模式 | 时间成本 | 修复方案 |
|---|---|---|
| 在SwiftUI Map足够满足需求的场景下使用MKMapView | 2-4小时的UIViewRepresentable样板代码 | 针对标准地图功能使用SwiftUI |
| 在SwiftUI视图body中创建标注 | 100+条数据时UI卡顿,每次更新都会重建视图 | 将标注迁移到模型层,使用 |
| 未复用标注视图(MKMapView) | 500+标注时内存飙升、滚动卡顿 | 使用 |
| 死循环 — 区域变更触发update,update又设置区域 | 增加 |
| 未使用MapCameraPosition(SwiftUI) | 无法编程控制相机视角,「定位到用户」功能失效 | 将 |
| 在主线程同步执行地理编码 | 每次编码会导致UI冻结1-3秒 | 使用 |
| 未根据可见区域过滤标注 | 一次性加载全部1万条标注 | 使用 |
MKLocalSearch未指定 | 搜索结果无关、搜索速度慢 | 设置 |
digraph {
"Need map in app?" [shape=diamond];
"iOS 17+ target?" [shape=diamond];
"Need custom tile overlay?" [shape=diamond];
"Need fine-grained delegate control?" [shape=diamond];
"Use SwiftUI Map" [shape=box];
"Use MKMapView\nvia UIViewRepresentable" [shape=box];
"Need map in app?" -> "iOS 17+ target?" [label="yes"];
"iOS 17+ target?" -> "Need custom tile overlay?" [label="yes"];
"iOS 17+ target?" -> "Use MKMapView\nvia UIViewRepresentable" [label="no"];
"Need custom tile overlay?" -> "Use MKMapView\nvia UIViewRepresentable" [label="yes"];
"Need custom tile overlay?" -> "Need fine-grained delegate control?" [label="no"];
"Need fine-grained delegate control?" -> "Use MKMapView\nvia UIViewRepresentable" [label="yes"];
"Need fine-grained delegate control?" -> "Use SwiftUI Map" [label="no"];
}digraph {
"Need map in app?" [shape=diamond];
"iOS 17+ target?" [shape=diamond];
"Need custom tile overlay?" [shape=diamond];
"Need fine-grained delegate control?" [shape=diamond];
"Use SwiftUI Map" [shape=box];
"Use MKMapView\nvia UIViewRepresentable" [shape=box];
"Need map in app?" -> "iOS 17+ target?" [label="yes"];
"iOS 17+ target?" -> "Need custom tile overlay?" [label="yes"];
"iOS 17+ target?" -> "Use MKMapView\nvia UIViewRepresentable" [label="no"];
"Need custom tile overlay?" -> "Use MKMapView\nvia UIViewRepresentable" [label="yes"];
"Need custom tile overlay?" -> "Need fine-grained delegate control?" [label="no"];
"Need fine-grained delegate control?" -> "Use MKMapView\nvia UIViewRepresentable" [label="yes"];
"Need fine-grained delegate control?" -> "Use SwiftUI Map" [label="no"];
}Annotation count?
├─ < 100 → Use Marker/Annotation directly in Map {} content builder
│ Simple, declarative, no performance concern
│
├─ 100-1000 → Enable clustering
│ Set .clusteringIdentifier on annotation views
│ SwiftUI: Marker("", coordinate:).tag(id)
│ MKMapView: view.clusteringIdentifier = "poi"
│
└─ 1000+ → Server-side clustering or visible-region filtering
Fetch only annotations within mapView.region
Or pre-cluster on server, send cluster centroids
MKMapView with view reuse is preferred for very large datasets标注数量?
├─ < 100 → 直接在Map {} 内容构造器中使用Marker/Annotation
│ 简单、声明式,无性能顾虑
│
├─ 100-1000 → 开启聚合功能
│ 为标注视图设置.clusteringIdentifier
│ SwiftUI: Marker("", coordinate:).tag(id)
│ MKMapView: view.clusteringIdentifier = "poi"
│
└─ 1000+ → 服务端聚合或者可见区域过滤
仅拉取mapView.region范围内的标注
或者在服务端预聚合,返回聚合中心点
超大优先选择带视图复用的MKMapViewstruct MapView: View {
@State private var cameraPosition: MapCameraPosition = .automatic
@State private var visibleAnnotations: [Location] = []
let allLocations: [Location] // Full dataset
var body: some View {
Map(position: $cameraPosition) {
ForEach(visibleAnnotations) { location in
Marker(location.name, coordinate: location.coordinate)
}
}
.onMapCameraChange(frequency: .onEnd) { context in
visibleAnnotations = allLocations.filter { location in
context.region.contains(location.coordinate)
}
}
}
}
extension MKCoordinateRegion {
func contains(_ coordinate: CLLocationCoordinate2D) -> Bool {
let latRange = (center.latitude - span.latitudeDelta / 2)...(center.latitude + span.latitudeDelta / 2)
let lngRange = (center.longitude - span.longitudeDelta / 2)...(center.longitude + span.longitudeDelta / 2)
return latRange.contains(coordinate.latitude) && lngRange.contains(coordinate.longitude)
}
}struct MapView: View {
@State private var cameraPosition: MapCameraPosition = .automatic
@State private var visibleAnnotations: [Location] = []
let allLocations: [Location] // 全量数据
var body: some View {
Map(position: $cameraPosition) {
ForEach(visibleAnnotations) { location in
Marker(location.name, coordinate: location.coordinate)
}
}
.onMapCameraChange(frequency: .onEnd) { context in
visibleAnnotations = allLocations.filter { location in
context.region.contains(location.coordinate)
}
}
}
}
extension MKCoordinateRegion {
func contains(_ coordinate: CLLocationCoordinate2D) -> Bool {
let latRange = (center.latitude - span.latitudeDelta / 2)...(center.latitude + span.latitudeDelta / 2)
let lngRange = (center.longitude - span.longitudeDelta / 2)...(center.longitude + span.longitudeDelta / 2)
return latRange.contains(coordinate.latitude) && lngRange.contains(coordinate.longitude)
}
}Search implementation:
├─ User types search query
│ └─ MKLocalSearchCompleter (real-time autocomplete)
│ Configure: resultTypes, region bias
│ └─ User selects result
│ └─ MKLocalSearch (full result with MKMapItem)
│ Use completion.title for MKLocalSearch.Request
│
└─ Programmatic search (e.g., "nearest gas station")
└─ MKLocalSearch with naturalLanguageQuery
Configure: resultTypes, region, pointOfInterestFilter
Directions implementation:
├─ MKDirections.Request
│ Set source (MKMapItem.forCurrentLocation()) and destination
│ Set transportType (.automobile, .walking, .transit)
│
└─ MKDirections.calculate()
└─ MKRoute
├─ .polyline → Display as MapPolyline or MKPolylineRenderer
├─ .expectedTravelTime → Show ETA
├─ .distance → Show distance
└─ .steps → Turn-by-turn instructions搜索实现:
├─ 用户输入搜索词
│ └─ MKLocalSearchCompleter (实时自动补全)
│ 配置: resultTypes, 区域偏置
│ └─ 用户选择结果
│ └─ MKLocalSearch (获取带MKMapItem的完整结果)
│ 使用completion.title作为MKLocalSearch.Request参数
│
└─ 编程式搜索 (例如「最近的加油站」)
└─ 搭配naturalLanguageQuery使用MKLocalSearch
配置: resultTypes, 区域, pointOfInterestFilter
路线规划实现:
├─ MKDirections.Request
│ 设置起点 (MKMapItem.forCurrentLocation()) 和终点
│ 设置transportType (.automobile, .walking, .transit)
│
└─ MKDirections.calculate()
└─ MKRoute
├─ .polyline → 展示为MapPolyline或MKPolylineRenderer
├─ .expectedTravelTime → 展示预计到达时间
├─ .distance → 展示距离
└─ .steps → 逐向导航指令Map {}Map {}.clusteringIdentifier.onMapCameraChange.clusteringIdentifier.onMapCameraChangeresultTypes.pointOfInterest.addressregionresultTypes.pointOfInterest.addressregionshowsUserLocation = trueUserAnnotation()showsUserLocation = trueUserAnnotation()// 1. Request authorization with context
let session = CLServiceSession(authorization: .whenInUse)
// 2. Then show map with user location
Map {
UserAnnotation()
}// 1. 带上下文申请权限
let session = CLServiceSession(authorization: .whenInUse)
// 2. 再展示带用户位置的地图
Map {
UserAnnotation()
}CLServiceSession@Observable
class MapModel {
var cameraPosition: MapCameraPosition = .automatic
private var locationSession: CLServiceSession?
func startShowingUserLocation() {
locationSession = CLServiceSession(authorization: .whenInUse)
}
func stopShowingUserLocation() {
locationSession = nil
}
}CLServiceSession@Observable
class MapModel {
var cameraPosition: MapCameraPosition = .automatic
private var locationSession: CLServiceSession?
func startShowingUserLocation() {
locationSession = CLServiceSession(authorization: .whenInUse)
}
func stopShowingUserLocation() {
locationSession = nil
}
}axiom-core-locationaxiom-core-location-diagaxiom-energyaxiom-core-locationaxiom-core-location-diagaxiom-energystruct ContentView: View {
@State private var cameraPosition: MapCameraPosition = .automatic
@State private var selectedItem: MKMapItem?
let locations: [Location] // Your model
var body: some View {
Map(position: $cameraPosition, selection: $selectedItem) {
UserAnnotation()
ForEach(locations) { location in
Marker(location.name, coordinate: location.coordinate)
.tint(location.category.color)
}
}
.mapStyle(.standard(elevation: .realistic))
.mapControls {
MapUserLocationButton()
MapCompass()
MapScaleView()
}
.onChange(of: selectedItem) { _, item in
if let item {
handleSelection(item)
}
}
}
}struct ContentView: View {
@State private var cameraPosition: MapCameraPosition = .automatic
@State private var selectedItem: MKMapItem?
let locations: [Location] // 你的模型数据
var body: some View {
Map(position: $cameraPosition, selection: $selectedItem) {
UserAnnotation()
ForEach(locations) { location in
Marker(location.name, coordinate: location.coordinate)
.tint(location.category.color)
}
}
.mapStyle(.standard(elevation: .realistic))
.mapControls {
MapUserLocationButton()
MapCompass()
MapScaleView()
}
.onChange(of: selectedItem) { _, item in
if let item {
handleSelection(item)
}
}
}
}@State var cameraPositionselection: $selectedItemMapCameraPosition.automatic.mapControls {}ForEach@State var cameraPositionselection: $selectedItemMapCameraPosition.automatic.mapControls {}ForEach@Observable
class SearchModel {
var searchText = ""
var completions: [MKLocalSearchCompletion] = []
var searchResults: [MKMapItem] = []
private let completer = MKLocalSearchCompleter()
private var completerDelegate: CompleterDelegate?
init() {
completerDelegate = CompleterDelegate { [weak self] results in
self?.completions = results
}
completer.delegate = completerDelegate
completer.resultTypes = [.pointOfInterest, .address]
}
func updateSearch(_ text: String) {
searchText = text
completer.queryFragment = text
}
func search(for completion: MKLocalSearchCompletion) async throws {
let request = MKLocalSearch.Request(completion: completion)
request.resultTypes = [.pointOfInterest, .address]
let search = MKLocalSearch(request: request)
let response = try await search.start()
searchResults = response.mapItems
}
func search(query: String, in region: MKCoordinateRegion) async throws {
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = query
request.region = region
request.resultTypes = .pointOfInterest
let search = MKLocalSearch(request: request)
let response = try await search.start()
searchResults = response.mapItems
}
}@Observable
class SearchModel {
var searchText = ""
var completions: [MKLocalSearchCompletion] = []
var searchResults: [MKMapItem] = []
private let completer = MKLocalSearchCompleter()
private var completerDelegate: CompleterDelegate?
init() {
completerDelegate = CompleterDelegate { [weak self] results in
self?.completions = results
}
completer.delegate = completerDelegate
completer.resultTypes = [.pointOfInterest, .address]
}
func updateSearch(_ text: String) {
searchText = text
completer.queryFragment = text
}
func search(for completion: MKLocalSearchCompletion) async throws {
let request = MKLocalSearch.Request(completion: completion)
request.resultTypes = [.pointOfInterest, .address]
let search = MKLocalSearch(request: request)
let response = try await search.start()
searchResults = response.mapItems
}
func search(query: String, in region: MKCoordinateRegion) async throws {
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = query
request.region = region
request.resultTypes = .pointOfInterest
let search = MKLocalSearch(request: request)
let response = try await search.start()
searchResults = response.mapItems
}
}class CompleterDelegate: NSObject, MKLocalSearchCompleterDelegate {
let onUpdate: ([MKLocalSearchCompletion]) -> Void
init(onUpdate: @escaping ([MKLocalSearchCompletion]) -> Void) {
self.onUpdate = onUpdate
}
func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
onUpdate(completer.results)
}
func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
// Handle error — network issues, rate limiting
}
}class CompleterDelegate: NSObject, MKLocalSearchCompleterDelegate {
let onUpdate: ([MKLocalSearchCompletion]) -> Void
init(onUpdate: @escaping ([MKLocalSearchCompletion]) -> Void) {
self.onUpdate = onUpdate
}
func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
onUpdate(completer.results)
}
func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
// 处理错误 — 网络问题、限流
}
}MKLocalSearchCompleterqueryFragmentMKLocalSearchMKLocalSearchMKLocalSearchCompleterqueryFragmentMKLocalSearchMKLocalSearchfunc calculateDirections(
from source: CLLocationCoordinate2D,
to destination: MKMapItem,
transportType: MKDirectionsTransportType = .automobile
) async throws -> MKRoute {
let request = MKDirections.Request()
request.source = MKMapItem(placemark: MKPlacemark(coordinate: source))
request.destination = destination
request.transportType = transportType
let directions = MKDirections(request: request)
let response = try await directions.calculate()
guard let route = response.routes.first else {
throw MapError.noRouteFound
}
return route
}func calculateDirections(
from source: CLLocationCoordinate2D,
to destination: MKMapItem,
transportType: MKDirectionsTransportType = .automobile
) async throws -> MKRoute {
let request = MKDirections.Request()
request.source = MKMapItem(placemark: MKPlacemark(coordinate: source))
request.destination = destination
request.transportType = transportType
let directions = MKDirections(request: request)
let response = try await directions.calculate()
guard let route = response.routes.first else {
throw MapError.noRouteFound
}
return route
}Map(position: $cameraPosition) {
if let route {
MapPolyline(route.polyline)
.stroke(.blue, lineWidth: 5)
}
Marker("Start", coordinate: startCoord)
Marker("End", coordinate: endCoord)
}Map(position: $cameraPosition) {
if let route {
MapPolyline(route.polyline)
.stroke(.blue, lineWidth: 5)
}
Marker("起点", coordinate: startCoord)
Marker("终点", coordinate: endCoord)
}// Add overlay
mapView.addOverlay(route.polyline, level: .aboveRoads)
// Implement renderer delegate
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if let polyline = overlay as? MKPolyline {
let renderer = MKPolylineRenderer(polyline: polyline)
renderer.strokeColor = .systemBlue
renderer.lineWidth = 5
return renderer
}
return MKOverlayRenderer(overlay: overlay)
}// 添加覆盖层
mapView.addOverlay(route.polyline, level: .aboveRoads)
// 实现renderer delegate
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if let polyline = overlay as? MKPolyline {
let renderer = MKPolylineRenderer(polyline: polyline)
renderer.strokeColor = .systemBlue
renderer.lineWidth = 5
return renderer
}
return MKOverlayRenderer(overlay: overlay)
}let route: MKRoute = ...
let travelTime = route.expectedTravelTime // TimeInterval in seconds
let distance = route.distance // CLLocationDistance in meters
let steps = route.steps // [MKRoute.Step]
for step in steps {
print("\(step.instructions) — \(step.distance)m")
// "Turn right on Main St — 450m"
}let route: MKRoute = ...
let travelTime = route.expectedTravelTime // 单位为秒的TimeInterval
let distance = route.distance // 单位为米的CLLocationDistance
let steps = route.steps // [MKRoute.Step]
for step in steps {
print("\(step.instructions) — \(step.distance)m")
// "在主街右转 — 450米"
}Map(position: $cameraPosition) {
ForEach(locations) { location in
Marker(location.name, coordinate: location.coordinate)
.tag(location.id)
}
.mapItemClusteringIdentifier("locations")
}Map(position: $cameraPosition) {
ForEach(locations) { location in
Marker(location.name, coordinate: location.coordinate)
.tag(location.id)
}
.mapItemClusteringIdentifier("locations")
}func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if let cluster = annotation as? MKClusterAnnotation {
let view = mapView.dequeueReusableAnnotationView(
withIdentifier: "cluster",
for: annotation
) as! MKMarkerAnnotationView
view.markerTintColor = .systemBlue
view.glyphText = "\(cluster.memberAnnotations.count)"
return view
}
let view = mapView.dequeueReusableAnnotationView(
withIdentifier: "pin",
for: annotation
) as! MKMarkerAnnotationView
view.clusteringIdentifier = "locations"
view.markerTintColor = .systemRed
return view
}func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if let cluster = annotation as? MKClusterAnnotation {
let view = mapView.dequeueReusableAnnotationView(
withIdentifier: "cluster",
for: annotation
) as! MKMarkerAnnotationView
view.markerTintColor = .systemBlue
view.glyphText = "\(cluster.memberAnnotations.count)"
return view
}
let view = mapView.dequeueReusableAnnotationView(
withIdentifier: "pin",
for: annotation
) as! MKMarkerAnnotationView
view.clusteringIdentifier = "locations"
view.markerTintColor = .systemRed
return view
}clusteringIdentifiermapView.register(MKMarkerAnnotationView.self, forAnnotationViewWithReuseIdentifier: "pin")clusteringIdentifiermapView.register(MKMarkerAnnotationView.self, forAnnotationViewWithReuseIdentifier: "pin")dequeueReusableAnnotationViewMKLookAroundSceneRequestdequeueReusableAnnotationViewMKLookAroundSceneRequest