【SwiftUI】ジオコーディングの実装方法!MapKitで逆ジオコーディング
この記事からわかること
- Swift UIで地図(Maps)を表示する方法
- MapKitフレームワークの使い方
- ジオコーディング/逆ジオコーディングの実装方法
- CLGeocoderやCLPlacemarkとは?
- 非同期のジオコーディングをグローバル変数に格納する方法
index
[open]
\ アプリをリリースしました /
環境
- Xcode:16.0
- iOS:18.0
- Swift:5.9
- macOS:Sonoma 14.6.1
今回は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学習に使用した参考書





