0

I am looking to have a custom view displayed above a certain point on the map( the start and end of a polyline ). It would look something like this: enter image description here

The view should look exactly like that on load, without tapping on the screen.

Lobont Andrei
  • 80
  • 1
  • 8
  • Do you need the annotation view and the label appear like in your image? Or are willing to consider the standard marker view? And what iOS versions do you need to support? – Rob Jul 05 '20 at 05:26
  • Also, re that label & disclosure indicator button below the annotation view. Is that always going to be present? E.g., if you tap elsewhere on the map, do you want that to disappear? I.e. is it a callout that appears and disappears as the destination annotation view is selected and deselected and you just want it to initially appear selected, or is it a permanent view that can never disappear? – Rob Jul 05 '20 at 06:14
  • @Rob I need the view to appear just like in the image. Only iOS 12 and above will be supported. – Lobont Andrei Jul 05 '20 at 06:38

2 Answers2

0

You should use MKMarkerAnnotationView, I found this article very helpful: http://infinityjames.com/blog/mapkit-ios11

It is mainly about clustering but there is also shown how to create a point with your own graphics.

mateuszm7
  • 49
  • 6
0

The basic idea is that you subclass MKAnnotationView and set its image.

You can implement the button below the annotation view any way you want, but make sure to implement a hitTest(_:with:) so that taps are correctly recognized.

E.g.

class CustomAnnotationView: MKAnnotationView {

    static let lineWidth: CGFloat = 2
    static let pinSize = CGSize(width: 30, height: 37)
    static let pinImage: UIImage = image(with: .blue)

    private let button: UIButton = DisclosureIndicatorButton()

    override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
        super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)

        image = Self.pinImage
        centerOffset = CGPoint(x: 0, y: -Self.pinSize.height / 2)

        configure(for: annotation)
        configureButton()
    }
        
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override var annotation: MKAnnotation? {
        didSet {
            configure(for: annotation)
        }
    }
    
    // this is required for a tap on the callout/button below the annotation view to be recognized

    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        guard let hitView = super.hitTest(point, with: event) else {
            let pointInButton = convert(point, to: button)
            return button.hitTest(pointInButton, with: event)
        }

        return hitView
    }
}

private extension CustomAnnotationView {
    func configure(for annotation: MKAnnotation?) {
        canShowCallout = false
        button.setTitle(annotation?.title ?? "Unknown", for: .normal)

        // if you were also doing clustering, you do that configuration here ...
    }

    func configureButton() {
        addSubview(button)

        NSLayoutConstraint.activate([
            button.centerXAnchor.constraint(equalTo: centerXAnchor),
            button.topAnchor.constraint(equalTo: bottomAnchor, constant: 20)
        ])

        button.addTarget(self, action: #selector(didTapButton(_:)), for: .touchUpInside)
    }

    /// Function to create pin image of the desired color
    ///
    /// You could obviously just create an pre-rendered image in your asset collection,
    /// but I find it just as easy to render them programmatically.

    static func image(with color: UIColor) -> UIImage {
        UIGraphicsImageRenderer(size: pinSize).image { _ in
            let bounds = CGRect(origin: .zero, size: pinSize)
            let rect = bounds.insetBy(dx: lineWidth / 2, dy: lineWidth / 2)
            let r = rect.width / 2
            let h = rect.height - r
            let theta = acos(r / h)
            let center = CGPoint(x: rect.midX, y: rect.midX)
            let path = UIBezierPath(arcCenter: center, radius: r, startAngle: .pi / 2 - theta, endAngle: .pi / 2 + theta, clockwise: false)
            path.addLine(to: CGPoint(x: rect.midX, y: rect.maxY))
            path.lineWidth = lineWidth
            path.close()

            color.setFill()
            path.fill()
            UIColor.white.setStroke()
            path.stroke()

            let path2 = UIBezierPath(arcCenter: center, radius: r / 3, startAngle: 0, endAngle: .pi * 2, clockwise: true)
            UIColor.white.setFill()
            path2.fill()
        }
    }

    /// Button handler
    ///
    /// Note, taps on the button are passed to map view delegate via
    /// mapView(_:annotationView:calloutAccessoryControlTapped)`.
    ///
    /// Obviously, you could use your own delegate protocol if you wanted.

    @objc func didTapButton(_ sender: Any) {
        if let mapView = mapView, let delegate = mapView.delegate {
            delegate.mapView?(mapView, annotationView: self, calloutAccessoryControlTapped: button)
        }
    }

    /// Map view
    ///
    /// Navigate up view hierarchy until we find `MKMapView`.

    var mapView: MKMapView? {
        var view = superview
        while view != nil {
            if let mapView = view as? MKMapView { return mapView }
            view = view?.superview
        }
        return nil
    }
}

That results in:

enter image description here

The creation of the button below the annotation view is beyond the scope of the question and could be created like one of the answers discussed in How do I put the image on the right side of the text in a UIButton? In the above example, I’ve just wrapped one of the those solutions in its own class, DisclosureIndicatorButton, but I don’t really want to wade into the debate about the preferred approach, so I’ll let you pick whichever you want.

Rob
  • 415,655
  • 72
  • 787
  • 1,044