[Swift] MapKitを使って”場所のサジェスト”を簡単に実装してみる

2018.12.04

はじめに

モバイルアプリサービス部の中安です。

最近イベント告知のブログばっかり書いてて、さすがによくないと思ったので久しぶりに「実装してみる」シリーズなブログを書こうかと思います(汗)

今回やるのは「場所のサジェスト機能」を簡単に実装してみるというものです。

「サジェスト機能」

インターネットのサーチエンジンで、検索した文字列に関連の深い語句を逐次予測して表示する機能。米国グーグル社のサーチエンジンではオートコンプリートといい、語句の候補を、他の利用者の検索語句や同社のデータベースから機械的に抽出して表示している。予測表示機能。検索予測機能。検索候補機能。(デジタル大辞泉)

「サジェスト機能」は、ここにも書いてあるように、文字を入力したら続きの候補が出てくる、いわゆる「オートコンプリート機能」とも言いかえられる機能のことです。

そして、この機能は MapKit フレームワークMKLocalSearchCompleter クラスを使用することで、あまり難しいことを考えなくてもサクッと作れてしまいます。

まずは基本実装まで

サンプルなので、ここらへんは簡潔に

ストーリーボード

画面の一番上に UITextField 、その下に UITableView を置いているだけです。

テーブルビューに配置するセルは、その CellStyle.subtitle、識別用のIDを "cell" としています。

ビューコントローラ

ビューコントローラのソースコードです。 とりあえずデーブルビューが表示できる程度の空っぽな実装にしてあります。

import UIKit

class ViewController: UIViewController {
    
    @IBOutlet private weak var tableView: UITableView!
    @IBOutlet private weak var textField: UITextField!
    
    private var searchCompleter = MKLocalSearchCompleter()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        tableView.delegate = self
        tableView.dataSource = self
    }
    
    @IBAction private func textFieldEditingChanged(_ sender: Any) {
        // あとで
    }
}

extension ViewController: UITableViewDelegate, UITableViewDataSource {
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 10
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        // あとで
        return cell
    }
}

IBOutlet はそれぞれのUIパーツに、IBAction はテキストフィールドの editingChanged イベントに紐づけています。

テキストフィールドに入力されると候補が一覧に出てくるような実装をこれからしていきます。

MKLocalSearchCompleter

A utility object for generating a list of completion strings based on a partial search string that you provide.

提供する部分検索文字列に基づいて補完文字列のリストを生成するためのユーティリティオブジェクトです。

https://developer.apple.com/documentation/mapkit/mklocalsearchcompleter

MKLocalSearchCompleter クラスは、リファレンスの見出しにもある通りの機能を提供してくれます。

与えた文字列から自動的に「場所」の検索をして、MKLocalSearchCompletion というデータオブジェクトの配列を作って返してくれます。

これを先ほどのビューコントローラクラスに実装してみます。

インポート

MapKit をインポートします。

import UIKit
import MapKit

MKLocalSearchCompleter オブジェクト

メンバ変数に MKLocalSearchCompleter のオブジェクトを初期化して代入しておきます。

class ViewController: UIViewController {
    
    // (省略)
    
    private var searchCompleter = MKLocalSearchCompleter()
    
    // (省略)
}

デリゲートになる

ビューコントローラが MKLocalSearchCompleter のデリゲートになります。

extension ViewController: MKLocalSearchCompleterDelegate {
    
    // 正常に検索結果が更新されたとき
    func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
        tableView.reloadData()
    }
    
    // 検索が失敗したとき
    func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
        // エラー処理
    }
}
class ViewController: UIViewController {
    
    // (省略)
    
    override func viewDidLoad() {
        // (省略)
        searchCompleter.delegate = self
    }
}

テキストフィールドの入力値が変わったとき

MKLocalSearchCompleterqueryFragment プロパティに文字を代入すると、自動的に検索が始まります。

class ViewController: UIViewController {
    
    @IBAction private func textFieldEditingChanged(_ sender: Any) {
        searchCompleter.queryFragment = textField.text!
    }
}

ネットワークを利用して結果を取得してきますが、 ネットワークに関する処理やキャッシュデータの持たせ方などを考える必要はないです。 前述のデリゲートメソッドで、成功時と失敗時のことだけを実装すればいいわけです。

テーブルビューに反映させる

検索の結果は MKLocalSearchCompleterresults プロパティに入っています。 ここには先述の MKLocalSearchCompletion が配列で格納されているので、それをテーブルビューで表示するだけです。

extension ViewController: UITableViewDelegate, UITableViewDataSource {
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return searchCompleter.results.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        let completion = searchCompleter.results[indexPath.row]
        cell.textLabel?.text = completion.title
        cell.detailTextLabel?.text = completion.subtitle
        return cell
    }
}

title プロパティには「場所の名前」、 subtitle プロパティには「住所」などの付加情報が文字列で入っています。

表示結果

ここまで実装できれば、もう完成です。 実際にアプリを動かしてみて、テキストフィールドに適当に文字列を入力してみます。

自分は大阪の人間なので、「しん」と打ってみると自動的に「新大阪」や「心斎橋」「新今宮」などの大阪の場所の名前が一覧化されました。

データ自体は取れているので、あとは煮るなり焼くなりできるかと思います。

少しカスタマイズ

フィルタの種別

MKLocalSearchCompleterfilterType プロパティに対して、フィルタの種別を与えてやります。

searchCompleter.filterType = .locationsOnly
  • .locationsAndQueries: 地図上の場所と一般的なクエリの両方を取得する
  • .locationsOnly: 地図上の場所のみを取得する

リファレンスには .locationsAndQueries では "cof" と検索すると "coffee" がサジェストされてくると書いてあります。

位置指定

先ほどの例では自分が大阪にいるので、大阪の検索結果が取れました。しかし、その位置を指定したいこともあると思います。 その場合は region を指定してやります。

        let tokyoStation = CLLocationCoordinate2DMake(35.6811673, 139.7670516) // 東京駅
        let span = MKCoordinateSpan(latitudeDelta: 0.001, longitudeDelta: 0.001) // ここは適当な値です
        let region = MKCoordinateRegion(center: tokyoStation, span: span)

        searchCompleter.region = region

例では、東京駅を中心として検索結果を得るよう緯度経度を渡しました。

この設定を含めて実際にアプリを動作させてみると

同じ検索文字列でも「新宿」や「新橋」「新丸の内」などの東京の地名になりました。

最後に

このようなサジェスト機能は GoogleMap の API などを使っても実現できると思いますが、iOS の機構だけでも手軽に作ることができます。

この記事で書いたのは本当に簡単なサンプルですが、少ないコード量でもちょっと「おっ」と思える機能を実現できます。

何かのお役に立てれば幸いです。