MapKit
iOS 17+を対象とした、SwiftUIのMapKitと最新のCoreLocation async APIを使用した地図ベースおよび位置情報認識機能を構築します。ビューにはMapとMapContentBuilderを、位置情報のストリーミングにはCLLocationUpdate.liveUpdates()を、ジオフェンシングにはCLMonitorを使用します。
拡張されたMapKitパターンについてはreferences/mapkit-patterns.mdを、CoreLocationパターンについてはreferences/mapkit-corelocation-patterns.mdを参照してください。
目次
- ワークフロー
- SwiftUI Map View (iOS 17+)
- CoreLocation モダンAPI
- ジオコーディング
- 検索
- ルート案内
- PlaceDescriptor (iOS 26+)
- よくある間違い
- レビューチェックリスト
- 参考資料
ワークフロー
1. マーカーまたはアノテーション付きの地図を追加する
MapKitをインポートします。- オプションで
MapCameraPositionバインディングを持つMapビューを作成します。 MapContentBuilderクロージャ内にMarker、Annotation、MapPolyline、MapPolygon、またはMapCircleを追加します。.mapStyle()で地図のスタイルを設定します。.mapControls { }で地図コントロールを追加します。selection:バインディングで選択を処理します。
2. ユーザーの位置を追跡する
Info.plistにNSLocationWhenInUseUsageDescriptionを追加します。- iOS 18+では、
CLServiceSessionを作成して認証を管理します。 Task内でCLLocationUpdate.liveUpdates()をイテレートします。- UIを更新する前に、距離または精度で更新をフィルタリングします。
- 位置追跡が不要になったらタスクを停止します。
3. 場所を検索する
- オートコンプリートの候補には
MKLocalSearchCompleterを設定します。 - クエリを設定する前に、ユーザー入力をデバウンス(少なくとも300ms)します。
- 選択された補完結果を
MKLocalSearch.Requestに変換して、完全な結果を取得します。 - 結果をマーカーまたはリストとして表示します。
4. ルート案内を取得して表示する
- 出発地と目的地の
MKMapItemを使用してMKDirections.Requestを作成します。 transportTypeを設定します(.automobile、.walking、.transit、.cycling)。MKDirections.calculate()を待機します。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でガードしているか?
参考資料
- MapKit Patterns — 高度な地図パターン(クラスタリング、カスタムタイル、3Dビュー)
- CoreLocation Patterns — 位置情報パターン(ジオフェンシング、バックグラウンド更新、iBeacon)
- Apple MapKit Documentation
- Apple CoreLocation Documentation