Swift MkMapViewで地図アプリ作成してみた（21）- 周辺検索した地点をセミモーダルビューに表示する

#記事一覧
[Swift MkMapViewで地図アプリ作成してみた（記事一覧）](https://qiita.com/rain_00_00_09/items/3151b3faf2303fe78312)

#これを作ります
googleMapみたいなモーダルビューを作成します。
![modal.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/362635/3963becd-2860-347e-c847-c79e733fc6ca.png)　![CustomFloatingPanelLayout2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/362635/ea820431-3e13-a74b-a7c5-8e6aea60a5aa.png)　![CustomFloatingPanelLayout3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/362635/a661dc60-1619-8d19-eaf5-da619a2b01bd.png)

#セミモーダルビューを表示する
###FloatingPanelをインストールする
swift単体では実装できません。
簡単な方法を探した結果、CocoaPodsで'FloatingPanel'をインストールすることにしました。
ターミナルから下記を実行してインストールする。

```swift:
pod 'FloatingPanel'
```

###モーダルビューのデザインパターンを設定する
FloatingPanelLayoutを継承したクラスを作成し、モーダルビューのデザインパターンを設定する。
insetForで、フリックした時のモーダルビューの表示領域を設定する。

```swift:CustomFloatingPanelLayout.swift：デザインパターンを設定する
import Foundation
import FloatingPanel

class CustomFloatingPanelLayout: FloatingPanelLayout {
    // セミモーダルビューの初期位置
    var initialPosition: FloatingPanelPosition {
        return .half
    }

    var topInteractionBuffer: CGFloat {
        return 0.0
    }
    
    var bottomInteractionBuffer: CGFloat {
        return 0.0
    }

    // セミモーダルビューの各表示パターンの高さを決定するためのInset
    func insetFor(position: FloatingPanelPosition) -> CGFloat? {
        var ret: CGFloat!
        switch position {
        case .full:
            ret = nil//56.0
        case .half:
            ret = 262.0
        case .tip:
            ret = 75.0
        default:
            ret = nil
        }
        return ret
    }

    // セミモーダルビューの背景Viewの透明度
    func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat {
        return 0.0
    }
}
```

###セミモーダルビューを表示する

WalkViewControllerビューから、PointPopupViewControllerビューをセミモーダルで表示する。

```swift:WalkViewController.swift：セミモーダルビューを表示する
import FloatingPanel

class WalkViewController:   UIViewController, 割愛
{
    var floatingPanelController: FloatingPanelController!

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.

        // モーダルビュー
        floatingPanelController = FloatingPanelController()
        floatingPanelController.delegate = self
    }

    // PointPopupViewControllerビューをセミモーダルで表示する
    func showPointPopupView() {
        // かどを丸くする
        floatingPanelController.surfaceView.cornerRadius = 24.0
        // PointPopupViewControllerをセミモーダルに登録する
        let viewCnt = PointPopupViewController()
        floatingPanelController.set(contentViewController: viewCnt)
        // セミモーダルビューを表示する
        floatingPanelController.addPanel(toParent: self, belowView: nil, animated: true)
        floatingPanelController.move(to: .half, animated: true)
    }
}
```

FloatingPanelControllerDelegateを実装して、カスタマイズしたデザインパターンを有効にする。
また、セミモーダルに登録したビューのサイズ変更を有効にする。
これを実装しないと、セミモーダルビューは表示されない。

```swift:WalkViewController.swift：FloatingPanelControllerDelegateを実装
extension WalkViewController : FloatingPanelControllerDelegate {
    // カスタマイズしたデザインパターンを返す
    func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
        return CustomFloatingPanelLayout()
    }
    
    // セミモーダルビューをサイズ変更した時に、登録したビューのサイズを変更する
    func floatingPanelDidEndDragging(_ vc: FloatingPanelController, withVelocity velocity: CGPoint, targetPosition: FloatingPanelPosition) {
        
        let appDelegate: AppDelegate = UIApplication.shared.delegate as! AppDelegate
        appDelegate.pointPopupViewController.resize(targetPosition)
    }
}
```

###セミモーダルビューを消去する
消去する場合は、以下の通り。

```swift:WalkViewController.swift：セミモーダルビューを消去する
    // PopUp画面の消去
    func ExitPointPopupView() {
        // セミモーダルビューを消去する
        floatingPanelController.removePanelFromParent(animated: true)
    }
```

#周辺検索した地点をセミモーダルビューに表示する
###検索バーに入力したワードを地図検索して、アノテーションに表示する

検索バーを表示する。

```swift:WalkViewController.swift：検索バーを表示する
class WalkViewController:   UIViewController,
                            UISearchBarDelegate, 割愛
{
    @IBOutlet var searchBar: UISearchBar!

    override func viewDidLoad() {

        // 検索バーの位置を指定して表示
        searchBar.frame = CGRect(x: 0, y: height-50, width: width, height: 50)
        self.view.addSubview(searchBar)

    }
```

検索結果を表示するためのアノテーションを準備する。

```swift:WalkViewController.swift：アノテーションのクラスを定義し、リスト変数を宣言する。
    
// MKPointAnnotation拡張用クラス
class MapAnnotationWalk: MKPointAnnotation {
    var pinColor: UIColor = .red
    
    func setPinColor(_ color: UIColor) {
        pinColor = color
    }
}

class WalkViewController:   UIViewController,
                            UISearchBarDelegate, 割愛
{
    // 検索結果を保存するアノテーションのリスト
    var annotationList = [MapAnnotationWalk]()
```

検索バーに入力された文字を地図検索して、アノテーションリストに設定して表示する。

```swift:WalkViewController.swift：検索地点をアノテーションリストに表示
    //検索バーで検索ボタン押した時に呼び出されるメソッド
    func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        // キーボードを消去する
        searchBar.resignFirstResponder()
        
        // 未入力なら無視する
        if "" == searchBar.text {
            return
        }

        // 検索条件を作成する。
        let request = MKLocalSearch.Request()
        request.naturalLanguageQuery = searchBar.text
        
        // 検索範囲はマップビューと同じにする。
        request.region = mapView.region
        
        // ローカル検索を実行する。
        let localSearch:MKLocalSearch = MKLocalSearch(request: request)
        localSearch.start(completionHandler: {(result, error) in
            if nil != result {
                for placemark in (result?.mapItems)! {
                    if(error == nil) {
                        //検索された場所にピンを刺す。
                        let annotation = MapAnnotationWalk()
                        annotation.coordinate =     CLLocationCoordinate2DMake(placemark.placemark.coordinate.latitude, placemark.placemark.coordinate.longitude)
                        annotation.title = placemark.placemark.name
                        annotation.subtitle = "〒\(placemark.placemark.postalCode ?? "")\n\(placemark.placemark.administrativeArea ?? "")\(placemark.placemark.locality ?? "")\n\(placemark.placemark.name ?? "")" //placemark.placemark.title
                        annotation.setPinColor(.green)
                        self.annotationList.append(annotation)
                        self.mapView.addAnnotation(annotation)
                    }
                    else {
                        //エラー
                        print(error.debugDescription)
                    }
                }
            }
        })
    }
```

ローカル検索で取得できる情報は、[こちら](https://developer.apple.com/documentation/mapkit/mkmapitem)を参照。
実行結果は以下の通り。
![image1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/362635/023f7ed1-b17a-5e1a-3771-7b6923b0528b.png)

###選択したアノテーションをセミモーダルビューに表示する

セミモーダルビューに表示する情報を格納する変数を宣言しておく。

```swift:WalkViewController.swift：セミモーダルビューに表示する情報
    // タップした地点の情報
    var tapPointTitle: String! = ""                 // タップした地点のタイトル
    var tapStreetAddr: String! = ""                 // 住所
    var tapDistance: CLLocationDistance! = 0        // 距離
```

アノテーションを選択した時に呼ばれるデリゲートを実装する。
先ほど宣言した変数に情報を設定してから、セミモーダルビューを表示する。

```swift:WalkViewController.swift：
    // アノテーションを選択した際に呼ばれるデリゲート
    func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
        let annotation = view.annotation
        
        // POP UP画面に表示する住所
        tapPointTitle = annotation?.title ?? ""
        tapStreetAddr = annotation?.subtitle ?? ""

        // POP UP画面に表示する距離
        let coordinate = CLLocationCoordinate2D(latitude: (annotation?.coordinate.latitude)!, longitude: (annotation?.coordinate.longitude)!)
        tapDistance = calcDistance(mapView.userLocation.coordinate, coordinate)
        
        // POP UP画面で探索する地点
        tapRoutePoint = coordinate

        // セミモーダルビューの表示
        showPointPopupView()
    }
```

セミモーダルビューに表示する情報を取得するためのメソッドを準備しておく。

```swift:WalkViewController.swift：セミモーダルビューから呼ばれるメソッド
    // タップした地点の名称を取得する
    public func getTapPointTitle() -> String {
        return tapPointTitle
    }
    
    // ロングタップした住所を取得する
    public func getTapStreetAddr() -> String {
        return tapStreetAddr
    }
    
    // ロングタップした位置までの距離を取得する
    public func getTapDistance() -> String {
        var retVal: String!
        let dist = Int(tapDistance)
        if (1000 > dist) {
            retVal = dist.description + " m"
        }
        else {
            let dDist: Double = floor((Double(dist)/1000)*100)/100
            retVal = dDist.description + " km"
        }
        
        return retVal
    }
```

#セミモーダルに登録したPointPopupViewControllerビューの実装

###選択したアノテーションの情報を表示する


```swift:PointPopupViewController.swift：セミモードに登録したビューの実装
import UIKit
import FloatingPanel

class PointPopupViewController: UIViewController
{
    var lblDistance: UILabel!
    var lblTitle: UILabel!
    var lblStreetAddr: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()
        
        lblTitle.text = appDelegate.walkViewController.getTapPointTitle().description
        lblDistance.text = appDelegate.walkViewController.getTapDistance().description
        lblStreetAddr.text = "住所 \n" + appDelegate.walkViewController.getTapStreetAddr().description
    }
```

表示した結果は以下の通り。
セミモーダルビューは以下の通りhalfので表示したので、表示結果はデザインパターンのhalfに従って以下の画像の通りとなる。

```swift:
floatingPanelController.move(to: .half, animated: true)
```

![image2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/362635/17bbd77b-2d88-1134-d963-384191416751.png)

※画像は英語ですが、デバイスの言語設定が日本語なら日本語が表示されます。

フリックすれば、表示はfull、tipと切り替わる。