mcpField
M
mapkit-location

mapkit-location

github.com/dpearson2699開発1.2k461 回インストール13 週間前に更新
リポジトリ

mapkit-location について

mapkit-locationは、SwiftUI MapKitと最新のCoreLocation APIで地図・位置機能を開発するためのSwiftスキルです。開発者向けに、地図、位置追跡、ジオコーディング、場所検索、経路案内、実装レビューを解説します。

主な機能

  1. 01マーカーやオーバーレイを使うSwiftUIマップ
  2. 02非同期の位置追跡とジオフェンス
  3. 03住所のジオコーディングと場所検索
  4. 04経路計算と地図表示
  5. 05実装チェックリストと注意点

活用シーン

  1. 01場所のマーカー付き地図を構築
  2. 02画面表示中にユーザー位置を追跡
  3. 03オートコンプリートで場所を検索
  4. 04移動経路を計算して表示

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/dpearson2699/swift-ios-skills --skill mapkit-location

インストール後、Claude Code で/mapkit-locationを入力して読み込まれたか確認します。

同梱ファイルreferences/corelocation-patterns.mdreferences/mapkit-patterns.md

詳細

MapKit と CoreLocation

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

MapKit の詳しいパターンについては references/mapkit-patterns.md を、CoreLocation のパターンについては references/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() を await します。
  4. MapPolyline(route.polyline) で経路を描画します。
5. 既存の地図/位置情報コードをレビューする

このファイルの最後にあるレビュー用チェックリストを確認します。

SwiftUI のマップビュー(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()
        }
    }
}
マーカーと注釈
// Balloon marker -- simplest way to pin a location
Marker("Cafe", systemImage: "cup.and.saucer.fill", coordinate: cafeCoord)
    .tint(.brown)

// Annotation -- custom SwiftUI view at a coordinate
Annotation("You", coordinate: userCoord, anchor: .bottom) {
    Image(systemName: "figure.wave")
        .padding(6)
        .background(.blue.gradient, in: .circle)
        .foregroundStyle(.white)
}
オーバーレイ:ポリライン、ポリゴン、円
Map {
    // Polyline from coordinates
    MapPolyline(coordinates: routeCoords)
        .stroke(.blue, lineWidth: 4)

    // Polygon (area highlight)
    MapPolygon(coordinates: parkBoundary)
        .foregroundStyle(.green.opacity(0.3))
        .stroke(.green, lineWidth: 2)

    // Circle (radius around a point)
    MapCircle(center: storeCoord, radius: 500)
        .foregroundStyle(.red.opacity(0.15))
        .stroke(.red, lineWidth: 1)
}
カメラ位置

MapCameraPosition は地図に表示する内容を制御します。ユーザーが操作できるようにし、プログラムからカメラを移動できるようにバインドします。

// Center on a region
@State private var position: MapCameraPosition = .region(
    MKCoordinateRegion(
        center: CLLocationCoordinate2D(latitude: 37.334, longitude: -122.009),
        span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
    )
)

// Follow user location
@State private var position: MapCameraPosition = .userLocation(fallback: .automatic)

// Specific camera angle (3D perspective)
@State private var position: MapCameraPosition = .camera(
    MapCamera(centerCoordinate: applePark, distance: 1000, heading: 90, pitch: 60)
)

// Frame specific items
position = .item(MKMapItem.forCurrentLocation())
position = .rect(MKMapRect(...))
地図のスタイル
.mapStyle(.standard)                                        // Default road map
.mapStyle(.standard(elevation: .realistic, showsTraffic: true))
.mapStyle(.imagery)                                         // Satellite
.mapStyle(.imagery(elevation: .realistic))                  // 3D satellite
.mapStyle(.hybrid)                                          // Satellite + labels
.mapStyle(.hybrid(elevation: .realistic, showsTraffic: true))
地図の操作モード
.mapInteractionModes(.all)           // Default: pan, zoom, rotate, pitch
.mapInteractionModes(.pan)           // Pan only
.mapInteractionModes([.pan, .zoom])  // Pan and zoom
.mapInteractionModes([])             // Static map (no interaction)
地図上の選択
@State private var selectedMarker: MKMapItem?

