Добавление выноски / речевого пузыря в пользовательскую аннотацию MapView - PullRequest
0 голосов
/ 09 декабря 2018

Прямо сейчас мое приложение отображает customUserAnnotationView с пользовательским изображением, где находится пользовательская аннотация (вы можете увидеть это в ViewController.swift).Я также создал пользовательский UIView, который хочу использовать в качестве аннотации чуть выше пользовательской аннотации (код и изображение для нее находятся в SpeechBubble.swift).

Я хочу объединить эти два объекта так, чтобы яможет отображать CustomUserAnnotationView с пользовательским UIView (SpeechBubble.swift), размещенным в аннотации выше.

Мои попытки создания программы Франкенштейна из несколько mapbox учебники не сработали для меня.Я только хочу разместить пользовательский класс аннотаций, который я создал, над изображением и, возможно, добавить небольшой треугольник, чтобы он выглядел как речевой пузырь.


ViewController.swift

import Mapbox

class ViewController: UIViewController, MGLMapViewDelegate {
    override func viewDidLoad() {
        super.viewDidLoad()
        let mapView = MGLMapView(frame: view.bounds)
        mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        mapView.delegate = self

        // Enable heading tracking mode so that the arrow will appear.
        mapView.userTrackingMode = .followWithHeading

        // Enable the permanent heading indicator, which will appear when the tracking mode is not `.followWithHeading`.
        mapView.showsUserHeadingIndicator = true

        view.addSubview(mapView)

        let idea =  UITextView(frame: CGRect(x: 0, y: 0, width: 100, height: 40))
        idea.text = "Hello There"
        idea.textAlignment = NSTextAlignment.center

        let sb = SpeechBubble(coord: mapView.targetCoordinate, idea: idea)
        mapView.addSubview(sb)
    }

    func mapView(_ mapView: MGLMapView, annotationCanShowCallout annotation: MGLAnnotation) -> Bool {
        return true
    }

    func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
        // Substitute our custom view for the user location annotation. This custom view is defined below.
        if annotation is MGLUserLocation && mapView.userLocation != nil {
            return Avatar()
        }
        return nil
    }

    // Optional: tap the user location annotation to toggle heading tracking mode.
    func mapView(_ mapView: MGLMapView, didSelect annotation: MGLAnnotation) {
        if mapView.userTrackingMode != .followWithHeading {
            mapView.userTrackingMode = .followWithHeading
        } else {
            mapView.resetNorth()
        }

        // We're borrowing this method as a gesture recognizer, so reset selection state.
        mapView.deselectAnnotation(annotation, animated: false)
    }
}

enter image description here


SpeechBubble.swift

import UIKit
import Mapbox

class SpeechBubble: UIView, MGLMapViewDelegate{

    //var sbView: UIView

    init(coord: CLLocationCoordinate2D, idea: UITextView) {

        let width = CGFloat(180)
        let height = UITextField.layoutFittingExpandedSize.height + 32
        super.init(frame: CGRect(x: CGFloat(coord.latitude), y: CGFloat(coord.longitude), width: width, height: height))

        self.addSubview(idea)
        self.addSubview(buttonsView());
        self.addSubview(upvoteView());
    }

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

    func upvoteView() -> UIView {
        let uView = UIView()

        let vCnt = UILabel(frame: CGRect(x: 0, y: 0, width: 200, height: 21))
        vCnt.center = CGPoint(x: 10.5, y: 32)
        vCnt.textAlignment = .center
        vCnt.text = "0"

        let uButton  = UIButton(type: .custom)
        uButton.frame = CGRect(x: vCnt.frame.size.width + 5, y: 0, width: 32, height: 32);
        let uImage = UIImage (named: "Upvote")
        uButton.setImage(uImage, for: .normal)

        uView.frame.size.width = vCnt.frame.size.width + uButton.frame.size.width + 5
        uView.frame.size.height = max(vCnt.frame.size.height, uButton.frame.size.height)

        uView.frame = CGRect(
            x: 0,
            y: self.frame.size.height - uView.frame.size.height,
            width: uView.frame.size.width,
            height: uView.frame.size.height );
        uView.addSubview(vCnt)
        uView.addSubview(uButton)
        return uView
    }

