Communitygithub.com

JordanCoin/ios-skills-collection

在 iOS/macOS 应用中实现、审查或改进使用 MapKit 和 CoreLocation 的地图与定位功能。用于处理 Map 视图、标注、标记、折线、用户位置追踪、地理编码、反向地理编码、搜索/自动补全、导航与路径、地理围栏、区域监控、CLLocationUpdate 异步流或位置授权流程。也用于处理 Swift 应用中的地图、坐标、地址、地点、方向、距离计算或基于位置的功能。

ios-skills-collection 是什么?

该技能为使用 Apple 框架(MapKit 和 CoreLocation)在 iOS 17+(使用 SwiftUI)和 macOS 应用中构建地图和基于位置的功能提供了全面的指南。它指导开发人员完成典型工作流程,例如添加带有标记的地图、使用现代 `CLLocationUpdate.liveUpdates()` API 跟踪用户位置、搜索地点、获取路线以及使用 `PlaceDescriptor`。它强调现代 Swift 并发模型(`async/await`),强烈建议弃用过时的 `CLLocationManagerDelegate` 模式,以支持更清晰的异步序列。该技能通过详细的代码片段和完整的审查清单,涵盖了从地图样式和相机控制到地理编码、自动补全、方向计算和地理围栏的主题。它旨在用于开发过程中的代码编写和审查,确保遵循最佳实践以避免常见错误,例如不必要的后台权限请求以及在视图不可见时保持位置更新导致的电池消耗。

兼容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/JordanCoin/ios-skills-collection/tree/HEAD/skills/dpearson2699--mapkit

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

MapKit

使用 SwiftUI MapKit 和现代 CoreLocation 异步 API 构建面向 iOS 17+ 的地图和基于位置的功能。使用带有 MapContentBuilder 的 Map 构建视图,使用 CLLocationUpdate.liveUpdates() 进行流式位置更新,使用 CLMonitor 进行地理围栏。

有关扩展的 MapKit 模式,请参阅 references/mapkit-patterns.md;有关 CoreLocation 模式,请参阅 references/mapkit-corelocation-patterns.md。

目录

工作流程

1. 添加带有标记或标注的地图

  1. 导入 MapKit。
  2. 创建一个 Map 视图,可选择绑定 MapCameraPosition。
  3. 在 MapContentBuilder 闭包内添加 Marker、Annotation、MapPolyline、MapPolygon 或 MapCircle。
  4. 使用 .mapStyle() 配置地图样式。
  5. 使用 .mapControls { } 添加地图控件。
  6. 使用 selection: 绑定处理选择事件。

2. 跟踪用户位置

  1. 在 Info.plist 中添加 NSLocationWhenInUseUsageDescription。
  2. 在 iOS 18+ 上,创建一个 CLServiceSession 来管理授权。
  3. 在 Task 中迭代 CLLocationUpdate.liveUpdates()。
  4. 在更新 UI 之前,根据距离或精度过滤更新。
  5. 当不再需要位置跟踪时停止任务。

3. 搜索地点

  1. 配置 MKLocalSearchCompleter 用于自动补全建议。
  2. 在设置查询之前,对用户输入进行防抖处理(至少 300 毫秒)。
  3. 将选中的补全项转换为 MKLocalSearch.Request 以获取完整结果。
  4. 将结果显示为标记或列表。

4. 获取导航并显示路线

  1. 创建一个包含源和目标 MKMapItem 的 MKDirections.Request。
  2. 设置 transportType(.automobile、.walking、.transit、.cycling)。
  3. 等待 MKDirections.calculate()。
  4. 使用 MapPolyline(route.polyline) 绘制路线。

5. 审查现有的地图/位置代码

运行本文件末尾的审查清单。

SwiftUI Map 视图 (iOS 17+)

import MapKit
import SwiftUI

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

    var body: some View {
        Map(position: $position) {
            Marker("Apple Park", coordinate: applePark)
            Marker("Infinite Loop", systemImage: "building.2",
                   coordinate: infiniteLoop)
        }
        .mapStyle(.standard(elevation: .realistic))
        .mapControls {
            MapUserLocationButton()
            MapCompass()
            MapScaleView()
        }
    }
}

标记和标注

// 气球标记 —— 定位地点的最简单方式
Marker("Cafe", systemImage: "cup.and.saucer.fill", coordinate: cafeCoord)
    .tint(.brown)

// 标注 —— 在坐标上自定义 SwiftUI 视图
Annotation("You", coordinate: userCoord, anchor: .bottom) {
    Image(systemName: "figure.wave")
        .padding(6)
        .background(.blue.gradient, in: .circle)
        .foregroundStyle(.white)
}

叠加层:折线、多边形、圆形

