【SwiftUI】ジオコーディングの実装方法!MapKitで逆ジオコーディング

【SwiftUI】ジオコーディングの実装方法!MapKitで逆ジオコーディング

この記事からわかること

  • Swift UIで地図(Maps)を表示する方法
  • MapKitフレームワークの使い方
  • ジオコーディング/逆ジオコーディングの実装方法
  • CLGeocoderやCLPlacemarkとは?
  • 非同期のジオコーディングをグローバル変数に格納する方法

index

[open]

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

みんなの誕生日

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

posted withアプリーチ

環境

今回はSwiftUIでMapKitフレームワークを使ったジオコーディング/逆ジオコーディングの実装方法をまとめていきます。MapKitの使い方や地図の表示方法については別記事で解説していますので参考にしてください。

ジオコーディング/逆ジオコーディングとは?

Map機能の根幹にもなりますが、住所を入力して地図上で位置を示すことができるのは内部的にジオコーディング(住所→緯度経度)が行われその座標に基づいた場所を特定できるからです。

住所は人向けに、座標は機械向けに定義された位置情報であり、両者が紐付けされていることでどちらにも扱いやすくなっています。

ちなみにGoogleマップではマウスの右クリックで選択した場所の緯度経度が表示されるようになっています。

Googleマップで住所から緯度経度を表示させる方法

Swiftでジオコーディング/逆ジオコーディングを実装するには?

SwiftではMapKitフレームワークをインポートするだけで簡単に地図を表示したり、操作することが可能です。MapKitフレームワークにはCoreLocationフレームワークが組み込まれており、その中に搭載されたCLGeocoderクラスを使用することでジオコーディング/逆ジオコーディングを実装することができます。

CLGeocoder

CLGeocoderクラスは座標と住所などの位置情報を相互に変換してくれるクラスです。メソッドにジオコーディングが可能なgeocodeAddressStringや逆ジオコーディングが可能なreverseGeocodeLocationなどが用意されており住所や座標を渡すだけで様々な位置情報を持ったオブジェクトを返してくれます。

geocodeAddressString

geocodeAddressStringメソッドは非同期でジオコーディングリクエストをサーバーへ送信し結果を取得できるメソッドです。非同期で行われるので結果を取得できるタイミングには注意してください。

イニシャライザは以下の通りです。

geocoder.geocodeAddressString(
  addressString: String, 
  completionHandler: CLGeocodeCompletionHandler <([CLPlacemark]?, Error?) -> Void>
)

addressString

座標などの位置情報へ変換したい住所を渡す引数です。

対象の住所を文字列型(「東京都墨田区押上1丁目1−2」など)として渡すだけでOKです。

completionHandle

ジオコーディングした結果を取得するためのハンドラーブロックです。

CLGeocodeCompletionHandler型はタイプエイリアスなので実際は([CLPlacemark]?, Error?) -> Voidの形式のクロージャになります。

typealias CLGeocodeCompletionHandler = ([CLPlacemark]?, Error?) -> Void

取得に成功した場合は該当の情報を持ったCLPlacemarkオブジェクトが配列形式で参照できるようになります。配列ですが基本的には1つの要素しか格納されていません。

失敗した場合はErrorに該当のエラーメッセージなどが格納されます。また両方とも?がついているのでnilが許容されています。

CLPlacemark

結果の返り値であるCLPlacemarkオブジェクトはNSObjectクラスに準拠したオブジェクトです。

class CLPlacemark : NSObject

CLPlacemarkオブジェクトのプロパティとしてその位置情報に基づいた住所や郵便番号、座標などを取得することができるようになります。

プロパティ名 概略
location CLLocationオブジェクト(※)
name 名前
isoCountryCode ISO国コード
country 国名
postalCode 郵便番号
administrativeArea 都道府県
subAdministrativeArea 郡
locality 市区町村
subLocality 丁番なしの地名
thoroughfare 丁目がある場合、それを含む地名
subThoroughfare 番地
region CLRegionオブジェクト
timeZone TimeZoneオブジェクト
inlandWater 内陸水
ocean 海名
areasOfInterest 目印に関連する関連分野

※:CLLocationオブジェクトは座標や緯度、経度などをプロパティとして保持しているオブジェクトです。

SwiftUIでのジオコーディング

では実際にジオコーディングできるコードを実装してみます。フレームワークはSwiftUIでも問題なく動作させることができるので実際にやってみたいと思います。まずはMapKitのインポートを忘れずに記述しておきます。

import MapKit

続いて管理しやすくするためにConversionSpotクラスを作成し、その中にジオコーディングの処理を記述していきます。

class ConversionSpot {
    // ジオコーディング
    func geocoding(){
            
        let address = "東京都墨田区押上1丁目1−2" // 東京スカイツリーの住所
        let geocoder = CLGeocoder()
        geocoder.geocodeAddressString(address) { (placemarks, error) in
            if let lat = placemarks?.first?.location?.coordinate.latitude {
                print("緯度 : \(lat)")
            }
            if let long = placemarks?.first?.location?.coordinate.longitude {
                print("経度 : \(long)")
            }
        }
    }
}

CLGeocoderクラスが使えるようになっているのでインスタンス化しジオコーディングするためのgeocodeAddressStringメソッドを呼び出します。nilであることも顧慮してオプショナルバインディングで展開し配列の1番目を参照します。