    func buttonsView() -> UIView {
        let bView = UIView()

        let jButton  = UIButton(type: .custom)
        rButton.frame = CGRect(x: 0, y: 0, width: 35, height: 32);
        let rImage = UIImage (named: "Rocket")
        rButton.setImage(rImage, for: .normal)

        let pButton  = UIButton(type: .custom)
        pButton.frame = CGRect(x: jButton.frame.size.width + 5, y: 0, width: 31, height: 36);
        let pImage = UIImage (named: "Profile")
        pButton.setImage(pImage, for: .normal)

        bView.frame.size.width = rButton.frame.size.width + pButton.frame.size.width + 5
        bView.frame.size.height = max(rButton.frame.size.height, pButton.frame.size.height)

        bView.frame = CGRect(
            x: self.frame.size.width - bView.frame.size.width,
            y: self.frame.size.height - bView.frame.size.height,
            width: bView.frame.size.width,
            height: bView.frame.size.height );
        bView.addSubview(rButton)
        bView.addSubview(pButton)
        return bView
    }

}

enter image description here


Аватар.swift

import Mapbox

class Avatar: MGLUserLocationAnnotationView {
    let size: CGFloat = 48
    var arrow: CALayer!
    //var arrow: CAShapeLayer!

    // -update is a method inherited from MGLUserLocationAnnotationView. It updates the appearance of the user location annotation when needed. This can be called many times a second, so be careful to keep it lightweight.
    override func update() {
        if frame.isNull {
            frame = CGRect(x: 0, y: 0, width: size, height: size)
            return setNeedsLayout()
        }

        // Check whether we have the user’s location yet.
        if CLLocationCoordinate2DIsValid(userLocation!.coordinate) {
            setupLayers()
            updateHeading()
        }
    }

    private func updateHeading() {
        // Show the heading arrow, if the heading of the user is available.
        if let heading = userLocation!.heading?.trueHeading {
            arrow.isHidden = false

            // Get the difference between the map’s current direction and the user’s heading, then convert it from degrees to radians.
            let rotation: CGFloat = -MGLRadiansFromDegrees(mapView!.direction - heading)

            // If the difference would be perceptible, rotate the arrow.
            if abs(rotation) > 0.01 {
                // Disable implicit animations of this rotation, which reduces lag between changes.
                CATransaction.begin()
                CATransaction.setDisableActions(true)
                arrow.setAffineTransform(CGAffineTransform.identity.rotated(by: rotation))
                CATransaction.commit()
            }
        } else {
            arrow.isHidden = true
        }
    }

    private func setupLayers() {
        // This dot forms the base of the annotation.
        if arrow == nil {
            arrow = CALayer()
            let myImage = UIImage(named: "will_smith")?.cgImage
            arrow.bounds = CGRect(x: 0, y: 0, width: size, height: size)
            arrow.contents = myImage
            layer.addSublayer(arrow)
        }

    }

    // Calculate the vector path for an arrow, for use in a shape layer.
    private func arrowPath() -> CGPath {
        let max: CGFloat = size / 2
        let pad: CGFloat = 3

        let top =    CGPoint(x: max * 0.5, y: 0)
        let left =   CGPoint(x: 0 + pad,   y: max - pad)
        let right =  CGPoint(x: max - pad, y: max - pad)
        let center = CGPoint(x: max * 0.5, y: max * 0.6)

        let bezierPath = UIBezierPath()
        bezierPath.move(to: top)
        bezierPath.addLine(to: left)
        bezierPath.addLine(to: center)
        bezierPath.addLine(to: right)
        bezierPath.addLine(to: top)
        bezierPath.close()

        return bezierPath.cgPath
    }
}

--------------------------------------------------------------------------------------------------------




ОБНОВЛЕНИЕ Я попытался создать программу ответа Франкенштейна и мой коди получаю следующее сообщение об ошибке Property 'self.representedObject' not initialized at super.init call в SpeechBubble.swift.Я также перемещаю весь свой старый код из speechBubble.swift в insideSpeechBubble.swift

