[はじめてのiOSアプリ]xcodeで地図アプリを作成（その８）

# はじめに

iOSアプリを作ってみたいけど
何から始めて良いのかわからない

とりあえず、
「やってみました」記事を参考に
地図アプリを真似てみようと思う

という記事の８回目です。

今回は、地名の取得と表示(Reverse Geocoding)までします。

# 位置情報から地名に変換するサービス
- 今回使うのは [Finds.jp](https://www.finds.jp/rgeocode/)
  - 【なぜ？】
     - 利用する準備が簡単（登録不要で利用可能）
         - やっぱり「やってみました」は簡単にやってみることができるのは重要！
     - 今回は、日本国内の地名に変換できれば十分だから
- キーワード「reverse geocoding」で検索してみると、いろいろ見つかる
  - 気になる人は、いろいろ試してみると良いですよ

## 簡単な Finds.jp の使い方
- ブラウザで以下のアドレスを表示してみる
  - [https://www.finds.jp/ws/rgeocode.php?lat=35.6804725&lon=139.7661837](https://www.finds.jp/ws/rgeocode.php?lat=35.6804725&lon=139.7661837)
  - 以下のよう内容が得られる（ブラウザに表示される）
     - 「東京都」、「千代田区」、「丸の内一丁目」との情報が得られた

     ```
<?xml version="1.0" encoding="UTF-8"?>
<rgeocode xmlns="http://aginfo.cgk.affrc.go.jp/ts" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <status>200</status>
  <result>
    <prefecture>
      <pcode>13</pcode>
      <pname>東京都</pname>
    </prefecture>
    <municipality>
      <mname>千代田区</mname>
      <mcode>13101</mcode>
    </municipality>
    <local>
      <section>丸の内一丁目</section>
      <homenumber>9</homenumber>
      <distance>128.53601336433</distance>
      <latitude>35.681252</latitude>
      <longitude>139.767235</longitude>
    </local>
  </result>
  <argument>
    <latitude>35.6804725</latitude>
    <longitude>139.7661837</longitude>
    <localradius>500</localradius>
    <localmax>1</localmax>
  </argument>
  <meta name="thanks" content="このサービスは 国土交通省 提供 国土数値情報(行政区域) を利用しています" />
  <meta name="thanks" content="このサービスは 国土交通省 提供 街区レベル位置参照情報および大字・町丁目レベル位置参照情報 を利用しています" />
</rgeocode>
    ```

- JSON の方が良いので、ブラウザで以下のアドレスを表示してみる
  - [https://www.finds.jp/ws/rgeocode.php?lat=35.6804725&lon=139.7661837&json](https://www.finds.jp/ws/rgeocode.php?lat=35.6804725&lon=139.7661837&json)
  - 以下のよう内容が得られる（ブラウザに表示される）
     - 改行も入っていないので読みにくいけど、プログラムで扱うので気にしない

    ```
{"status":200,"result":{"prefecture":{"pcode":13,"pname":"\u6771\u4eac\u90fd"},"municipality":{"mname":"\u5343\u4ee3\u7530\u533a","mcode":13101},"local":[{"section":"\u4e38\u306e\u5185\u4e00\u4e01\u76ee","homenumber":"9","distance":128.53601336433,"latitude":35.681252,"longitude":139.767235}]},"argument":{"latitude":35.6804725,"longitude":139.7661837,"localradius":500,"localmax":1},"meta":[{"name":"thanks","content":"\u3053\u306e\u30b5\u30fc\u30d3\u30b9\u306f \u56fd\u571f\u4ea4\u901a\u7701 \u63d0\u4f9b \u56fd\u571f\u6570\u5024\u60c5\u5831(\u884c\u653f\u533a\u57df) \u3092\u5229\u7528\u3057\u3066\u3044\u307e\u3059"},{"name":"thanks","content":"\u3053\u306e\u30b5\u30fc\u30d3\u30b9\u306f \u56fd\u571f\u4ea4\u901a\u7701 \u63d0\u4f9b \u8857\u533a\u30ec\u30d9\u30eb\u4f4d\u7f6e\u53c2\u7167\u60c5\u5831\u304a\u3088\u3073\u5927\u5b57\u30fb\u753a\u4e01\u76ee\u30ec\u30d9\u30eb\u4f4d\u7f6e\u53c2\u7167\u60c5\u5831 \u3092\u5229\u7528\u3057\u3066\u3044\u307e\u3059"}]}
    ```

## 簡単な Swift からの Finds.jp の使い方（http get）
- swift では、以下のようにするのが基本形（らしい）

    ```
    let url = URL(string: "https://あどれす")!
    let request = URLRequest(url: url)
    let session = URLSession.shared
    session.dataTask(with: request) { (data, response, error) in
        if error == nil, let data = data, let response = response as? HTTPURLResponse {
            // response.statusCode
            // String(data: data, encoding: String.Encoding.utf8) ?? ""
        }
    }.resume()
    ```

# 位置情報から地名を表示する
1. 地名表示領域とボタンを配置
   - 地名表示領域として、テキストフィールドを配置する
   - 【なぜ？】
     - 地名表示にテキストフィールドを使うのは、クリップボードにコピー可能とするため
     - ボタンを配置するのは、地名を確認したいときだけ更新するため
         - 位置情報が更新されるたびに地名を確認(取得)していると、不必要な通信が多発するから
         - ほんの少しの移動で地名を確認(取得)するのは無駄な処理だし、通信データの無駄使いだから
     - 今回は、画面の上部に配置した
   - 前の記事でも書いたことなので、簡単に書きます
     1. [Main.storyboard]を表示
     2. メニュー [View]-[Show Library]を選択
     3. [Text Field]を [MapView]に配置（ドラッグ&ドロップし大きさを調整）
     4. ボタンも同様に配置
 
1. テキストフィールド用の変数(locationName)を追加
   - ViewController.swift を以下のように修正
   - 【なぜ？】
     - 確認(取得)した地名を設定するため
     - プログラムからアプリの状態を変化させるには**変数**を使うから

    ```swift:ViewController.swift
    @IBOutlet var mapView: MKMapView!
    var locationManager: CLLocationManager!
    @IBOutlet weak var locationName: UITextField! // この行を追加
    ```
1. ボタンの関連づけ
   - 前の記事でも書いたことなので、簡単に書きます
     1. [Main.storyboard]と[ViewController.swift]を表示
     2. ボタンを Ctrl を押しながらクリックし[Touch down]を[ViewController.swift]にドラッグ＆ドロップ（ドロップ位置は、以下を参照）

      ```swift:ViewController.swift
      @IBOutlet weak var locationName: UITextField!
      // この行あたりにドロップする
      @IBAction func clickLocation(_ sender: Any) {
      }
      ```
   - 【なぜ？】
     - ボタンをタップしたときだけ、位置情報から地名に変換する処理を実行するため
     - 利用者がアプリケーションに対し指示を出すには UI 部品を使うため
1. 表示している地図の中心位置（緯度と経度）を取得
   - ViewController.swift を以下のように修正

    ```swift:ViewController.swift
    @IBAction func clickLocation(_ sender: Any) {
        myLock.lock()
        let latitude = mapView.region.center.latitude
        let longitude = mapView.region.center.longitude
        myLock.unlock()
    }
    ```
   - 【なぜ？】
     - 地図表示(MKMapView)オブジェクトが中心位置を保持しており、その情報を使うため
     - GPSで位置情報は更新されるので、排他処理は必須です
     - 排他処理は、できるだけ短時間にするため、緯度・経度を取得するだけ
1. 逆 GeoCoding サービス URL 文字列作成準備
   - ViewController.swift を以下のように修正
     - func clickLocation の直前に１行追加

    ```swift:ViewController.swift
    let FMT_url_rev_geo = "https://www.finds.jp/ws/rgeocode.php?lat=%f&lon=%f&json"  // この行を追加
    @IBAction func clickLocation(_ sender: Any) {
    ```
   - 【なぜ？】
     - [printf 書式](https://ja.wikipedia.org/wiki/Printf)を定義
     - プログラムで、変数の値を文字列に埋め込むなら sprinf （と同じような機能）を使うから
1. 位置情報（緯度・経度）を地名に変換
   - ViewController.swift を以下のように修正
     - func clickLocation の内容を丸々載せますので置き換えてください

    ```swift:ViewController.swift
    @IBAction func clickLocation(_ sender: Any) {
        myLock.lock()  // 
        let latitude = mapView.region.center.latitude
        let longitude = mapView.region.center.longitude
        myLock.unlock()
        
        let url = URL(string: String(format: FMT_url_rev_geo, latitude, longitude))!
        let request = URLRequest(url: url)
        let session = URLSession.shared
        session.dataTask(with: request) {
            (data, response, error) in
            if error == nil, let data = data, let response = response as? HTTPURLResponse {
                print("statusCode: \(response.statusCode)")
                let jsonString: String = String(data: data, encoding: String.Encoding.utf8) ?? ""
                var locationData =  jsonString.data(using: String.Encoding.utf8)!
                do {
                    let items = try JSONSerialization.jsonObject(with: locationData) as! Dictionary<String, Any>
                    let result = items["result"] as! Dictionary<String, Any>
                    let prefecture = result["prefecture"] as! Dictionary<String, Any>
                    let municipality = result["municipality"] as! Dictionary<String, Any>
                    let local = result["local"] as! Array<Any>
                    let local0 = local[0] as! Dictionary<String, Any>
                    self.locationName.text = prefecture["pname"] as! String
                    self.locationName.text! += municipality["mname"] as! String
                    self.locationName.text! += local0["section"] as! String
                }
                catch {
                    print(error)
                }
            }
        }.resume()
    }
    ```
   - 【なぜ？】
     - エラー処理は、ほぼ省略しました（汗）
     - 取得した位置情報（緯度・経度）をもとに URL 文字列を構成
         - 位置情報を埋め込んだ文字列が作成される
         - ブラウザに入力すれば Finds.jp を使って、位置情報から地名への変換結果が表示されるはず
     - Finds.jp の変換結果(JSON形式)をSwiftのデータ型(Dictionary<String, Any>)に変換
     - Dictionary からデータを取り出す
         - ["result"]-["prefecture"]-["pname"] に都道府県
         - ["result"]-["municipality"]-["mname"] に市区町村
         - ["result"]-["local"] は配列
             - 最初の要素の ["section"] に町丁目や字など
             - ２番目以降の要素は、今回使用しない
     - [都道府県]+[市区町村]+[町丁目や字など]をテキストフィールドに設定
1. テスト実行
   - Simulatorを起動
     - Simulator メニューから [Debug]-[Location]-[Custom Location...]を選択し、東京の中心を設定
         - 緯度(Latitude)に35.6804725
         - 経度に(Longitude)に139.7661837
   - 地図を拡大表示し、地名表示のボタンを押すと、地名が表示される
   - 【注意】地図の中心は、日本国内で、人が住んでいる場所を選ばないと、異常終了する
     - 【なぜ？】
         - 国外とか海とかは、位置情報に対応する地名が存在しない（取得できない）ため
         - エラー処理を省いたため

# 今回の到達点
- 日本国内の地図を表示し、ボタンを押すことで地名が表示されるようになった

# 連載
1. [[はじめてのiOSアプリ]xcodeで地図アプリを作成（その１：プロジェクト作成）](https://qiita.com/shinobee/items/1f6d26451336bf736484)
1. [[はじめてのiOSアプリ]xcodeで地図アプリを作成（その２：地図表示）](https://qiita.com/items/50e1acee3024c3e26fb9)
1. [[はじめてのiOSアプリ]xcodeで地図アプリを作成（その３：位置情報取得）](https://qiita.com/items/b8d287d2e34af20aab64)
1. [[はじめてのiOSアプリ]xcodeで地図アプリを作成（その４：位置情報と連携した地図表示）](https://qiita.com/items/a91c49ebf5301d936120)
1. [[はじめてのiOSアプリ]xcodeで地図アプリを作成（その５：アプリアイコン設定）](https://qiita.com/items/24804294ab86635ee5d3)
1. [[はじめてのiOSアプリ]xcodeで地図アプリを作成（その６：拡大・縮小ボタン追加）](https://qiita.com/items/12c54c4e253bc48cad4d)
1. [[はじめてのiOSアプリ]xcodeで地図アプリを作成（その７：地図を拡大・縮小）](https://qiita.com/items/4406c4fdf2fbdcc40533)
1. [[はじめてのiOSアプリ]xcodeで地図アプリを作成（その８：地名表示）](https://qiita.com/items/3363779d0ea11ca6073c)
1. [[はじめてのiOSアプリ]xcodeで地図アプリを作成（その９：ソースコード管理）](https://qiita.com/items/d0e63020b0dce4c70542)

<!-- eof -->