Map {
    // 根据坐标绘制折线
    MapPolyline(coordinates: routeCoords)
        .stroke(.blue, lineWidth: 4)

    // 多边形(区域高亮)
    MapPolygon(coordinates: parkBoundary)
        .foregroundStyle(.green.opacity(0.3))
        .stroke(.green, lineWidth: 2)

    // 圆形(围绕点的一定半径范围)
    MapCircle(center: storeCoord, radius: 500)
        .foregroundStyle(.red.opacity(0.15))
        .stroke(.red, lineWidth: 1)
}

相机位置

MapCameraPosition 控制地图显示的内容。绑定它可以让用户交互或以编程方式移动相机。

// 中心到某个区域
@State private var position: MapCameraPosition = .region(
    MKCoordinateRegion(
        center: CLLocationCoordinate2D(latitude: 37.334, longitude: -122.009),
        span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
    )
)

// 跟随用户位置
@State private var position: MapCameraPosition = .userLocation(fallback: .automatic)

// 特定相机角度(3D 透视)
@State private var position: MapCameraPosition = .camera(
    MapCamera(centerCoordinate: applePark, distance: 1000, heading: 90, pitch: 60)
)

// 框选特定项
position = .item(MKMapItem.forCurrentLocation())
position = .rect(MKMapRect(...))

地图样式

.mapStyle(.standard)                                        // 默认道路地图
.mapStyle(.standard(elevation: .realistic, showsTraffic: true))
.mapStyle(.imagery)                                         // 卫星
.mapStyle(.imagery(elevation: .realistic))                  // 3D 卫星
.mapStyle(.hybrid)                                          // 卫星 + 标签
.mapStyle(.hybrid(elevation: .realistic, showsTraffic: true))

地图交互模式

.mapInteractionModes(.all)           // 默认:平移、缩放、旋转、倾斜
.mapInteractionModes(.pan)           // 仅平移
.mapInteractionModes([.pan, .zoom])  // 平移和缩放
.mapInteractionModes([])             // 静态地图(无交互)

地图选择

@State private var selectedMarker: MKMapItem?

Map(selection: $selectedMarker) {
    ForEach(places) { place in
        Marker(place.name, coordinate: place.coordinate)
            .tag(place.mapItem)     // 标签类型必须匹配选择类型
    }
}
.onChange(of: selectedMarker) { _, newValue in
    guard let item = newValue else { return }
    // 响应选择
}

CoreLocation 现代 API

CLLocationUpdate.liveUpdates() (iOS 17+)

用单个异步序列替换 CLLocationManagerDelegate 回调。每次迭代产生一个包含可选 CLLocation 的 CLLocationUpdate。

import CoreLocation

@Observable
final class LocationTracker: @unchecked Sendable {
    var currentLocation: CLLocation?
    private var updateTask: Task<Void, Never>?

    func startTracking() {
        updateTask = Task {
            let updates = CLLocationUpdate.liveUpdates()
            for try await update in updates {
                guard let location = update.location else { continue }
                // 根据水平精度过滤
                guard location.horizontalAccuracy < 50 else { continue }
                await MainActor.run {
                    self.currentLocation = location
                }
            }
        }
    }

    func stopTracking() {
        updateTask?.cancel()
        updateTask = nil
    }
}

CLServiceSession (iOS 18+)

声明某个功能生命周期内的授权要求。在需要位置服务期间持有对该会话的引用。

// 使用中授权,首选完全精度
let session = CLServiceSession(
    authorization: .whenInUse,
    fullAccuracyPurposeKey: "NearbySearchPurpose"
)
// 将 `session` 作为存储属性持有;完成后释放它。

在 iOS 18+ 上,如果你没有显式创建 CLServiceSession,CLLocationUpdate.liveUpdates() 和 CLMonitor 会使用隐式 CLServiceSession。当你需要 .always 授权或完全精度时,请显式创建一个。

授权流程

// Info.plist 键(必需):
// NSLocationWhenInUseUsageDescription
// NSLocationAlwaysAndWhenInUseUsageDescription(仅当需要 .always 时)

// 检查授权并在被拒绝时引导用户前往设置
struct LocationPermissionView: View {
    @Environment(\.openURL) private var openURL

    var body: some View {
        ContentUnavailableView {
            Label("位置访问被拒绝", systemImage: "location.slash")
        } description: {
            Text("请在设置中启用位置访问以使用此功能。")
        } actions: {
            Button("打开设置") {
                if let url = URL(string: UIApplication.openSettingsURLString) {
                    openURL(url)
                }
            }
        }
    }
}

地理编码

CLGeocoder (iOS 8+)

let geocoder = CLGeocoder()

