[iOS] 地図アプリを作る – 現在地から近い順に並べる

はじめに

数日前に中の人になりました中安です。よろしくお願いします。

中の人になる以前は、調べ物をするたびにDevelopers.IOで基本的なところから応用編に至るまで色々とお世話になりました。自分も同じように基本的な技術と応用技術を徐々にアウトプットしていけるようにブログの執筆をしていきたいと思っております。

今回は最初なので基本的なところといいますか、初心者向けな内容からスタートしてみたいと思います。

テーマは「マップ」 さまざまなアプリで使用される「地図」でよく使うTipsを実践混じえて書いていきます。

つくるもの

今回は下記のようなアプリを作るぞーというサンプルのもと話を進めようかと思います。

京都に遊びにきた人が現在地から様々な観光地の場所を地図上で確認するアプリ

  • 現在地から観光地までの距離が表示される
  • 現在地から近い順に並ぶリストを表示する

なぜ京都なのか…。それは「自分の出身地だから」というのと、日本人ならみんな知ってる観光スポットが多いからです。特に深い意味はありません。そもそも何か役に立つアプリなのか? もよくわかりません。

ただ、これを応用すると、よくある「店舗検索」や「事業所検索」など、現在地から近い順に地図に表示されてほしいというアプリ要件を満たすために使えるんじゃないかなぁと思います。

モデル

モデルクラス定義

今回は「観光地」ということで「Spot」というモデルを作成しました

import UIKit
import CoreLocation

class Spot {
        
    let name: String
    let location: CLLocation
    
    init (_ lat: CLLocationDegrees, _ lng: CLLocationDegrees, _ name: String = "") {
        self.location = CLLocation(latitude: lat, longitude: lng)
        self.name = name
    }
}

緯度経度と観光地名を持つだけのシンプルなエンティティモデルです。 観光地名はイニシャライザで省略可能としています。

緯度経度はイニシャライザに渡した時点でCLLocation型で保持させておくようにしました。 今回はサンプルなのでデータは下記のように直書きしてます。 本来であればDBやWebAPIなどの処理がもっと絡んでくるとは思いますが、割愛します。

extension Spot {
	
	static var list: [Spot] {
        return [
            Spot(34.98747669999999, 135.75949259999993, "京都タワー"),
            Spot(34.987578, 135.74722599999996, "京都水族館"),
            Spot(34.9811177, 135.74759640000002, "東寺"),
            Spot(34.9793536, 135.74259499999994, "羅城門跡"),
            Spot(34.9903771, 135.75109659999998, "西本願寺"),
            Spot(35.007345, 135.75443059999998, "本能寺"),
            Spot(35.0050538, 135.76329770000007, "錦市場"),
            ・
            ・
            {中略}
            ・
            ・
            Spot(35.1241, 135.82104500000003, "寂光院"),
            Spot(34.8892908, 135.80767830000002, "平等院"),
            Spot(34.892068, 135.81143900000006, "宇治上神社"),
            Spot(34.891048, 135.810701, "宇治神社"),
        ]
    }
}

このようなデータはGoogleのジオコーディングのサービスなどを使えば、簡単に作れます。

ある地点からの距離を保持するプロパティを作る

モデルに現在地からの距離を計算する処理を実装してみます。 距離の算出はCLLocationを使用するのですが、 先にCLLocationに次のようなメソッドを拡張追加しておくとよいでしょう。

extension CLLocation {
    
    // 同じ座標かどうかを返す
    func isEqual(location: CLLocation?) -> Bool {
        if let location = location {
            return self.coordinate.latitude  == location.coordinate.latitude
                && self.coordinate.longitude == location.coordinate.longitude
        }
        return false
    }
}

次にモデルに以下のような実装をします

    private(set) var distance: CLLocationDistance?
    
    var targetLocation: CLLocation? {
        didSet {
            guard let location = targetLocation else {
                distance = nil
                return
            }
            if location.isEqual(location: oldValue) {
                return
            }
            distance = self.location.distance(from: location)
        }
    }

対象の緯度経度targetLocation(ここでいうところの現在地)を代入したときに、同時にそこまでの距離distanceを同時に計算するようにします。

if location.isEqual(location: oldValue)

上のように代入前のtargetLocationと比較をかませることで、あとでソートするときに無駄な計算が何度も走らないで済むようにしています。

ある地点からの近い順にソートする処理を入れる

これまで作ったものを踏まえて、バラバラに定義した緯度経度のデータをある地点(現在地)から近い順に並べる処理を以下のように書きました。

extension Spot {
    
    static func sortedList(nearFrom location: CLLocation) -> [Spot] {
        return self.list.sorted(by: { spot1, spot2 in
            spot1.targetLocation = location
            spot2.targetLocation = location
            return spot1.distance! < spot2.distance!
        })
    }
}

ここで前項に書いた location.isEqual(location:) が効いてきます。比較のためのdistanceプロパティを算出する処理が、何度も走らないようになります。

例えば96件の観光地データがあれば、フィルタをかけないと1394回計算処理が走りますが、フィルタをかけることで96回に計算コストを抑えられます。

実際に近い順に並ぶかどうかのテストです。

let currentLocation = CLLocation(latitude: 34.984337, longitude: 135.757797)

Spot.sortedList(nearFrom: currentLocation).forEach({ spot in
    print("\(spot.name) (\(spot.distance!)m)")
})

上記のcurrentLocationで指定している緯度経度は「京都駅八条口(新幹線側)」です。 京都駅から近い順の観光地がコンソールにバババっと並べば動作確認成功です。

京都タワー (381.174436771062m)
東本願寺 (742.389150695634m)
西本願寺 (907.344227042644m)
東寺 (997.484309967112m)
京都水族館 (1029.97027989507m)
三十三間堂 (1330.01337398748m)
京都鉄道博物館 (1396.13369920685m)
羅城門跡 (1494.11841530655m)

大丈夫のようです。

まとめ

現在地から近い順に並べる方法は以上になります。このデータ配列をテーブルビューなどに反映させるなどすれば、最初の要件に合致するでしょう。

わかりやすいようにシンプルなモデルクラスをひとつ用意しましたが、実装方法はそれぞれかと思います。やりやすいように適宜扱ってください。

また別の記事で実際にマップにこれらのデータを反映させていく方法を書ければと思います。