Обновлен SpeechBubble.swift

    import UIKit
    import Mapbox

    class SpeechBubble: UIView, MGLCalloutView {

        // Your IBOutlets //

        var representedObject: MGLAnnotation
        var annotationPoint: CGPoint
        // Required views but unused for this implementation.
        lazy var leftAccessoryView = UIView()
        lazy var rightAccessoryView = UIView()
        var contentView: MGLMapView
        weak var delegate: MGLCalloutViewDelegate?

        // MARK: - init methods

        required init(annotation: MGLAnnotation, frame: CGRect, annotationPoint: CGPoint) {
            let idea =  UITextView(frame: CGRect(x: 0, y: 0, width: 100, height: 40))
            idea.text = "Hello There"
            idea.textAlignment = NSTextAlignment.center
            self.representedObject = annotation
            self.annotationPoint = annotationPoint
            contentView = InsideSpeechBubble(coord: annotationPoint, idea: idea )
            super.init(frame: frame)
            commonInit()
        }

        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            commonInit()
        }

        private func commonInit() {
            Bundle.main.loadNibNamed("SpeechBubble", owner: self, options: nil)
            addSubview(contentView as UIView)
            contentView.frame = self.bounds

            // Do your initialisation //
        }

        // MARK: - MGLCalloutView methods

        func presentCallout(from rect: CGRect, in view: UIView, constrainedTo constrainedRect: CGRect, animated: Bool) {
            // Present the custom callout slightly above the annotation's view. Initially invisble.
            self.center = annotationPoint.applying(CGAffineTransform(translationX: 0, y: -self.frame.height - 20.0))

            // I have logic here for setting the correct image and button states //
        }

        func dismissCallout(animated: Bool) {
            removeFromSuperview()
    }
}

Обновлен ViewController.swift

import Mapbox


class ViewController: UIViewController, MGLMapViewDelegate {
    //let point = MGLPointAnnotation()

    override func viewDidLoad() {
        super.viewDidLoad()
        let mapView = MGLMapView(frame: view.bounds)
        mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        mapView.delegate = self

        // Enable heading tracking mode so that the arrow will appear.
        mapView.userTrackingMode = .followWithHeading

        // Enable the permanent heading indicator, which will appear when the tracking mode is not `.followWithHeading`.
        mapView.showsUserHeadingIndicator = true

        view.addSubview(mapView)

        let HighDea =  UITextView(frame: CGRect(x: 0, y: 0, width: 100, height: 40))
        HighDea.text = "Hello There"
        HighDea.textAlignment = NSTextAlignment.center

        //let sb = SpeechBubble()
        //mapView.addSubview(sb)
    }

    func mapView(_ mapView: MGLMapView, annotationCanShowCallout annotation: MGLAnnotation) -> Bool {
        return true
    }

    func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
        // Substitute our custom view for the user location annotation. This custom view is defined below.
        if annotation is MGLUserLocation && mapView.userLocation != nil {
            return Avatar()
        }
        return nil
    }

    func mapView(_ mapView: MGLMapView, calloutViewFor annotation: MGLAnnotation) -> MGLCalloutView? {
        // Do your annotation-specific preparation here //

        // I get the correct size from my xib file.
        let viewFrame = CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: 261.0, height: 168.0))
        // Get the annotation's location in the view's coordinate system.
        let annotationPoint = mapView.convert(annotation.coordinate, toPointTo: nil)

        let customCalloutView = SpeechBubble(annotation: annotation, frame: viewFrame, annotationPoint: annotationPoint)

        return customCalloutView
    }

//    func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
        // This example is only concerned with point annotations.
//        guard annotation is MGLPointAnnotation else {
//            return nil
//        }

        // Use the point annotation’s longitude value (as a string) as the reuse identifier for its view.
//        let reuseIdentifier = "\(annotation.coordinate.longitude)"

        // For better performance, always try to reuse existing annotations.
//        var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseIdentifier)

        // If there’s no reusable annotation view available, initialize a new one.
//        if annotationView == nil {
//            annotationView = CustomAnnotationView(reuseIdentifier: reuseIdentifier)
//            annotationView!.bounds = CGRect(x: 0, y: 0, width: 40, height: 40)

            // Set the annotation view’s background color to a value determined by its longitude.
  //          let hue = CGFloat(annotation.coordinate.longitude) / 100
  //          annotationView!.backgroundColor = UIColor(hue: hue, saturation: 0.5, brightness: 1, alpha: 1)
  //      }

  //      return annotationView
  //  }

    // Optional: tap the user location annotation to toggle heading tracking mode.
    func mapView(_ mapView: MGLMapView, didSelect annotation: MGLAnnotation) {
        if mapView.userTrackingMode != .followWithHeading {
            mapView.userTrackingMode = .followWithHeading
        } else {
            mapView.resetNorth()
        }

        // We're borrowing this method as a gesture recognizer, so reset selection state.
        mapView.deselectAnnotation(annotation, animated: false)
    }
}

1 Ответ

0 голосов
/ 10 декабря 2018

Когда я реализовал пользовательскую выноску для своих аннотаций Mapbox, я использовал файл xib для разработки фактической выноски.Я считаю, что это дает мне гораздо больше мгновенной обратной связи, чем попытка вызвать пользовательский интерфейс из кода (но, очевидно, делать то, что вы предпочитаете).