// 正向地理编码:地址字符串 -> 坐标
let placemarks = try await geocoder.geocodeAddressString("1 Apple Park Way, Cupertino")
if let location = placemarks.first?.location {
    print(location.coordinate) // CLLocationCoordinate2D
}

// 反向地理编码:坐标 -> 地标
let location = CLLocation(latitude: 37.3349, longitude: -122.0090)
let placemarks = try await geocoder.reverseGeocodeLocation(location)
if let placemark = placemarks.first {
    let address = [placemark.name, placemark.locality, placemark.administrativeArea]
        .compactMap { $0 }
        .joined(separator: ", ")
}

MKGeocodingRequest 和 MKReverseGeocodingRequest (iOS 26+)

新的 MapKit 原生地理编码,返回包含更丰富数据的 MKMapItem,并支持 MKAddress / MKAddressRepresentations 以灵活地格式化地址。

@available(iOS 26, *)
func reverseGeocode(location: CLLocation) async throws -> MKMapItem? {
    guard let request = MKReverseGeocodingRequest(location: location) else {
        return nil
    }
    let mapItems = try await request.mapItems
    return mapItems.first
}

@available(iOS 26, *)
func forwardGeocode(address: String) async throws -> [MKMapItem] {
    guard let request = MKGeocodingRequest(addressString: address) else { return [] }
    return try await request.mapItems
}

搜索

MKLocalSearchCompleter (自动补全)

@Observable
final class SearchCompleter: NSObject, MKLocalSearchCompleterDelegate {
    var results: [MKLocalSearchCompletion] = []
    var query: String = "" { didSet { completer.queryFragment = query } }

    private let completer = MKLocalSearchCompleter()

    override init() {
        super.init()
        completer.delegate = self
        completer.resultTypes = [.address, .pointOfInterest]
    }

    func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
        results = completer.results
    }

    func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
        results = []
    }
}

MKLocalSearch (完整搜索)

func search(for completion: MKLocalSearchCompletion) async throws -> [MKMapItem] {
    let request = MKLocalSearch.Request(completion: completion)
    request.resultTypes = [.pointOfInterest, .address]
    let search = MKLocalSearch(request: request)
    let response = try await search.start()
    return response.mapItems
}

// 在区域内根据自然语言查询搜索
func searchNearby(query: String, region: MKCoordinateRegion) async throws -> [MKMapItem] {
    let request = MKLocalSearch.Request()
    request.naturalLanguageQuery = query
    request.region = region
    let search = MKLocalSearch(request: request)
    let response = try await search.start()
    return response.mapItems
}

导航

func getDirections(from source: MKMapItem, to destination: MKMapItem,
                   transport: MKDirectionsTransportType = .automobile) async throws -> MKRoute? {
    let request = MKDirections.Request()
    request.source = source
    request.destination = destination
    request.transportType = transport
    let directions = MKDirections(request: request)
    let response = try await directions.calculate()
    return response.routes.first
}

在地图上显示路线

@State private var route: MKRoute?

Map {
    if let route {
        MapPolyline(route.polyline)
            .stroke(.blue, lineWidth: 5)
    }
    Marker("起点", coordinate: startCoord)
    Marker("终点", coordinate: endCoord)
}
.task {
    route = try? await getDirections(from: startItem, to: endItem)
}

ETA 计算

func getETA(from source: MKMapItem, to destination: MKMapItem) async throws -> TimeInterval {
    let request = MKDirections.Request()
    request.source = source
    request.destination = destination
    let directions = MKDirections(request: request)
    let response = try await directions.calculateETA()
    return response.expectedTravelTime
}

骑行导航 (iOS 26+)

@available(iOS 26, *)
func getCyclingDirections(to destination: MKMapItem) async throws -> MKRoute? {
    let request = MKDirections.Request()
    request.source = MKMapItem.forCurrentLocation()
    request.destination = destination
    request.transportType = .cycling
    let directions = MKDirections(request: request)
    let response = try await directions.calculate()
    return response.routes.first
}

PlaceDescriptor (iOS 26+)

无需地点 ID,即可从坐标或地址创建丰富的地点引用。需要 import GeoToolbox。

@available(iOS 26, *)
func lookupPlace(name: String, coordinate: CLLocationCoordinate2D) async throws -> MKMapItem {
    let descriptor = PlaceDescriptor(
        representations: [.coordinate(coordinate)],
        commonName: name
    )
    let request = MKMapItemRequest(placeDescriptor: descriptor)
    return try await request.mapItem
}

常见错误

切勿: 一开始就请求 .authorizedAlways——用户不信任过宽的权限。 应该: 从 .requestWhenInUseAuthorization() 开始,仅当用户启用了后台功能时才升级到 .always。

