【SwiftUI】現在地を取得して表示する方法!MapKitで地図アプリ
この記事からわかること
- Swift UIで地図(Maps)を表示する方法
- MapKitフレームワークの使い方
- ユーザーの現在地を常に取得する方法
- 現在地を常に取得しMapの表示領域を更新し続ける方法
- ボタンをクリックごとに表示領域を更新し続ける方法方法
- CLLocationManagerの使い方
- locationManagerメソッド(デリゲートメソッド)とは?
- 位置情報取得の許可/拒否で処理を切り替える
index
[open]
\ アプリをリリースしました /
友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-
posted withアプリーチ
環境
- Xcode:16.0
- iOS:18.0
- Swift:5.9
- macOS:Sonoma 14.6.1
今回はSwiftUIでMapKitフレームワークを使った現在地の取得方法をまとめていきます。MapKitの使い方や地図の表示方法については別記事で解説していますので参考にしてください。
【SwiftUI】地図(Maps)を表示するMapKitの使い方!Map()とは?
CoreLocationとMapKitフレームワーク
実装していく機能
- 地図を表示
- 現在地を常に取得しMapの表示領域を更新し続ける方法
- ボタンのクリックごとに位置情報を取得し更新する
- 位置情報取得の許可/拒否で処理を切り替える
SwiftUIで現在地を取得するにはCoreLocationフレームワーク
のCLLocationManager
クラスを使用します。地図を表示、操作できるMapKitフレームワーク
にはCoreLocationフレームワーク
が組み込まれているのでMapKitをインポートするだけで地図の表示から現在地の取得、さらにはジオコーディング(住所→座標への変換)などが簡単に行えるようになります。
import MapKit // MapKitフレームワークのインポート
1.SwiftUIで地図の表示方法をおさらい
一番初めにMapKitフレームワークを使用して地図を表示する際のコードとポイントをおさらいしておきます。
import MapKit
struct ContentView: View {
@State var region = MKCoordinateRegion(
center : CLLocationCoordinate2D(
latitude: 35.710057714926265, // 緯度
longitude: 139.81071829999996 // 経度
),
latitudinalMeters: 1000.0, // 南北
longitudinalMeters: 1000.0 // 東西
)
var body: some View {
// 地図を表示
Map(coordinateRegion: $region)
.edgesIgnoringSafeArea(.bottom)
}
}
ここでは詳しい説明は省きますが、以下のポイントは理解しておいてください。
地図を表示させるポイント
- MKCoordinateRegion構造体と各プロパティ
- 座標/緯度/経度
- MapView
2.現在地を常に取得しMapの表示領域を更新し続ける方法
今回はSwiftUIを使用した場合の「現在地を常に取得しMapの表示領域を更新し続ける方法」をまとめていきます。
実装していく流れ
- プロジェクトのinfoパネルからユーザー利用許可を追加する
- 位置情報を管理するクラスの生成
- デリゲートメソッドの実装
プロジェクトのinfoパネルからユーザー利用許可を追加する
アプリ内でユーザーの現在地を取得して使用する場合はユーザーの許可が必要になります。アプリ起動時にポップアップで確認事項が出るようにinfoパネルから設定を追加しておきます。
プロジェクトを開いたら左のナビゲータエリアから「プロジェクト名」をクリック、「info」タブに切り替えます。
その中の「Custom iOS Target Properties」の中に新たな項目を追加します。どこでも良いのですが「Bundle Category」の横にある「」をクリックすると以下のように新たな「Key」が追加できるようになるので「Privacy - Location When In Use Usage Description」を選択します。
右側の「Value」にはポップアップで表示したい確認文を記述しておいてください。追加するとアプリ起動時に以下のように許可を求めるポップアップが表示されるようになります。
位置情報を管理するクラスの作成
続いて新規のファイルを作成し、位置情報を管理するクラスを作成していきます。「File」>「new」>「File...」から「Cocoa Touch Class」を選択し「MapModels.swift」ファイルを作成しておきます。
既存のクラスを書き換えLocationManager
クラスとしておきます。スーパクラスはNSObject
、プロトコルはObservableObject
、CLLocationManagerDelegate
の2つに準拠させておきます。
import MapKit
class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
// 処理を記述していく
}
各スーパクラス/プロトコルの役割
- NSObject:イニシャライザをオーバーライド
- ObservableObject:クラスの変更を観測
- CLLocationManagerDelegate:locationManager(_:didFailWithError:)を実装
CLLocationManagerクラスの設定
まずは現在地を取得するための準備をしていきます。クラス内でCLLocationManager
クラスをインスタンス化し、スーパクラスのイニシャライザをオーバーライド(元のクラスのイニシャライザは変更せず引き継ぐ)して処理を追加していきます。
class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
// CLLocationManagerをインスタンス化
let manager = CLLocationManager()
override init() {
super.init() // スーパクラスのイニシャライザを実行
manager.delegate = self // 自身をデリゲートプロパティに設定
manager.requestWhenInUseAuthorization() // 位置情報を利用許可をリクエスト
manager.desiredAccuracy = kCLLocationAccuracyBest // 最高精度の位置情報を要求
manager.distanceFilter = 3.0 // 更新距離(m)
manager.startUpdatingLocation()
}
}
イニシャライザ内ではインスタンス化したCLLocationManager
クラスのプロパティやメソッドを呼び出して値を設定していきます。このクラスを操作することで位置情報に関するイベントを操作することが可能になります。
class CLLocationManager : NSObject
delegateプロパティ
delegate
プロパティにはCLLocationManagerDelegateプロトコルに準拠したクラスを指定しなければなりません。今回の場合はデリゲートの「処理を任せるクラス」と「処理を任されるクラス」が同じなので自分自身(クラス)を指すselfを格納します。
weak var delegate: CLLocationManagerDelegate? { get set }
requestWhenInUseAuthorizationメソッド
利用許可のアラートを表示させるメソッドです。WhenInUseAuthorization
の言葉通り「使用中のみの許可」を申請していますがポップアップには「Appの使用中は許可」と「1度だけ許可」の2つが表示されます。表示させるアラートに関するメソッドは以下の通りです。
requestAlwaysAuthorization() : 「常に許可」をリクエスト
requestWhenInUseAuthorization(): 「Appの使用中は許可」をリクエスト
requestAlwaysAuthorization
を使用する場合は「infoパネルからユーザー利用許可を追加」で「Privacy Location Always Usage Description」項目を追加してください。
desiredAccuracyプロパティ
位置情報の正確さを設定できるプロパティです。
kCLLocationAccuracyBest // 最高精度
kCLLocationAccuracyKilometer // キロメートル単位の精度
kCLLocationAccuracyNearestTenMeters // 10メートル単位の精度
精度が高い位置情報を要求する場合はデバイスのバッテリー消費も嵩むので適切な値を指定することが重要になります。
distanceFilterプロパティ
位置情報の更新頻度(メートル単位)を数値で指定するプロパティです。
var distanceFilter: CLLocationDistance { get set }
定義を見ると設定値の型はCLLocationDistance
となっていますがこれはDouble
型のタイプエイリアスです。
typealias CLLocationDistance = Double
startUpdatingLocationメソッド
ユーザーの現在地を監視し追跡をスタートさせるメソッドです。
位置情報が更新されるとlocationManager(_:didUpdateLocations:)
メソッドが呼び出されデリゲートへと情報が譲渡されていきます。
領域を更新するデリゲートメソッド
続いてデリゲートメソッド内に領域を更新する処理を実装していきます。先ほどstartUpdatingLocationメソッド
から呼び出されるlocationManager(_:didUpdateLocations:)
がデリゲートメソッドになります。
class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
let manager = CLLocationManager()
// 追加 地図を表示するための領域を保持
// 更新のたびに変化するので@Publishedを付与して観測
@Published var region = MKCoordinateRegion()
override init() {
// 処理
}
// 領域の更新をするデリゲートメソッド
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
// 配列の最後に最新のロケーションが格納される
// map関数を使って全要素にアクセス map{ $0←要素に参照 }
locations.last.map {
let center = CLLocationCoordinate2D(
latitude: $0.coordinate.latitude,
longitude: $0.coordinate.longitude)
// 地図を表示するための領域を再構築
region = MKCoordinateRegion(
center: center,
latitudinalMeters: 1000.0,
longitudinalMeters: 1000.0
)
}
}
}
locationManagerメソッドとは?
locationManager
メソッドはCLLocationManagerDelegate
プロトコルにあらかじめ定義されているメソッド(デリゲートメソッド)です。CLLocationManagerDelegate
定義を見ると「定義を必須としないため」のoptional
キーワードがついているので未定義でもエラーは発生しません。
public protocol CLLocationManagerDelegate : NSObjectProtocol {
@available(iOS 6.0, *)
optional func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation])
@available(iOS 3.0, *)
optional func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading)
.
.
}
定義を見ると数多くの同名のメソッドが定義されています。これらの違いは呼び出されるタイミングと引数で受け取れる値の違いです。
種類 | 概要 | 引数 |
---|---|---|
locationManager (_:didUpdateLocations locations: [CLLocation]) |
位置情報が更新されたタイミング | 位置データを含むオブジェクトの配列 [CLLocation] |
locationManagerDidChangeAuthorization(_:) | 位置情報と認証状態が更新されたタイミング | - |
locationManager(_:didUpdateHeading newHeading: CLHeading) | 進行方向が更新されたタイミング | デバイスの方位角(方向) |
locationManagerメソッドの役割
- デリゲートメソッド
- CLLocationManagerDelegateプロトコルに定義
- 同名のメソッドが複数ある
- optionalが指定されており定義は必須でない
- 今回は位置情報が更新されるたびに実行される
- 明示的に呼び出しはせずstartUpdatingLocationメソッドが自動で呼び出す
ユーザートラッキングモードを追従モードにして表示
最後に表示部分を調整していきます。LocationManager
クラスの領域情報を保持するregion
プロパティは@Published
で更新を観測しているので適応されるように@ObservedObject
をつけた変数にインスタンス化します。
MapView
の引数に領域を保持するregion
とマップ上にユーザーの場所を表示させるための
Bool値
、位置情報の更新に対してのトラッキングモードを指定します。
struct ContentView: View {
@ObservedObject var manager = LocationManager()
// ユーザートラッキングモードを追従モードにするための変数を定義
@State var trackingMode = MapUserTrackingMode.follow
var body: some View {
Map(coordinateRegion: $manager.region,
showsUserLocation: true, // マップ上にユーザーの場所を表示するオプションをBool値で指定
userTrackingMode: $trackingMode) // マップがユーザーの位置情報更新にどのように応答するかを決定
.edgesIgnoringSafeArea(.bottom)
}
}
これで「現在地を常に取得しMapの表示領域を更新し続ける機能」の実装が完了しました。
シミュレーション方法に関しては後述しています。
「シミュレーターで現在地の更新を試す方法」へジャンプする
3.ボタンのクリックごとに位置情報を取得し更新する
続いて「ボタンのクリックごとに位置情報を取得し更新する」方法をみていきたいと思います。
ユーザーの位置情報はCLLocationManager
クラスのlocation
プロパティに格納されています。位置情報の取得が許可されていない場合はnil
が格納されます。
@NSCopying var location: CLLocation? { get }
新しく定義したreloadRegion
メソッドの中にオプショナルバインディングを使用して変数に格納します。位置情報はCLLocation
型なので緯度や経度にアクセスすることができるので同様の手順でregion
を作成していきます。
func reloadRegion (){
// オプショナルバインディング
if let location = manager.location {
let center = CLLocationCoordinate2D(
latitude: location.coordinate.latitude,
longitude: location.coordinate.longitude)
region = MKCoordinateRegion(
center: center,
latitudinalMeters: 1000.0,
longitudinalMeters: 1000.0
)
} else {
print("位置情報の許可がされていない")
}
}
あとはこのメソッドを適当なボタンのクリック時に実行されるようにすれば「ボタンのクリックごとに位置情報を取得し更新する」機能の実装は完了です。
シミュレーターで現在地の更新を試す方法
アプリ内で現在位置を使用する場合プレビューや簡易的なライブプレビューでは現在地の更新とともに地図が変更する様子は確認できません。
動作確認をするにはツールバーのRunボタン( )をクリックしてシミュレーターを起動します。
シミュレーターが起動したら上のメニューから「Features」>「Location」>「Freeway Drive」をクリックするとシミュレーターのユーザーの位置表示が動き出し、正常に動作していることが確認できます。
シミュレーターは「SE」や「iPhone 11」などでは問題なく動作しましたが、「iPhone 13」では以下のようなエラーが発生し起動できなかったので注意してください。
CLLocationManager(<CLLocationManager: 0x600000231c30>) for <MKCoreLocationProvider: 0x600003238cf0> did fail with error: Error Domain=kCLErrorDomain Code=1 "(null)"
4.位置情報取得の許可/拒否で処理を切り替える
ユーザーが位置情報の取得の承認申請に対して「許可または拒否」した場合に処理を分岐させる方法をみていきます。
悩み
位置情報を拒否されるとマップが機能しなくなる(マップの初期表示位置に現在地を渡すため)
解決方法
拒否された場合は仮の初期表示位置を渡す
今回は位置情報(現在地)の取得が拒否された時にマップ機能が使えるようにデフォルトの位置情報を渡すようにしたいと思います。
locationManagerDidChangeAuthorizationメソッド
optional func locationManagerDidChangeAuthorization(_ manager: CLLocationManager)
実装するには位置情報の認証状態が変更された時に呼び出されるlocationManagerDidChangeAuthorization
メソッドを使用します。このメソッドは変更された時に実行されるだけなのでその中に認証状態により処理を分岐させる処理を記述する必要があります。
ユーザー選択した認証状態はauthorizationStatus
プロパティで取得することができます。このプロパティの値は列挙型CLAuthorizationStatus
に定義されている値になります。
var authorizationStatus: CLAuthorizationStatus { get }
列挙型CLAuthorizationStatus
列挙型CLAuthorizationStatus
には以下の値が定義されています。
enum CLAuthorizationStatus : Int32, @unchecked Sendable {
// 未選択
case notDetermined = 0
// 位置情報サービスが許可されていない
case restricted = 1
// 拒否または設定が無効
case denied = 2
// 常に許可
case authorizedAlways = 3
// 使用中のみ許可
case authorizedWhenInUse = 4
// 許可 Deprecated:非推奨 iOS 2.0–8.0
static var authorized: CLAuthorizationStatus
}
以下のようにlocationManagerDidChangeAuthorization
メソッド内でauthorizationStatus
プロパティを参照し列挙型の値によって処理を分岐させることで解決できました。
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager){
let guarded = manager.authorizationStatus.rawValue
if guarded == 2 {
// 拒否された場合に実行される処理を定義
print("拒否された")
let center = CLLocationCoordinate2D(
latitude: 35.709152712026265,
longitude: 139.80771829999996)
region = MKCoordinateRegion(
center: center,
latitudinalMeters: 1000.0,
longitudinalMeters: 1000.0
)
}
}
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。
私がSwift UI学習に使用した参考書