【SwiftUI】ジオコーディングの実装方法!MapKitで逆ジオコーディング
この記事からわかること
- Swift UIで地図(Maps)を表示する方法
- MapKitフレームワークの使い方
- ジオコーディング/逆ジオコーディングの実装方法
- CLGeocoderやCLPlacemarkとは?
- 非同期のジオコーディングをグローバル変数に格納する方法
index
[open]
\ アプリをリリースしました /
友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-
posted withアプリーチ
今回はSwiftUIでMapKitフレームワークを使ったジオコーディング/逆ジオコーディングの実装方法をまとめていきます。MapKitの使い方や地図の表示方法については別記事で解説していますので参考にしてください。
ジオコーディング/逆ジオコーディングとは?
- ジオコーディング :住所→緯度経度 に変換
- 逆ジオコーディング:緯度経度→住所 に変換すること
Map機能の根幹にもなりますが、住所を入力して地図上で位置を示すことができるのは内部的にジオコーディング(住所→緯度経度)が行われその座標に基づいた場所を特定できるからです。
住所は人向けに、座標は機械向けに定義された位置情報であり、両者が紐付けされていることでどちらにも扱いやすくなっています。
ちなみに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
が許容されています。
おすすめ記事:Optional(オプショナル)型と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学習に使用した参考書