enter image description here

Что дает мне что-то вроде следующего.

enter image description here

Использование UIImage для фона позволяет мне получить любую форму, которую я выберу.Здесь я использую прозрачность вокруг белого, чтобы получить круговые элементы и нижний треугольник, который вы упомянули в своем вопросе.

Файл Swift для этого UIView (ваш SpeechBubble) должен соответствовать протоколу MGLCalloutView, а не MGLMapViewDelegate как у вас сейчас.Ваш ViewController - это MGLMapViewDelegate, а не ваш пользовательский выноска.Соедините файл xib и файл Swift обычным способом в Identity Inspector в IB.Итак, было бы что-то вроде этого:

    import UIKit
    import Mapbox

    class SpeechBubble: UIView, MGLCalloutView {

    // Your IBOutlets //
    @IBOutlet var contentView: UIView! // The custom callout's view.

    var representedObject: MGLAnnotation
    var annotationPoint: CGPoint
    // Required views but unused for this implementation.
    lazy var leftAccessoryView = UIView()
    lazy var rightAccessoryView = UIView()

    weak var delegate: MGLCalloutViewDelegate?

    // MARK: - init methods

    required init(annotation: YourAnnotation, frame: CGRect, annotationPoint: CGPoint) {
        self.representedObject = annotation
        self.annotationPoint = annotationPoint

        super.init(frame: frame)

        commonInit()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)

        commonInit()
    }

    private func commonInit() {
        Bundle.main.loadNibNamed("SpeechBubble", owner: self, options: nil)
        addSubview(contentView)
        contentView.frame = self.bounds

        // Do your initialisation //
    }

    // MARK: - MGLCalloutView methods

    func presentCallout(from rect: CGRect, in view: UIView, constrainedTo constrainedRect: CGRect, animated: Bool) {
        // Present the custom callout slightly above the annotation's view. Initially invisble.
        self.center = annotationPoint.applying(CGAffineTransform(translationX: 0, y: -self.frame.height - 20.0))

        // I have logic here for setting the correct image and button states //
    }

    func dismissCallout(animated: Bool) {
        removeFromSuperview()
    }

Тогда вы, похоже, просто упускаете метод MGLMapViewDelegate для фактического возврата вашего SpeechBubble представления по запросу.Он должен быть в вашем файле ViewController.

    func mapView(_ mapView: MGLMapView, calloutViewFor annotation: MGLAnnotation) -> MGLCalloutView? {
        // Do your annotation-specific preparation here //

        // I get the correct size from my xib file.
        let viewFrame = CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: 261.0, height: 168.0))
        // Get the annotation's location in the view's coordinate system.
        let annotationPoint = mapView.convert(annotation.coordinate, toPointTo: nil)
        let customCalloutView = SpeechBubble(annotation: YourAnnotation, frame: viewFrame, annotationPoint: annotationPoint)

        return customCalloutView
    }

Надеюсь, это приблизит вас к достижению того, что вы пытаетесь сделать.Кстати, эта версия вашего вопроса намного опережает первую.

РЕДАКТИРОВАТЬ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Это будетпочти невозможно проработать это без внимания вашего проекта, поэтому я собрал голую реализацию.Он основан на примере Mapbox: Mapbox Custom Callout , который по какой-то причине не показывает, как на самом деле предоставить представление выноски.Я также расширил его, чтобы учесть пользовательское изображение аннотации.Если вы можете заставить это работать, вы сможете перемещать соответствующие части в свой собственный проект.

Я настоятельно рекомендую вам, если вы попытаетесь реализовать описанные ниже вещи, сделать это в новом проекте.

Контроллер представления.

    import Mapbox

    class ViewController: UIViewController, MGLMapViewDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()

        let mapView = MGLMapView(frame: view.bounds, styleURL: MGLStyle.lightStyleURL)
        mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        mapView.tintColor = .darkGray
        view.addSubview(mapView)

        // Set the map view‘s delegate property.
        mapView.delegate = self

        // Initialize and add the marker annotation.
        let coordinate = CLLocationCoordinate2D(latitude: 0, longitude: 0)
        let marker = MyAnnotation(coordinate: coordinate, title: "Bingo", subtitle: "Bongo")

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

    func mapView(_ mapView: MGLMapView, annotationCanShowCallout annotation: MGLAnnotation) -> Bool {
        return true
    }

    func mapView(_ mapView: MGLMapView, calloutViewFor annotation: MGLAnnotation) -> MGLCalloutView? {
        // Instantiate and return our custom callout view.
        let annotationPoint = mapView.convert(annotation.coordinate, toPointTo: nil)
        let viewFrame = CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: 250.0, height: 178.0))
        return CustomCalloutView(representedObject: annotation, frame: viewFrame, annotationPoint: annotationPoint)
    }

    func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
        if let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "myAnnotationView") {
            return annotationView
        } else {
            let annotationView = MyAnnotationView(reuseIdentifier: "myAnnotationView", size: CGSize(width: 45, height: 45), annotation: annotation)
            return annotationView
        }
    }

    func mapView(_ mapView: MGLMapView, tapOnCalloutFor annotation: MGLAnnotation) {
        // Optionally handle taps on the callout.
        print("Tapped the callout for: \(annotation)")

        // Hide the callout.
        mapView.deselectAnnotation(annotation, animated: true)
    }
}

