mcpField
M
mapkit

mapkit

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

mapkit について

MapKitは、MapKitとCoreLocationのモダンな非同期APIで地図・位置情報機能を開発するSwiftUI開発者向けのパターン集です。地図上の注釈、位置情報更新、場所検索、ジオコーディング、経路検索を扱います。

主な機能

  1. 01地図、注釈、オーバーレイ
  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

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

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

evals/
evals.json
references/
mapkit-corelocation-patterns.md
mapkit-patterns.md

詳細

MapKit

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

マップの完全なセットアップ、検索、ルート、Look Around、スナップショット、または iOS 26 の場所 API が必要な場合は、references/mapkit-patterns.md をお読みください。タスクに位置情報の更新ライフサイクル、ジオフェンシング、バックグラウンド位置情報、テスト、プライバシーキーが関係する場合は、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() を 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(...))
マップスタイル

デフォルトは .standard とし、機能上必要な場合にのみ .imagery または .hybrid、リアルな標高、交通情報、ポイントオブインタレストのフィルタリングを選択します。マップビューの完全なセットアップを参照してください。

マップの操作モード

インタラクティブなマップには .all を使用します。ジェスチャーの調整を意図する場合にのみモードを制限し、静的な埋め込みマップには [] を使用します。リストまたは ScrollView 内のマップを参照してください。

マップの選択
@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 が返されます。iOS 18 以降では、認証の拒否、位置情報サービス全体の無効化、位置情報の取得不可、使用中の条件不足などの診断状態に対して、永久に待機し続けるのではなく、状態の低下をユーザーに示す経路を用意します。機能からキャンセルできるようタスクを保持し、マップ UI やバックグラウンド処理に反映する前に、無効、不正確、古い、または使用できない移動データを除外します。

import CoreLocation

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

    func startTracking() {
        updateTask = Task {
            do {
                let updates = CLLocationUpdate.liveUpdates()
                for try await update in updates {
                    guard let location = update.location else { continue }
                    // Filter by horizontal accuracy
                    guard location.horizontalAccuracy >= 0,
                          location.horizontalAccuracy < 50 else { continue }
                    currentLocation = location
                }
            } catch is CancellationError {
                // Expected when tracking stops.
            } catch {
                currentLocation = nil
            }
        }
    }

    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 14 以降)

リクエストの移動手段の種類として .cycling を使用してください。完全な例については、 自転車の経路をご覧ください。

PlaceDescriptor(iOS 26 以降)

Place ID がなくても、座標や住所から詳細な場所の参照を作成できます。import GeoToolbox が必要です。

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
}

よくある間違い

しないでください: 最初から「常に許可」の認証をリクエストする。 してください: まずは使用中のみの認証から始めます。iOS 18 以降では、機能の利用期間中は CLServiceSession を保持します。終了後にシステムによる再起動が必要なバックグラウンド機能に限り、.always をリクエストしてください。

しないでください: iOS 17 以降で、単純な位置情報の取得に CLLocationManagerDelegate を使用する。 してください: より簡潔で読みやすいコードにするため、非同期ストリーム CLLocationUpdate.liveUpdates() を使用します。

しないでください: 拒否、全体での拒否、位置情報の利用不可など、CLLocationUpdate の診断情報を無視する。 してください: 機能を停止または縮退させ、設定への誘導などの復旧用 UI を表示し、検索や手動操作のフローは引き続き利用できるようにします。

しないでください: 地図やビューがなくなった後も、所有者のないタスクで liveUpdates() を実行し続ける。 してください: 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 で分離する

参考資料

コメント