Map(selection: $selectedMarker) {
    ForEach(places) { place in
        Marker(place.name, coordinate: place.coordinate)
            .tag(place.mapItem)     // Tag must match selection type
    }
}
.onChange(of: selectedMarker) { _, newValue in
    guard let item = newValue else { return }
    // React to selection
}

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 }
                // Filter by horizontal accuracy
                guard location.horizontalAccuracy < 50 else { continue }
                await MainActor.run {
                    self.currentLocation = location
                }
            }
        }
    }

    func stopTracking() {
        updateTask?.cancel()
        updateTask = nil
    }
}
CLServiceSession(iOS 18 以降)

機能の使用期間に必要な認証要件を宣言します。位置情報サービスが必要な間は、セッションへの参照を保持してください。

// When-in-use authorization with full accuracy preference
let session = CLServiceSession(
    authorization: .whenInUse,
    fullAccuracyPurposeKey: "NearbySearchPurpose"
)
// Hold `session` as a stored property; release it when done.

iOS 18 以降では、明示的に作成しない場合、CLLocationUpdate.liveUpdates() と CLMonitor は暗黙的な CLServiceSession を使用します。.always 認証または完全な精度が必要な場合は、セッションを明示的に作成してください。

認証フロー
// Info.plist keys (required):
// NSLocationWhenInUseUsageDescription
// NSLocationAlwaysAndWhenInUseUsageDescription (only if .always needed)

// Check authorization and guide user to Settings when denied
struct LocationPermissionView: View {
    @Environment(\.openURL) private var openURL

    var body: some View {
        ContentUnavailableView {
            Label("Location Access Denied", systemImage: "location.slash")
        } description: {
            Text("Enable location access in Settings to use this feature.")
        } actions: {
            Button("Open Settings") {
                if let url = URL(string: UIApplication.openSettingsURLString) {
                    openURL(url)
                }
            }
        }
    }
}

ジオコーディング

CLGeocoder(iOS 8 以降)
let geocoder = CLGeocoder()

// Forward geocoding: address string -> coordinates
let placemarks = try await geocoder.geocodeAddressString("1 Apple Park Way, Cupertino")
if let location = placemarks.first?.location {
    print(location.coordinate) // CLLocationCoordinate2D
}

// Reverse geocoding: coordinates -> placemark
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
}

// Search by natural language query within a region
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)
}
到着予定時刻の計算
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 ?? "Unknown"。

避けてください: キー入力のたびに MKLocalSearchCompleter クエリを実行すること。 推奨: .task(id: searchText) と Task.sleep(for: .milliseconds(300)) を組み合わせてデバウンスします。

避けてください: 位置情報の使用許可が拒否されたときに、何も通知せず失敗させること。 推奨: .denied の状態を検出し、「設定」へのディープリンクを含むアラートを表示します。

避けてください: ジオコーディングが必ず成功すると想定すること — 結果が空の場合やネットワークエラーを処理してください。

レビューチェックリスト

  • Info.plist に、具体的な理由を記載した NSLocationWhenInUseUsageDescription がある
  • 使用許可の拒否を「設定」へのディープリンクで処理している
  • 不要なときに CLLocationUpdate のタスクをキャンセルしている(バッテリー対策)
  • ユースケースに適した位置情報の精度を使用している
  • マップのアノテーションで、安定した ID を持つ Identifiable データを使用している
  • ジオコーディングのエラー(ネットワーク障害、結果なし)を処理している
  • 検索補完の入力をデバウンスしている
  • CLMonitor の条件数を 20 個までに制限し、インスタンスを保持している
  • バックグラウンドでの位置情報に CLBackgroundActivitySession を使用している
  • VoiceOver でマップをテストしている
  • マップのアノテーション用ビューモデルと位置情報の UI 更新を @MainActor で分離している

参考資料

  • references/mapkit-patterns.md — マップの設定、アノテーション、検索、経路、クラスタリング、Look Around、スナップショット。
  • references/corelocation-patterns.md — CLLocationUpdate、CLMonitor、CLServiceSession、バックグラウンドでの位置情報、テスト。

コメント