CustomCalloutView.swift

import UIKit
import Mapbox

class CustomCalloutView: UIView, MGLCalloutView {

@IBOutlet var contentView: UIView!

weak var delegate: MGLCalloutViewDelegate?

var representedObject: MGLAnnotation
var annotationPoint: CGPoint
// Required views but unused for this implementation.
lazy var leftAccessoryView = UIView()
lazy var rightAccessoryView = UIView()

required init(representedObject: MGLAnnotation, frame: CGRect, annotationPoint: CGPoint) {
    self.representedObject = representedObject
    self.annotationPoint = annotationPoint

    super.init(frame: frame)

    commonInit()
}

required init?(coder aDecoder: NSCoder) {
    let coordinate = CLLocationCoordinate2D(latitude: 0.0, longitude: 0.0)
    self.representedObject = MyAnnotation(coordinate: coordinate, title: "", subtitle: "")
    self.annotationPoint = CGPoint(x: 50.0, y: 50.0)

    super.init(coder: aDecoder)

    commonInit()
}

func commonInit() {
    Bundle.main.loadNibNamed("CustomCalloutView", owner: self, options: nil)
    addSubview(contentView)
}

func presentCallout(from rect: CGRect, in view: UIView, constrainedTo constrainedRect: CGRect, animated: Bool) {
    // Present the custom callout slightly above the annotation's view. Initially invisble.
    self.center = annotationPoint.applying(CGAffineTransform(translationX: 0.0, y: -120.0))

    view.addSubview(self)
}

func dismissCallout(animated: Bool) {
    removeFromSuperview()
}

}

Это связано / идентифицируется с файлом XIB.Это просто содержит простую форму изображения на данный момент.Мне пришлось (заново) представить IBOutlet contentView, так как у меня были проблемы с загрузкой вещей из Bundle и добавлением их в self в commonInit (), которые сделали все счастливым.

enter image description here

Пользовательский класс аннотаций.

    import UIKit
    import Mapbox

    // MGLAnnotation protocol reimplementation
    class MyAnnotation: NSObject, MGLAnnotation {

    // As a reimplementation of the MGLAnnotation protocol, we have to add mutable coordinate and (sub)title properties ourselves.
    var coordinate: CLLocationCoordinate2D
    var title: String?
    var subtitle: String?

    // Custom properties that we will use to customize the annotation.
    var image: UIImage?
    var reuseIdentifier: String?

    init(coordinate: CLLocationCoordinate2D, title: String?, subtitle: String?) {
        self.coordinate = coordinate
        self.title = title
        self.subtitle = subtitle

        self.reuseIdentifier = "myAnnotation"
    }
}

Подкласс MGLAnnotationView.

    import UIKit
    import Mapbox

    class MyAnnotationView: MGLAnnotationView {

    init(reuseIdentifier: String, size: CGSize, annotation: MGLAnnotation) {
        super.init(reuseIdentifier: reuseIdentifier)

        // This property prevents the annotation from changing size when the map is tilted.
        scalesWithViewingDistance = false

        // Begin setting up the view.
        frame = CGRect(x: 0, y: 0, width: size.width, height: size.height)

        let imageView = UIImageView(frame: frame)
        var image = UIImage()
        if annotation is MyAnnotation {
            image = UIImage(named: "frog")!
        }

        imageView.image = image
        addSubview(imageView)
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
    }

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

enter image description here

Естественно, естьмного жестко закодированных чисел и требование для изображения, называемого лягушкой, но вы можете изменить все это и улучшить его, как вы хотите.CustomCalloutView.swift и CustomCalloutView.xib должны быть связаны обычным способом в инспекторе идентификации и т. Д.

...