【Swift】Mapアプリを作成する

今回は、↓こちらの講座で作ったMapアプリの復習です
[【iOS14対応】未経験者がiPhoneアプリ開発者になるための全て iOS Boot Camp](https://www.udemy.com/share/103I3AAEcTdVhUTHQH/)

# Mapアプリ
主な仕様は2つあります。
①マップ上で特定の位置をロングタップすると、その位置の住所が表示される
![b6328210d50df2973c0a3be55506ac2a.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/956215/fab27033-a72e-1704-af1c-b4bb716fd414.gif)
②緯度経度を入力すると、その位置のマップと住所が表示される
![d51a947e6cfb7fa95c944f26344583fa.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/956215/75b6b944-80f8-ebfb-8cd4-c7780d36b0de.gif)

但し、このアプリでは正確なバインディングは行ってないので雑に緯度経度を入力したり、何もない所を指定すると落ちます。

このアプリでは主にデリゲートとプロトコルの仕組みを理解することを目的としています。

# UI
![スクリーンショット 2021-02-17 22.48.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/956215/a8c67f36-ce30-6aee-de82-90df84b1b5fb.png)
左の画面が`ViewController`クラス、右の画面が`NextViewController`クラスです。

# Map機能を実装する
`Main.StoryBoard`では`MKMapView`という要素を使用します。
`ViewController`ではマップ機能を実装するために`import MapKit`、`import Corelocation`が必要です。
クラスには、住所を取り扱うための`CLLocationManagerDelegate`、ロングタップを使えるようにするための`UIGestureRecognizerDelegate`を継承します。
すると冒頭は以下のようになります。

```ViewController.swift
import UIKit
import MapKit
import CoreLocation

class ViewController: UIViewController, CLLocationManagerDelegate, UIGestureRecognizerDelegate {
//　中略
}
```

# ロングタップを実装する
`Main.StoryBoard`上で`Long Press Gesture Recognizer`というパーツを`MKMapView`の上に置き、プログラムに`IBoutlet`で`longPress`という名前でインスタンス化します。
さらにもう一度プログラムに繋ぎ、`IBAction`で、`Type`を`UILongpressRecognizer`に変更して`longPressTap`という名前でインスタンス化します。
この時、`IBAction`の引数`sender`に、ロングタップに関する状態についての情報が入っているので、この引数を使用して中身の処理を記述します。

```ViewController.swift
@IBAction func longPressTap(_ sender: UILongPressGestureRecognizer) {
        
  if sender.state == .began{
    //タップを開始した時の処理
  } else if sender.state == .ended{
    //タップが終了した(=画面から手を離した)時の処理
  }
//中略
}
```
今回は終わった時だけに処理を記述します。
この時の処理の流れとしては、
①タップした位置を指定してMKMapView上の緯度経度を取得する
②取得した緯度経度を住所に変換する
という流れになります。

ここで、この処理を記述する前に、緯度経度を住所に変換する関数を先に作成します。

```ViewController.swift
func convert(lat:CLLocationDegrees, log:CLLocationDegrees){
  //↑latは緯度を入れる引数、logは経度を入れる引数です
  let geocoder = CLGeocoder()
  let location = CLLocation(latitude:lat, lognitude:log)
  //                                 ↑引数に入れられた値がここに入りますね

  //クロージャーという仕組みを使用しています↓
  //クロージャー内で宣言していない変数に関しては全てselfを記述する必要があります
  //クロージャーは値が入ってくるまでは発動しないようになっています
  geocoder.reverseGeocodeLocation(location) { (placeMark, error) in
    //      ↓が空でなければ、という条件分岐
    if let placeMark = placeMark{
      //placeMarkには住所が入っています 値が入っているかで条件分岐をしています
      if let pm = placeMatk.first{
        if pm.administrativeArea != || pm.locality != nil {
                               //↓には〇〇県、〇〇市といった住所に関する情報が入っています
          self.addressString = pm.name! + pm.anministArea! + pm.locality!
        } else {
              //上のような整った住所がない場合の処理
              self.addressString = pm.name!
        }
        self.addresslabel.text = self.addressString
      }
    }
  }
}
```
それではロングタップの実装に戻ります。

```ViewController.swift
@IBAction func longPressTap(_ sender: UILongPressGestureRecognizer) {
        
  if sender.state == .began{
    //タップを開始した時の処理
  } else if sender.state == .ended{
    //タップが終了した(=画面から手を離した)時の処理

  //タップした位置から緯度経度を取得する
    let tapPoint = sender.location(in: view) //viewの中でタップしたポイントを変数に格納

    //タップした位置(CGPoint)を指定してMKViewMap上の緯度経度を取得する
    //つまり、Mapview上でタップした部分をconvertメソッドを用いて緯度経度に変換している
    let center = mapView.convert(tapPoint, toCoordinateFrom: mapView)
    let lat = center.latitude
    let log = center.latitude
    //住所を取得する ここで、先ほど作成したメソッドを発動させます
    convert(lat: lat, log: log)
    //convertメソッドですでに住所はロングタップした部分に反映されています
}
```
# 入力した緯度経度からMapと住所を取得する
`ViewController`の設定ボタンから緯度経度入力画面に遷移するよう実装します(遷移の実装は割愛）
そして、緯度経度入力画面からMapの画面に値を渡すためのプロトコルを設定します。
まず、値を受け取る`ViewController`に以下を記述します。

```ViewController.swift
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
  //忘れがちですがsegueのidentifierに"next"を入力します
  if segue.identifier == "next" {
    let nextVC = segue.destination as! NextViewController
    nextVC.delegate = self
  }
}
```
次に`NextViewController`にプロトコルを記述していきます。

```NextViewController.swift
protocol SearchLocationDelegate {
    func searchLocation(idoValue:String, keidoValue:String)
}
```
このメソッドを使用できるよう、`class NextViewController`配下に以下の記述を行います。

```NextViewController.swift
var delegate = SearchLocationDelegate?
```
`OKボタン`を押したら、入力された緯度と経度を`ViewController`に渡したいので、`OKボタン`をプログラムにつないで`okAction`と名前をつけインスタンス化したら、

```NextViewController.swift
@IBAction func okAction(_ sender: Any) {
  //テキストフィールドに入力された値を取得し
  let idoValue = idoTextField.text!
  let keidoValue = keidoTextField.text!

  //デリゲートメソッドの引数に入れてデリゲートメソッドを発動させる
  //2つのテキストフィールドに値があれば画面遷移を行うという条件分岐を設ける
  if idoTextField != nil && keidoTextField != nil {

    delegate?.searchLocation(idoValue:String(idoValue), keidoValue:String(keidoValue))
}

```
`ViewController`に委任するために`SearchLocationDelegate`を`class`に追記し、デリゲートメソッドを追加します。

```ViewController.swift
func searchLocation(idoValue:String, keidoValue:String) {
  //デリゲートメソッドで渡された値がちゃんとあるかどうかで条件分岐
  if idoValue.isEmpty != true && keidoValue.isEmpty != true {
    let idoString = idoValue
    let keidoString = keidoValue
    //緯度経度からコーディネート(=画面の中心に持ってくる)
    let coordinate = CLLocationCoordinate2DMake(Double(idoString)!, Double(keidoString)!)

    //Map上で表示する範囲を指定
    let span = MKCoodinateSpan(latitudeData: 0.01,
lognitudeData: 0.01)

    //Mapの領域を指定
    let region = MKCoodinateRegion(coordinate, span: span)

    //領域をmapViewに設定
    mapView.setRegion(region, animated: true)

    //受け取った緯度経度から住所に変換する(すでに作成済み)
    convert(lat:Double(idoString)!, log:Double(keidoString)!)
  } else {
    //もし値を取得できなければ
    addressLabel.text = "表示できません"
  }
}
```
以上で完成です。
# 感想
言われてみればなんとなくわかるけど、説明しろと言われるとなかなか難しい、っていう状態ですね。
Appleが用意しているプロトコルをたくさん回数を重ねて使用すればわかってくるようになってくる気がします。
引き続き根気強く学習していきます。