【Swift】MKMapView:ピンの吹き出しにCustomViewを設定する
こんにちは、前回に引き続きMapViewのTipsになります。
今回はマップの吹き出しカスタムビューを設定する方法になります。
まず、この吹き出しはCalloutというらしいですね。なので以下ではそう記述します。
デフォルトでもこの機能自体はあるのですが、タイトル、サブタイトル、右側に付与するアクセサリーなどレイアウト等は限定されているため、カスタムビューを設定したいケースが出てきます。その場合にCustomViewをCalloutに設定する方法です。
このカスタムもよく利用するかと思うのでご参考にどぞー。
CustomViewを作成する
まずは設定したいカスタムビューをUIViewを継承して用意します。自分はXibを利用します。
import UIKit
final class PlaceCalloutView: UIView {
@IBOutlet weak var label: UILabel!
@IBOutlet weak var imageView: UIImageView!
@IBAction func buttonDidTap(_ sender: Any) {
print("button was tapped")
}
}
ボタンのActionも少し手を加える必要があるのも記述したいので追加しています。
Xibは適当ですがこんな感じ。
Xibを利用する場合はdelegateメソッドのdidSelectでnibから引っ張ってきて利用するのですが、特に明示的に記述しない場合このxibで設定したframeサイズがそのまま適用されます。
そしてcellのようにself-sizingを行ってくれるわけではないので、動的にcalloutサイズを変えるというのが困難です。
例えば、Labelの行数指定が最大2行で、改行した時にサイズもその分拡張する、というのは実現できます。
その場合は2行になった場合、つまりViewの最大サイズというのをxibでレイアウトしておきます。(2行ならlabel.height * 2分を設定する)
あとは改行設定をし、 topの制約に ≧ 0 を設定しておけばその範囲内で動的に可変してくれます。
Annotation(ピン)にCalloutを設定する
実装していきます。
MKMapViewDelegate に didSelect, didDeselect があるのでAnnotationが選択された場合の処理を記述します。
Annotationの加工は以下を見てみてください!
【Swift】MKMapView:ピンに画像やCustomViewを設定する
private func setAnnotations() {
// set an annotation that changes its image
let annotation = MKPointAnnotation()
annotation.coordinate = CLLocationCoordinate2D(latitude: 35.68493104294112, longitude: 139.75273538025138)
mapView.addAnnotation(annotation)
// set an annotation that Custom View is used
mapView.register(PlaceAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
let tokyoDome = PlaceAnnotation(index: 2, name: "東京ドーム", image: R.image.im_sp()!,
latitude: 35.70580548548312, longitude: 139.7517604413992)
tokyoDome.coordinate = CLLocationCoordinate2DMake(tokyoDome.latitude, tokyoDome.longitude)
let jinbochoSt = PlaceAnnotation(index: 3,
name: "神保町駅 ー 神保町は古本いっぱい楽しいうわーいうわーいうわーいうわーいうわーいうわーいうわーいうわーいうわーいうわーいうわーいうわーい",
image: R.image.im_sp()!, latitude: 35.69591725953706, longitude: 139.75773500398736)
jinbochoSt.coordinate = CLLocationCoordinate2DMake(jinbochoSt.latitude, jinbochoSt.longitude)
mapView.addAnnotations([tokyoDome, jinbochoSt])
}
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
// 自身の位置は無視
if view.annotation is MKUserLocation { return }
let calloutView = R.nib.placeCalloutView.firstView(owner: nil)!
if let annotatioin = view.annotation as? PlaceAnnotation {
calloutView.label.text = annotatioin.name
calloutView.imageView.image = annotatioin.image
}
// make inset because the callout's bottom focuses on the center of annotation
let inset = PlaceAnnotationView.height / 2
calloutView.center = CGPoint(x: view.bounds.size.width / 2,
y: (-calloutView.bounds.size.height / 2) - inset)
view.addSubview(calloutView)
mapView.setCenter((view.annotation?.coordinate)!, animated: true)
}
func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) {
if view.isKind(of: MKAnnotationView.self) {
for subview in view.subviews {
if subview.isKind(of: PlaceCalloutView.self) {
subview.removeFromSuperview()
}
}
}
}
Calloutの下部がAnnotationの中心にフォーカスされてしまうため、Annotation.height / 2 分インセットをかけてあげるといい感じになります。
didDeselectはマップをタップするとCalloutが閉じるような処理を記述しています。
これで表示はこんな感じ、いいですね。
Callout上のボタンが反応するようにする
このままだとCalloutにボタン等のイベントを設定している場合、タップするとCalloutが閉じてしまいます。
なので hitTest を override してボタンにアクションが行くように設定してあげます。
ここで設定するのはAnnotation方に設定します。(ちょっと調べきれていない)
import UIKit
import MapKit
final class PlaceAnnotationView: MKAnnotationView {
@IBOutlet weak var label: UILabel!
static let height: CGFloat = 53
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
centerOffset = CGPoint(x: 0, y: -frame.size.height / 2)
loadFromNib()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
loadFromNib()
}
private func loadFromNib() {
let nib = R.nib.placeAnnotationView
guard let view = nib.instantiate(withOwner: self).first as? UIView else { return }
view.frame = bounds
addSubview(view)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, with: event)
if hitView == self {
return nil
}
return hitView
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
var isInside = self.bounds.contains(point)
if !isInside {
for view in self.subviews {
isInside = view.frame.contains(point)
if isInside {
break
}
}
}
return isInside
}
}
こうすることでCallout上のボタンにもアクセスできるようになるはずです。
終わりに
2回に分けてMapViewに関して説明しました。
結構案件で実装する場合はデザインを独自で用意するケースがあると思うので、参考になれば嬉しいです!
もう夏ですね、世間は騒がしいですが頑張って今年も乗り切っていきましょう。
ディスカッション
コメント一覧
まだ、コメントがありません