如何为Mapbox注释创建自定义标注弹出窗口?

3

我已经尝试了几个小时。Mapbox网站上的材料只显示如下内容:

func mapView(_ mapView: MGLMapView, calloutViewFor annotation: MGLAnnotation) -> MGLCalloutView? {
// Instantiate and return our custom callout view.
return CustomCalloutView(representedObject: annotation)
}

问题在于没有详细说明“CustomCalloutView”是什么或包含什么内容以实现自定义标注。我理解(我想)它是一个实现MGLCalloutView的类,但创建一个正确实现该方法的类并不容易,我遇到了各种错误,特别是围绕一个函数“self” -> Self。
能够看到如何实际实现自定义标注会很好。Mapbox Git上的所有讨论对像我这样的蒟蒻来说都太复杂了。
1个回答

10

MGLAnnotation是一个NSObjectProtocol,只需要实现它的类或对象具有CLLocationCoordinate2D即可。这个对象应该是您的数据模型或与之密切相关的对象。为了简单起见,我继承自NSObject

CustomAnnotation.swift

import Foundation
import UIKit
import Mapbox

class CustomAnnotation: NSObject, MGLAnnotation {

    var coordinate: CLLocationCoordinate2D
    var title: String?
    var subtitle: String?
    var image: UIImage

    init(coordinate: CLLocationCoordinate2D, title: String, subtitle: String, image: UIImage) {
        self.coordinate = coordinate
        self.title = title
        self.subtitle = subtitle
        self.image = image
    }
}

您定制的弹出窗视图(MGLCalloutView)是另一个协议,任何继承自NSObject的类或对象都可以遵循,并具有以下必需属性,注意我正在使用UIView进行子类化,它也继承自NSObject:

class CustomCallOutView: UIView, MGLCalloutView {

    var representedObject: MGLAnnotation
    // Required views but unused for now, they can just relax
    lazy var leftAccessoryView = UIView()
    lazy var rightAccessoryView = UIView()

    var delegate: MGLCalloutViewDelegate?

    required init(annotation: MGLAnnotation) {
        self.representedObject = annotation
        super.init()
    }

    func presentCallout(from rect: CGRect, in view: UIView, constrainedTo constrainedRect: CGRect, animated: Bool) {

    }

    func dismissCallout(animated: Bool) {

    }
}
请注意, require init(annotation:) 有点误导人,因为人们会期望 annotation 是一个对象,但实际上它是符合 MGLAnnotation 的对象,因此我们可以将其更改为我们自己的数据模型版本的MGLAnnotation。
required init(annotation: CustomAnnotation) {
    self.representedObject = annotation
    super.init()
}

现在,在MGLCalloutViewDelegate代理方法presentCallout(rect:view:constrainedRect:)中,我们将自定义气泡(self)添加到作为参数传递给代理函数的地图视图中。当其被关闭时,我们也想要将视图从超级视图中移除:

func presentCallout(from rect: CGRect, in view: UIView, constrainedTo constrainedRect: CGRect, animated: Bool) {
    view.addSubview(self)

}

func dismissCallout(animated: Bool) {
    if (animated){
        //do something cool
        removeFromSuperview()
    } else {
        removeFromSuperview()
    }
}

最后在你的mapView(_: calloutViewFor annotation:)方法中,创建一个新的自定义注释,该注释来自已遵循MGLAnnotation协议的类或对象,并将其传递给你的自定义标注视图:

func mapView(_ mapView: MGLMapView, calloutViewFor annotation: MGLAnnotation) -> MGLCalloutView? {

    let title = annotation.title ?? nil
    let subtitle = annotation.subtitle ?? nil
    let image = UIImage(named: "apple.png")!
    let customAnnotation = CustomAnnotation(coordinate: annotation.coordinate, title: title ?? "no title", subtitle: subtitle ?? "no subtitle", image: image)

    return CustomCalloutView(annotation: customAnnotation)
}

查看层级结构

以下是完整实现的参考:

CustomAnnotation.swift

请见上文。

ViewController.swift

import UIKit
import Mapbox

class ViewController: UIViewController, MGLMapViewDelegate {

    lazy var mapView: MGLMapView = {
        let mv = MGLMapView(frame: self.view.bounds, styleURL: URL(string: "mapbox://styles/mapbox/streets-v10"))
        mv.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        mv.setCenter(CLLocationCoordinate2D(latitude: 40.7326808, longitude: -73.9843407), zoomLevel: 9, animated: false)
        return mv
    }()


    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        setup()

        // Declare the marker `hello` and set its coordinates, title, and subtitle.
        let hello = MGLPointAnnotation()
        hello.coordinate = CLLocationCoordinate2D(latitude: 40.7326808, longitude: -73.9843407)
        hello.title = "Hello world!"
        hello.subtitle = "Welcome to my marker"