if let lat = placemarks?.first?.location?.coordinate.latitude {
    print("緯度 : \(lat)")
}

緯度と軽度にはlocationプロパティからCLLocationオブジェクトに参照し、その中のcoordinate(= 座標)の中から参照することができます。

このままでは表示できないので下記のようなUI部分を作成し実行してみます。

struct ContentView: View {
    
    var conversionspot = ConversionSpot()
    
    var body: some View {    
        Button {
            conversionspot.geocoding()
        } label: {
            Text("クリック")
        }
    }
}
 // ボタンをクリックするとデバッグエリアに表示される
緯度 : 35.7100625
経度 : 139.8107343

全体のコード

import SwiftUI
import MapKit

class ConversionSpot {
    // ジオコーディング
    func geocoding(){
        let address = "東京都墨田区押上1丁目1−2" // 東京スカイツリーの住所
        let geocoder = CLGeocoder()
        geocoder.geocodeAddressString(address) { (placemarks, error) in
            if let lat = placemarks?.first?.location?.coordinate.latitude {
                print("緯度 : \(lat)")
            }
            if let long = placemarks?.first?.location?.coordinate.longitude {
                print("経度 : \(long)")
            }
        }
    }
}

struct ContentView: View {
    var conversionspot = ConversionSpot()
    var body: some View {
        Button {
            conversionspot.geocoding()
        } label: {
            Text("クリック")
        }   
    }
}

SwiftUIでの逆ジオコーディング

続いて逆ジオコーディングする場合を見てみます。基本的なポイントと流れは先ほどと同様で渡す位置情報と呼び出すメソッドが異なります。

先ほどは住所でしたが逆ジオコーディングでは緯度経度を準備します。ただ数字の羅列を渡すわけではなくCLLocationオブジェクトに倣った形式で渡す必要があるので以下のように定義します。

let location = CLLocation(latitude: 35.7100625, longitude: 139.8107343)

reverseGeocodeLocationメソッドのイニシャライザは以下の通りです。

geocoder.reverseGeocodeLocation(
  location: CLLocation, 
  preferredLocale: Locale?, // ロケールの指定は任意
  completionHandler: CLGeocodeCompletionHandler <([CLPlacemark]?, Error?) -> Void>
)

では実際に実装してみます。先ほどのConversionSpotクラスにregeocodingメソッドを追加してみます。

func regeocoding(){
        
    let location = CLLocation(latitude: 35.7100625, longitude: 139.8107343)

    let geocoder = CLGeocoder()
        CLGeocoder().reverseGeocodeLocation(location) { placemarks, error in
            if let post = placemarks?.first?.postalCode {
                print("郵便番号 : \(post)")
            }
    }
}

忘れないように呼び出すメソッドを変更して実行してみます。

Button {
    conversionspot.regeocoding() // 変更
} label: {
    Text("クリック")
}
 // ボタンをクリックするとデバッグエリアに表示される
郵便番号 : 131-0045

正しい郵便番号を取得することができました。

非同期のジオコーディングをグローバル変数に格納する

非同期で行われるgeocodeAddressStringメソッドなどはデバッグエリアへのプリントは簡単に可能ですが、変数などに格納し、実際にアプリ画面に表示させるとなると取得タイミングがわからない以上容易ではありません。色々模索してみたのですが知識が足りずググってみたところ解決策が「teratail」にありました。

そのコードを参考に、SwiftUIでも動作するようにしたのが以下のコードになります。

import SwiftUI
import MapKit

class ConversionSpot {
    
    func geocode(completionHandler: @escaping (CLLocationCoordinate2D? , Error?) -> (), errorHandler: @escaping () -> ()){
        let testKey:String? = "東京都千代田区千代田1−1"
        
        guard  let searchKey = testKey else {
            errorHandler()
            return
        }
        
        let geocoder = CLGeocoder()
        geocoder.geocodeAddressString(searchKey) { (placemarks, error) in
            guard let unwrapPlacemark = placemarks else {
                DispatchQueue.main.async {
                    // エラーを返す
                    completionHandler(nil, error)
                }
                return
            }
            
            let firstPlacemark = unwrapPlacemark.first!
            let location = firstPlacemark.location!
            
            DispatchQueue.main.async {
                completionHandler(location.coordinate, nil)
            }
        }
    }
}

struct ContentView: View {
    
    @State var  longitude:Double = 2.0
    var conversionspot = ConversionSpot()
    
    var body: some View {
        
        VStack {
            Text("経度:\(longitude)")
            Button {
                conversionspot.geocode { location, error in
                    guard let location = location else {
                        if let error = error {
                            print(error.localizedDescription)
                        } else {
                            print("Unknown error.")
                        }
                        return
                    }
                    
                    longitude = location.longitude
                    // 緯度軽度を表示
                    print(location)

                } errorHandler: {
                    print("error")
                }
            } label: {
                Text("クリック")
        }
        }   
    }
}

ここには私の知らない知識やコードがたくさんありましたのでまた時間をかけて読み解いてみたいと思います。

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

私がSwift UI学習に使用した参考書

Search Box

Sponsor

ProFile

ame

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

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

New Article

index