切勿: 在 iOS 17+ 上为简单的位置获取使用 CLLocationManagerDelegate。 应该: 使用 CLLocationUpdate.liveUpdates() 异步流以获得更清晰、更简洁的代码。

切勿: 在地图/视图不可见时保持位置更新运行(会消耗电池)。 应该: 在 SwiftUI 中使用 .task { },这样更新会在视图消失时自动取消。

切勿: 强制解包 CLPlacemark 属性——它们都是可选的。 应该: 使用 nil 合并:placemark.locality ?? "Unknown"。

切勿: 在用户每次按键时都触发 MKLocalSearchCompleter 查询——会导致速率限制和糟糕的 UX。

[文件被截断以完成翻译]

Individual skills in this repo

This repo contains 11 individual skills — each has its own dedicated page.

JordanCoin/ios-skills-collection

Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries. Use when the user asks about GitHub issues, pull requests, workflows, or wants to interact with GitHub repositories from the command line — including tasks like check CI status, create PR, list issues, or query the GitHub API.

JordanCoin/ios-skills-collection

Create media playback experiences using AVKit. Use when adding video players with AVPlayerViewController, enabling Picture-in-Picture, routing media with AirPlay, using SwiftUI VideoPlayer views, configuring transport controls, displaying subtitles and closed captions, or integrating AVFoundation playback with system UI.

JordanCoin/ios-skills-collection

Integrate and optimize Core ML models in iOS apps for on-device machine learning inference. Covers model loading (.mlmodelc, .mlpackage), predictions with auto-generated classes and MLFeatureProvider, compute unit configuration (CPU, GPU, Neural Engine), MLTensor, VNCoreMLRequest, MLComputePlan, multi-model pipelines, and deployment strategies. Use when loading Core ML models, making predictions, configuring compute units, or profiling model performance.

JordanCoin/ios-skills-collection

Display and manipulate PDF documents using PDFKit. Use when embedding PDFView to show PDF files, creating or modifying PDFDocument instances, adding annotations (highlights, notes, signatures), extracting text with PDFSelection, navigating pages, generating thumbnails, filling PDF forms, or wrapping PDFView in SwiftUI.

JordanCoin/ios-skills-collection

Implement, review, or improve in-app tips and onboarding using Apple's TipKit framework. Use when adding feature discovery tooltips, onboarding flows, contextual tips, first-run experiences, coach marks, or working with Tip protocol, TipView, popoverTip, tip rules, tip events, or feature education UI.

JordanCoin/ios-skills-collection

When the user wants to optimize their Google Play Store listing — title, short description, full description, keywords, ratings, or Play Store-specific features. Use when the user mentions "Google Play", "Android", "Play Store", "Play Console", "short description", "full description indexed", "Google Play ASO", or wants Google Play-specific keyword, creative, or ratings strategy. For iOS App Store optimization, see aso-audit and metadata-optimization.

JordanCoin/ios-skills-collection

When the user wants to implement, optimize, or use App Clips for app discovery and conversion. Use when the user mentions "App Clip", "app clip code", "mini app", "instant app", "App Clip card", "App Clip link", "no download required", "instant experience", or wants to understand how App Clips appear in App Store search. For general App Store discoverability, see aso-audit. For marketing campaigns, see ua-campaign.

JordanCoin/ios-skills-collection

When the user wants to plan a launch strategy for a new app or major update. Also use when the user mentions "app launch", "launch plan", "launch checklist", "pre-launch", "launch day", or "how to launch my app". For ongoing ASO after launch, see aso-audit. For paid acquisition during launch, see ua-campaign.

JordanCoin/ios-skills-collection

When the user wants to analyze their own app's actual performance data from App Store Connect — real downloads, revenue, IAP, subscriptions, trials, or country breakdowns synced via Appeeky Connect. Use when the user asks about "my downloads", "my revenue", "how is my app performing", "ASC data", "sales and trends", "my subscription numbers", "App Store Connect metrics", or wants to compare periods or top markets. For third-party app estimates, see app-analytics. For subscription analytics depth, see monetization-strategy.

JordanCoin/ios-skills-collection

When the user wants a full ASO health audit, review their App Store listing quality, or diagnose why their app isn't ranking. Also use when the user mentions "ASO audit", "ASO score", "why am I not ranking", "listing review", or "optimize my app store page". For keyword-specific research, see keyword-research. For metadata writing, see metadata-optimization.

JordanCoin/ios-skills-collection

Always use when working on any iOS, macOS, Swift, SwiftUI, Xcode, or Apple platform project. Routes to the correct specialized skills based on development phase and task context. This collection contains 210 skills from 28 authors covering the full iOS lifecycle.

相关技能