        // Add marker `hello` to the map.
        mapView.addAnnotation(hello)
    }

    func setup() {
        self.view.addSubview(mapView)
        mapView.delegate = self
    }


    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }


    // Use the default marker. See also: our view annotation or custom marker examples.
    func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
        return nil
    }

    // Allow callout view to appear when an annotation is tapped.
    func mapView(_ mapView: MGLMapView, annotationCanShowCallout annotation: MGLAnnotation) -> Bool {
        return true
    }


    func mapView(_ mapView: MGLMapView, calloutViewFor annotation: MGLAnnotation) -> MGLCalloutView? {

        let title = annotation.title ?? nil
        let subtitle = annotation.subtitle ?? nil
        let image = UIImage(named: "apple.png")!
        let customAnnotation = CustomAnnotation(coordinate: annotation.coordinate, title: title ?? "no title", subtitle: subtitle ?? "no subtitle", image: image)

        return CustomCalloutView(annotation: customAnnotation)
    }
}

自定义Callout视图

import Foundation
import Mapbox

class CustomCalloutView: UIView, MGLCalloutView {

    var representedObject: MGLAnnotation
    // Required views but unused for now, they can just relax
    lazy var leftAccessoryView = UIView()
    lazy var rightAccessoryView = UIView()

    weak var delegate: MGLCalloutViewDelegate?

    //MARK: Subviews -
    let titleLabel:UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.font = UIFont.boldSystemFont(ofSize: 17.0)
        return label
    }()

    let subtitleLabel:UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()

    let imageView:UIImageView = {
        let imageview = UIImageView(frame: CGRect(x: 0, y: 0, width: 25, height: 25))
        imageview.translatesAutoresizingMaskIntoConstraints = false
        imageview.contentMode = .scaleAspectFit
        return imageview
    }()

    required init(annotation: CustomAnnotation) {
        self.representedObject = annotation
        // init with 75% of width and 120px tall
        super.init(frame: CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: UIScreen.main.bounds.width * 0.75, height: 120.0)))

        self.titleLabel.text = self.representedObject.title ?? ""
        self.subtitleLabel.text = self.representedObject.subtitle ?? ""
        self.imageView.image = annotation.image
        setup()
    }

    required init?(coder decoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func setup() {
        // setup this view's properties
        self.backgroundColor = UIColor.white

        // And their Subviews
        self.addSubview(titleLabel)
        self.addSubview(subtitleLabel)
        self.addSubview(imageView)

        // Add Constraints to subviews
        let spacing:CGFloat = 8.0

        imageView.topAnchor.constraint(equalTo: self.topAnchor, constant: spacing).isActive = true
        imageView.leftAnchor.constraint(equalTo: self.leftAnchor, constant: spacing).isActive = true
        imageView.heightAnchor.constraint(equalToConstant: 52.0).isActive = true
        imageView.widthAnchor.constraint(equalToConstant: 52.0).isActive = true

        titleLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: spacing).isActive = true
        titleLabel.leftAnchor.constraint(equalTo: self.imageView.rightAnchor, constant: spacing * 2).isActive = true
        titleLabel.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -spacing).isActive = true
        titleLabel.heightAnchor.constraint(equalToConstant: 50.0).isActive = true

        subtitleLabel.topAnchor.constraint(equalTo: self.titleLabel.bottomAnchor, constant: spacing).isActive = true
        subtitleLabel.leftAnchor.constraint(equalTo: self.leftAnchor, constant: spacing).isActive = true
        subtitleLabel.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -spacing).isActive = true
        subtitleLabel.heightAnchor.constraint(equalToConstant: 20.0).isActive = true
    }


    func presentCallout(from rect: CGRect, in view: UIView, constrainedTo constrainedRect: CGRect, animated: Bool) {
        //Always, Slightly above center
        self.center = view.center.applying(CGAffineTransform(translationX: 0, y: -self.frame.height))
        view.addSubview(self)

    }

    func dismissCallout(animated: Bool) {
        if (animated){
            //do something cool
            removeFromSuperview()
        } else {
            removeFromSuperview()
        }

    }
}

非常感谢您提供这个完整且解释得很清楚的答案。 - Magnas
谢谢!我会尽力提供优质内容! - RLoniello
@RLoniello,当你移动地图时,Callout是否与标记一起移动,还是会立即消失?这是我的错误还是SDK的问题? - just_deko
如果我没记错的话,当地图被滚动时,呼出框会消失。但是标记将保留在地图上。从一个新项目中尝试并查看! - RLoniello
非常感谢你,兄弟!你救了我的一天。 - Kerim Khasbulatov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接