首页 文章

将标注/语音气泡添加到MapView的用户注释中

提问于
浏览
1

现在我的应用程序显示一个customUserAnnotationView,其中包含用户注释所在的自定义图像(您可以在ViewController.swift中看到这一点) . 我还创建了一个自定义UIView,我想在用户注释的上方用作注释(它的代码和图像在SpeechBubble.swift下) .

我想组合这两个对象,以便我可以在上面的注释中放置Custom UIView(SpeechBubble.swift)来显示CustomUserAnnotationView .

我试图制定弗兰肯斯坦计划的努力并没有给我带来好处 . 我只想放置我在图像上方创建的自定义注释类,并可能添加一个小三角形,使其看起来像一个语音泡泡 .


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)
    }
}

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
    }

}

Avatar.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
    }
}

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




UPDATE 我试图创建一个Frankenstein程序的答案和我的代码,并在SpeechBubble.swift中收到以下错误 Property 'self.representedObject' not initialized at super.init call . 我还将所有旧代码从speechBubble.swift移到insideSpeechBubble.swift中

Updated 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()
    }
}

Updated 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 回答

  • 1

    当我为我的Mapbox注释实现自定义标注时,我使用了一个xib文件来设计实际的标注 . 我发现它给了我更多的即时反馈,而不是试图从代码中唤起用户界面(但显然可以做任何你喜欢的事情) .

    这给了我类似以下的东西 .

    使用UIImage作为背景可以让我实现我选择的任何形状 . 在这里,我使用白色的透明度给我圆形元素和你在问题中提到的底部三角形 .

    此UIView的Swift文件(您的SpeechBubble)需要符合 MGLCalloutView 协议,而不是当前的 MGLMapViewDelegate . 您的 ViewControllerMGLMapViewDelegate ,而不是您的自定义标注 . 在IB中的Identity Inspector中以通常的方式配对xib文件和Swift文件 . 所以会是这样的:

    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
        }
    

    希望这会让你更接近实现你想要做的事情 . 顺便说一句,这个问题的版本比第一个版本提前了几英里 .

相关问题