Communitygithub.com

MapKit & CoreLocation iOSスキル

iOS 17+のSwiftUI MapKitと最新CoreLocation async APIを使用して、地図ビュー、アノテーション、ユーザー位置追跡、ジオコーディング、検索、ルート案内、ジオフェンシングなどの地図・位置情報機能を実装、レビュー、改善します。

MapKit & CoreLocation iOSスキル とは?

このスキルは、iOS/macOSアプリ向けの地図および位置情報機能を実装・改善するための包括的なガイドです。iOS 17+のSwiftUI MapKit (`Map`, `MapContentBuilder`) と最新のCoreLocation async API (`CLLocationUpdate.liveUpdates()`, `CLMonitor`) に焦点を当て、従来のデリゲートベースのコードに代わるモダンで効率的なアプローチを提供します。マーカー・アノテーションの追加、ユーザー位置の追跡、住所と座標の双方向変換(ジオコーディング)、ローカル検索(オートコンプリート含む)、ルート案内・ETA計算、iOS 26+の`PlaceDescriptor`による新しい地図情報取得など、幅広いユースケースをカバー。各機能の具体的なコード例に加え、「よくある間違い」と「レビューチェックリスト」により、実装時のベストプラクティスとバグ防止に役立ちます。主要なマークダウンファイルへの参照も含まれ、開発者が自信を持って地図・位置情報機能を構築できるよう設計されています。

対応~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/JordanCoin/ios-skills-collection/tree/HEAD/skills/dpearson2699--mapkit

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

MapKit

iOS 17+を対象とした、SwiftUIのMapKitと最新のCoreLocation async APIを使用した地図ベースおよび位置情報認識機能を構築します。ビューにはMapとMapContentBuilderを、位置情報のストリーミングにはCLLocationUpdate.liveUpdates()を、ジオフェンシングにはCLMonitorを使用します。

拡張されたMapKitパターンについてはreferences/mapkit-patterns.mdを、CoreLocationパターンについてはreferences/mapkit-corelocation-patterns.mdを参照してください。

目次

ワークフロー

1. マーカーまたはアノテーション付きの地図を追加する

  1. MapKitをインポートします。
  2. オプションでMapCameraPositionバインディングを持つMapビューを作成します。
  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. クエリを設定する前に、ユーザー入力をデバウンス(少なくとも300ms)します。
  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 View (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("Start", coordinate: startCoord)
    Marker("End", 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+)

Place 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.start()を呼び出す(レート制限とUXの問題)。 すべきこと: まずMKLocalSearchCompleterでオートコンプリートを表示し、選択後にのみフル検索を実行します。

してはいけない: ルート案内リクエストを1つのTaskにまとめる — ルートはかなりの時間がかかることがあります。 すべきこと: ユーザーにローディングインジケータを表示し、Taskはバックグラウンドで実行できるようにします。

してはいけない: CLServiceSessionへの参照を保持せず、出し抜けにARCに解放させる。 すべきこと: 位置情報機能の存続期間中は必ず強い参照を保持します。

レビューチェックリスト

  • Info.plistにNSLocationWhenInUseUsageDescriptionが含まれているか?(iOS 18+でCLServiceSessionを使用する場合は常に必要)
  • .always認証は絶対に必要な場合にのみ使用し、段階的にアップグレードしているか?
  • 位置情報の更新が非表示になると適切にキャンセルされるか?(SwiftUIの.taskを使用)
  • すべてのCLLocationUpdateのイテレーションで水平精度をチェックしているか?
  • ユーザーが認証を拒否した場合の処理(設定画面へのリンクなど)があるか?
  • MKLocalSearch.start()の前にオートコンプリートを使用して、無駄なAPIコールを減らしているか?
  • CLGeocoderの結果はユーザーに表示する前にnilチェックしているか?
  • 地図のスタイル(標準、衛星、ハイブリッド)はアプリのコンテキストに適しているか?
  • ルート案内の計算中にローディング状態を表示しているか?
  • ジオフェンシングではCLMonitorを使用し、デリゲートは使用していないか?
  • デバッグ目的で、CLLocationのtimestampとspeedを検証しているか?
  • iOS 17+のコードベースで、非推奨のCLLocationManagerDelegateを避けているか?
  • iOS 26+のAPI(MKGeocodingRequest、PlaceDescriptor、サイクリングルート)は適切な@availableでガードしているか?

参考資料

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.

関連スキル