はじめまして、エンジニアの小川です。

最近活動を始めた DRIP – Drecom Invention Project にエンジニアとしてジョインしてます。

DRIP - Drecom Invention Project

その中で現在、マップではじまるライブコミュニケーション Pass! のクライアント側(iOS)およびサーバー側の開発を日々行っています。

Pass! は「地図の上でユーザー同士がコミュニケーションできるアプリ」なので、地図の上に View を配置し、地図を動かしたら View も追従していくシステムを作る必要がありました。
プロトタイプ開発時代からそのやり方を試行錯誤した歴史があり、今回はそこで得た知見を整理していこうと思います。

追従させるやり方

調べた結果、2つのやり方を見つけました。

  1. マーカーを使う
  2. 緯度経度<-->座標変換を使う

マーカーを使う

let coord = CLLocationCoordinate2D(latitude: 35.632011, longitude: 139.713542)
let marker = GMSMarker(position: coord)
marker.icon = image
marker.map = mapView

GMSMarkerMKAnnotationView を使うと、いわゆるピンを地図に立てる事ができます。
これらに UIImage を設定する事ができるため、ここに表示したい画像を設定すれば、ピンの代わりに画像が地図上に表示されるようになります。

メリット

実装が容易

Google Map SDKMapKit Framework にある機能をそのまま使うため、比較的簡単に実装できます。

地図に完全フィットする

どれだけ地図を動かしてもきちんと付いてくるのは当たり前として、表示している地図そのものの大きさを変えるアニメーションを仕込んでも、コード上特に意識する事無く追従してくれます。

View を触っても地図を動かせる

マーカーをタップした場合はタップイベントが発火し、マーカーを触ってドラッグした場合はきちんと地図が動きます。
そのためマーカーがたくさん表示されている状態でも、難なく地図の操作ができます。

デメリット

UIImage だけしか使えない

マーカーに対して UIView を設定してやる事はできないため、必然 UIImage にできる範囲でしか表現できません。
静止画、またはパラパラアニメですね。

動きをカスタマイズできない

とあるマーカーが東京駅に居るとして、アクションがあったら名古屋駅まで動かしたいという場合、マーカーに名古屋駅の緯度経度を設定すると、そのマーカーが名古屋駅までヒュンっと移動します。

// 東京駅がセットされる
marker.position = CLLocationCoordinate2D(latitude: 35.681295, longitude: 139.766271)

〜〜〜

// 名古屋駅をセット
marker.position = CLLocationCoordinate2D(latitude: 35.171009, longitude: 136.881754)

// ヒュンっと移動する

このアニメーションはカスタマイズできないため、「もっと全体的にゆっくり移動して欲しい」や「動き始めと動き終わりだけゆっくりにして欲しい」という要望に対応できません。

マーカー内のどこをタップされたのかを区別できない

func mapView(mapView: GMSMapView, didTapMarker marker: GMSMarker) -> Bool {
    if let user = markder.userData as? User {
        // ユーザーに関わる処理
    }
}

マーカーのどこかしらがタップされたら上記イベントが発火します。
上半分をタップされたらこう、下半分をタップされたらこう、のように、タップ領域によって処理を分けることはできません。

緯度経度<-->座標変換を使う

let coord = CLLocationCoordinate2D(latitude: 35.632011, longitude: 139.713542)
let point = mapView.projection.pointForCoordinate(coord)
userView.center = point

gmsMapView.projection.pointForCoordinate(coordinate)mkMapView.convertCoordinate(:coordinate, toPointToView:mkMapView) を使うと、緯度経度から地図 View 上における CGPoint を取得できます。
CGPoint がわかるという事は、そこを中心とした UIView を置けばマーカーのように UIView が設置されます。
そして、マップが動かされた、拡大縮小された時のイベントを拾ってきて逐一 UIView に反映してやれば、まるで地図に追従しているかのような UIView になるのです。

メリット

アニメーションに強い

let coord = CLLocationCoordinate2D(latitude: 35.681295, longitude: 139.766271)
let point = mapView.projection.pointForCoordinate(coord)
UIView.animateWithDuration(1.0, animations: {
    view.center = point
})

UIKit の提供するアニメーションを使えるようになるため、様々なアニメーションに対応できます。

タップ処理を細かに制御できる

Pass! のようにユーザーと吹き出しがそれぞれ表示されている場合、ユーザー部分をタップされたらこう、吹き出し部分をタップされたらこう、のように処理を分けることができます。

デメリット

UIView を自分で管理する必要がある

当然ですが UIView そのものを地図側が管理してくれないので、自分で全て管理する必要があります。
そのためマーカー表示に比べ、コードが複雑になりがちです。

追従がほんの少し遅れる

  1. 地図を動かす
  2. 動かされた地図が画面に反映されてから動いたイベントが発行される
  3. 動いた後の地図に合わせて UIView を動かす
  4. 動かされた UIView が画面に反映される

という流れを踏むため、ほんの少しだけ UIView の画面反映が遅れます。
そのため地図をメチャクチャ高速に動かすと UIView が若干浮いているかのような体感になります。

UIView を触ると地図を動かせない

マーカーと違い UIView を触ってしまうと、ドラッグしても地図が動きません。
そのため UIView がたくさん表示されている場合、地図の操作が困難になりがちです。
タップ領域を調整するなど細かな対応をしておくと体感向上に繋がりますが、その分コードを書く必要があります。

まとめ

地図に追従する View を置く場合、2つのやり方があります。

  1. マーカーを使う
    • 実装は容易ですが、できることは制限されます。
  2. 緯度経度<-->座標変換を使う
    • アニメーションに強くなりますが、 UIView 管理やタップ処理の部分で実装すべきコードは増えます。

地図上に何かを表示するようなアプリを作る場合、要件を考慮して上記から選択するのが良いかなと思います。

最後に

Pass! のプロトタイプを作った当初はマーカーを採用し、ユーザー間のやり取りはチャットUIな画面で行っていました。

ですがある日、
「地図上でコミュニケーションまで完結する体験をどうすればユーザーに与えられるか、追求してみないか?」
という問いかけから、何とかして UIView を地図上に表示するやり方が無いだろうか?と調べた結果見出したのが、緯度経度<-->座標変換です。

β版ではこのやり方を採用しており、その結果マーカーに比べてユーザー体感をグッと上げる事ができました。
もし「他にもこんなやり方あるよ!」という方いらっしゃいましたら、ご連絡お待ちしております!

DRIP – Drecom Invention Project
http://drip.drecom.co.jp/

マップではじまるライブコミュニケーション Pass!
紹介ページ: https://pass.town/
AppStore: https://itunes.apple.com/jp/app/id1090318837?mt=8