【SwiftUI】現在地を取得して表示する方法!MapKitで地図アプリ

この記事からわかること

  • Swift UI地図(Maps)を表示する方法
  • MapKitフレームワーク使い方
  • ユーザー現在地を常に取得する方法
  • 現在地を常に取得しMapの表示領域を更新し続ける方法
  • ボタンをクリックごとに表示領域を更新し続ける方法方法
  • CLLocationManager使い方
  • locationManagerメソッド(デリゲートメソッド)とは?
  • 位置情報取得許可/拒否で処理を切り替える

index

[open]

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

今回はSwiftUIでMapKitフレームワークを使った現在地の取得方法をまとめていきます。MapKitの使い方や地図の表示方法については別記事で解説していますので参考にしてください。

【SwiftUI】地図(Maps)を表示するMapKitの使い方!Map()とは?

CoreLocationとMapKitフレームワーク

実装していく機能

  1. 地図を表示
  2. 現在地を常に取得しMapの表示領域を更新し続ける方法
  3. ボタンのクリックごとに位置情報を取得し更新する
  4. 位置情報取得の許可/拒否で処理を切り替える

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)
    }
}
SwiftUIのMapKitで表示した東京スカイツリーの地図

ここでは詳しい説明は省きますが、以下のポイントは理解しておいてください。

地図を表示させるポイント

2.現在地を常に取得しMapの表示領域を更新し続ける方法

今回はSwiftUIを使用した場合の「現在地を常に取得しMapの表示領域を更新し続ける方法」をまとめていきます。

実装していく流れ

  1. プロジェクトのinfoパネルからユーザー利用許可を追加する
  2. 位置情報を管理するクラスの生成
  3. デリゲートメソッドの実装

プロジェクトのinfoパネルからユーザー利用許可を追加する

アプリ内でユーザーの現在地を取得して使用する場合はユーザーの許可が必要になります。アプリ起動時にポップアップで確認事項が出るようにinfoパネルから設定を追加しておきます。

プロジェクトを開いたら左のナビゲータエリアから「プロジェクト名」をクリック、「info」タブに切り替えます。

その中の「Custom iOS Target Properties」の中に新たな項目を追加します。どこでも良いのですが「Bundle Category」の横にある「」をクリックすると以下のように新たな「Key」が追加できるようになるので「Privacy - Location When In Use Usage Description」を選択します。

Swift/Xcodeでプロジェクトのinfoパネルからユーザー利用許可を追加する

右側の「Value」にはポップアップで表示したい確認文を記述しておいてください。追加するとアプリ起動時に以下のように許可を求めるポップアップが表示されるようになります。

Swift/Xcodeでプロジェクトのinfoパネルからユーザー利用許可を追加する

位置情報を管理するクラスの作成

続いて新規のファイルを作成し、位置情報を管理するクラスを作成していきます。「File」>「new」>「File...」から「Cocoa Touch Class」を選択し「MapModels.swift」ファイルを作成しておきます。

Swift/XcodeのMapKitを使って現在地を取得するアプリを作成する様子

既存のクラスを書き換えLocationManagerクラスとしておきます。スーパクラスはNSObject、プロトコルはObservableObjectCLLocationManagerDelegateの2つに準拠させておきます。


import MapKit
class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
  // 処理を記述していく
}

各スーパクラス/プロトコルの役割

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メソッドの役割

ユーザートラッキングモードを追従モードにして表示

最後に表示部分を調整していきます。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
        )
    }
}

あとはこのメソッドを適当なボタンのクリック時に実行されるようにすれば「ボタンのクリックごとに位置情報を取得し更新する」機能の実装は完了です。

シミュレーターで現在地の更新を試す方法

アプリ内で現在位置を使用する場合プレビューや簡易的なライブプレビューでは現在地の更新とともに地図が変更する様子は確認できません

動作確認をするにはツールバーのRunボタン( )をクリックしてシミュレーターを起動します。

シミュレーターが起動したら上のメニューから「Features」>「Location」>「Freeway Drive」をクリックするとシミュレーターのユーザーの位置表示が動き出し、正常に動作していることが確認できます。

Swift/Xcodeでシミュレーターを使用して現在位置情報の更新を試す方法

シミュレーターは「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学習に使用した参考書

searchbox

スポンサー

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑
今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index