Communitygithub.com

JordanCoin/ios-skills-collection

在 iOS/macOS 應用程式中使用 MapKit 和 CoreLocation 實作、審查或改進地圖與位置功能。適用於處理 Map 視圖、標註、標記、折線、使用者位置追蹤、地理編碼、反向地理編碼、搜尋/自動完成、導航路線、地理圍欄、區域監控、CLLocationUpdate 非同步串流或位置授權流程。也適用於在 Swift 應用程式中處理地圖、座標、地址、地點、導航、距離計算或基於位置的功能。

ios-skills-collection 是什麼?

這項技能為開發者提供了一個全面的指南,用於在 iOS 17 以上的應用程式中,使用 SwiftUI 的 MapKit 和現代化的 CoreLocation 非同步 API 來建構地圖和位置感知功能。它涵蓋了從基礎的標記和註釋顯示,到非同步位置串流(`CLLocationUpdate.liveUpdates()`)、地理圍欄(`CLMonitor`)、地理編碼、地點搜尋(包含自動完成)以及路線規劃和顯示的完整工作流程。技能文件特別強調了在 iOS 18+ 中使用 `CLServiceSession` 管理授權的最佳實踐,並提供了具體的程式碼範例來說明如何取代舊有的代理模式(delegate pattern),從而撰寫更簡潔、更現代的 Swift 程式碼。此技能支援的平台包括 iPhone、iPad 和 Mac(透過 Mac Catalyst),並且建議在處理任何與地圖或位置相關的任務時使用,從新增地圖標記到審查現有程式碼皆適用。通過遵循其提供的審查清單和常見錯誤指南,開發者可以避免常見陷阱(例如不必要地保持位置更新或請求過高的權限),確保應用程式的效能、電池續航力和用戶體驗。

相容平台~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+ 的地圖與位置感知功能。使用 Map 搭配 MapContentBuilder 來建立視圖,使用 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. 建立一個 MKDirections.Request,並設定起點和終點 MKMapItem。
  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("咖啡廳", systemImage: "cup.and.saucer.fill", coordinate: cafeCoord)
    .tint(.brown)

// 註釋 — 在座標上放置自訂 SwiftUI 視圖
Annotation("你", 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+)

在某項功能的生命週期內宣告授權需求。只要你需要位置服務,就保留對 session 的參考。

// 使用中授權,並偏好完整準確度
let session = CLServiceSession(
    authorization: .whenInUse,
    fullAccuracyPurposeKey: "NearbySearchPurpose"
)
// 將 `session` 保留為儲存屬性;使用完畢後釋放。

在 iOS 18+ 上,如果你沒有明確建立 CLServiceSession,CLLocationUpdate.liveUpdates() 和 CLMonitor 會隱式使用一個。當你需要 .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)
}

預估到達時間計算

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 ?? "未知"。

不要: 在文字變更時立即觸發 MKLocalSearch。 要: 對文字輸入進行防抖處理(300-500 毫秒),然後使用 MKLocalSearchCompleter 取得建議。

不要: 在建立 MKDirections.Request 時略過 transportType。 要: 明確設定運輸類型,以獲得準確的路線和到達時間。

不要: 假設 CLLocation 的 speed 和 course 始終可用。 要: 在使用前檢查 speed >= 0 和 course >= 0。

不要: 在 iOS 18+ 上建立多個 CLServiceSession 實例。 要: 為應用程式的位置相關功能保留一個 session 參考,以簡化授權管理。

審查清單

  • 資訊清單(Info.plist)是否包含位置授權的說明?
  • 是否使用 .task { } 和 CLLocationUpdate.liveUpdates() 而非 CLLocationManagerDelegate?
  • 是否在設定查詢前對 MKLocalSearchCompleter 的輸入進行了防抖處理?
  • MKMapItem 的標記是否與選取類型(.tag)相符?
  • 路線和到達時間計算是否在背景佇列上執行?
  • 地圖疊加層(折線/多邊形/圓形)是否使用了正確的樣式(線條顏色、填充、不透明度)?
  • 所有 CLPlacemark 屬性在使用前是否都進行了安全解包?
  • 當視圖消失時,位置更新任務是否會取消?
  • 對於 iOS 18+,是否使用 CLServiceSession 來管理授權?
  • 對於 iOS 26+,是否考慮使用 PlaceDescriptor 和 MKGeocodingRequest?
  • 是否已處理位置授權被拒絕的情況?
  • 地圖互動模式是否與預期行為相符?
  • 地圖樣式是否適合應用程式的設計?
  • 如果返回 async 搜尋結果,呼叫端是否處理了錯誤?
  • 地理編碼請求是否會頻繁觸發?考慮加入快取機制。

參考資料

  • references/mapkit-patterns.md — 地圖與 CoreLocation 的擴展模式
  • references/mapkit-corelocation-patterns.md — 現代化 CoreLocation 的使用方式
  • MapKit 建議:使用 MapContentBuilder、MapCameraPosition、MapStyle
  • CoreLocation 建議:使用非同步串流 (CLLocationUpdate.liveUpdates) 和 CLServiceSession
  • MKLookAroundViewController / MKLookAroundSceneRequest 用於街景預覽(iOS 17+)
  • MKMapItem openInMaps(launchOptions:) 用於啟動 Apple 地圖

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.

相關技能