Подтвердить что ты не робот

Стрелка заголовка iOS 10 для точки MKUserLocation

Приложение Maps в iOS 10 теперь включает стрелку направления курса вверху MKUserLocation MKAnnotationView. Есть ли способ добавить это к MKMapView в моих собственных приложениях?

введите описание изображения здесь

Изменить: Я был бы рад сделать это вручную, но я не уверен, возможно ли это? Могу ли я добавить аннотацию к карте и следить за ее местоположением, включая анимированные ходы?

4b9b3361

Ответ 1

Я решил это, добавив subview в MKUserLocation annotationView, например

func mapView(mapView: MKMapView, didAddAnnotationViews views: [MKAnnotationView]) {
if annotationView.annotation is MKUserLocation {
    addHeadingViewToAnnotationView(annotationView)
    }
}

func addHeadingViewToAnnotationView(annotationView: MKAnnotationView) {
    if headingImageView == nil {
        if let image = UIImage(named: "icon-location-heading-arrow") {
            let headingImageView = UIImageView()
            headingImageView.image = image
            headingImageView.frame = CGRectMake((annotationView.frame.size.width - image.size.width)/2, (annotationView.frame.size.height - image.size.height)/2, image.size.width, image.size.height)
            self.headingImageView = headingImageView
        }
    }

    headingImageView?.removeFromSuperview()
    if let headingImageView = headingImageView {
        annotationView.insertSubview(headingImageView, atIndex: 0)
    }

    //use CoreLocation to monitor heading here, and rotate headingImageView as required
}

Ответ 2

Я также столкнулся с этой же проблемой (мне нужен индикатор ориентации без вращения карты, как в приложении Apple Maps). К сожалению, Apple пока не выпустила API "синий значок для заголовка".

Я создал следующее решение, полученное из реализации @alku83.

  1. Убедитесь, что класс соответствует MKViewDelegate
  2. Добавьте метод делегата, чтобы добавить значок с синей стрелкой в точку расположения карты

    func mapView(_ mapView: MKMapView, didAdd views: [MKAnnotationView]) {
        if views.last?.annotation is MKUserLocation {
            addHeadingView(toAnnotationView: views.last!)
        }
    }
    
  3. Добавьте метод для создания "синей стрелки".

    func addHeadingView(toAnnotationView annotationView: MKAnnotationView) {
        if headingImageView == nil {
            let image = #YOUR BLUE ARROW ICON#
            headingImageView = UIImageView(image: image)
            headingImageView!.frame = CGRect(x: (annotationView.frame.size.width - image.size.width)/2, y: (annotationView.frame.size.height - image.size.height)/2, width: image.size.width, height: image.size.height)
            annotationView.insertSubview(headingImageView!, at: 0)
            headingImageView!.isHidden = true
         }
    }
    
  4. Добавьте var headingImageView: UIImageView? в ваш класс. Это в основном необходимо для преобразования/поворота изображения с синей стрелкой.

  5. (В другом классе/объекте в зависимости от вашей архитектуры) Создайте экземпляр менеджера местоположений, класс которого соответствует протоколу CLLocationManagerDelegate

    lazy var locationManager: CLLocationManager = {
        let manager = CLLocationManager()
        // Set up your manager properties here
        manager.delegate = self
        return manager
    }()
    
  6. Убедитесь, что ваш менеджер местоположения отслеживает данные о курсе пользователя locationManager.startUpdatingHeading() и что он останавливает отслеживание, когда это необходимо locationManager.stopUpdatingHeading()

  7. Добавьте var userHeading: CLLocationDirection?, который будет содержать значение ориентации

  8. Добавьте метод делегата, чтобы получать уведомления об изменении значений заголовка, и соответствующим образом измените значение userHeading

    func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
         if newHeading.headingAccuracy < 0 { return }
    
         let heading = newHeading.trueHeading > 0 ? newHeading.trueHeading : newHeading.magneticHeading
         userHeading = heading
         NotificationCenter.default.post(name: Notification.Name(rawValue: #YOUR KEY#), object: self, userInfo: nil)
        }
    
  9. Теперь в вашем классе, соответствующем MKMapViewDelegate, добавьте метод для "преобразования" ориентации изображения заголовка

       func updateHeadingRotation() {
            if let heading = # YOUR locationManager instance#,
                let headingImageView = headingImageView {
    
                headingImageView.isHidden = false
                let rotation = CGFloat(heading/180 * Double.pi)
                headingImageView.transform = CGAffineTransform(rotationAngle: rotation)
            }
        }
    

Ответ 3

Да, вы можете сделать это вручную.

Основная идея - отслеживать местоположение пользователя с помощью CLLocationManager и использовать его для размещения и поворота представления аннотаций на карте.

Вот код. Я опускаю некоторые вещи, которые напрямую не связаны с вопросом (например, я предполагаю, что пользователь уже разрешил ваше приложение для доступа к местоположению и т.д.), Поэтому вы, вероятно, захотите немного изменить этот код

ViewController.swift

import UIKit
import MapKit

class ViewController: UIViewController, CLLocationManagerDelegate, MKMapViewDelegate {
    @IBOutlet var mapView: MKMapView!
    lazy var locationManager: CLLocationManager = {
        let manager = CLLocationManager()
        manager.delegate = self
        return manager
    }()

    var userLocationAnnotation: UserLocationAnnotation!

    override func viewDidLoad() {
        super.viewDidLoad()

        locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation

        locationManager.startUpdatingHeading()
        locationManager.startUpdatingLocation()

        userLocationAnnotation = UserLocationAnnotation(withCoordinate: CLLocationCoordinate2D(), heading: 0.0)

        mapView.addAnnotation(userLocationAnnotation)
    }

    func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
        userLocationAnnotation.heading = newHeading.trueHeading
    }

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        userLocationAnnotation.coordinate = locations.last!.coordinate
    }

    public func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
        if let annotation = annotation as? UserLocationAnnotation {
            let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "UserLocationAnnotationView") ?? UserLocationAnnotationView(annotation: annotation, reuseIdentifier: "UserLocationAnnotationView")
            return annotationView
        } else {
            return MKPinAnnotationView(annotation: annotation, reuseIdentifier: nil)
        }
    }

}

Здесь мы делаем базовую настройку вида карты и начинаем отслеживать местоположение пользователя и заголовок с помощью CLLocationManager.

UserLocationAnnotation.swift

import UIKit
import MapKit

class UserLocationAnnotation: MKPointAnnotation {
    public init(withCoordinate coordinate: CLLocationCoordinate2D, heading: CLLocationDirection) {
        self.heading = heading

        super.init()
        self.coordinate = coordinate
    }

    dynamic public var heading: CLLocationDirection
}

Очень простой подкласс MKPointAnnotation, способный хранить направление заголовка. dynamic ключевое слово здесь. Это позволяет нам наблюдать изменения свойства heading с помощью KVO.

UserLocationAnnotationView.swift

import UIKit
import MapKit

class UserLocationAnnotationView: MKAnnotationView {

    var arrowImageView: UIImageView!

    private var kvoContext: UInt8 = 13

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

        arrowImageView = UIImageView(image: #imageLiteral(resourceName: "Black_Arrow_Up.svg"))
        addSubview(arrowImageView)
        setupObserver()
    }

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

        arrowImageView = UIImageView(image: #imageLiteral(resourceName: "Black_Arrow_Up.svg"))
        addSubview(arrowImageView)
        setupObserver()
    }

    func setupObserver() {
        (annotation as? UserLocationAnnotation)?.addObserver(self, forKeyPath: "heading", options: [.initial, .new], context: &kvoContext)
    }

    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if context == &kvoContext {
            let userLocationAnnotation = annotation as! UserLocationAnnotation
            UIView.animate(withDuration: 0.2, animations: { [unowned self] in
                self.arrowImageView.transform = CGAffineTransform(rotationAngle: CGFloat(userLocationAnnotation.heading / 180 * M_PI))
            })
        }
    }

    deinit {
        (annotation as? UserLocationAnnotation)?.removeObserver(self, forKeyPath: "heading")
    }
}

MKAnnotationView, который выполняет наблюдение за свойством heading, а затем устанавливает для него соответствующее преобразование поворота (в моем случае это просто изображение со стрелкой. Вы можете создать более сложный вид аннотации и повернуть только некоторые часть его, а не весь вид.)

UIView.animate является необязательным. Он добавляется, чтобы сделать вращение более плавным. CLLocationManager не способен наблюдать за значением заголовка 60 раз в секунду, поэтому при быстром вращении анимация может быть немного изменчивой. UIView.animate вызов решает эту крошечную проблему.

Правильная обработка обновлений значений coordinate уже реализована в классах MKPointAnnotation, MKAnnotationView и MKMapView для нас, поэтому нам не нужно делать это самостоятельно.

Ответ 4

Интересно, почему никто не предложил решение delegate. Он не опирается на MKUserLocation, а скорее использует подход, предложенный @Dim_ov, по большей части, то есть подклассы MKPointAnnotation и MKAnnotationView (самый чистый и наиболее общий способ IMHO). Единственное отличие состоит в том, что теперь наблюдатель заменяется методом delegate.

  1. Создайте протокол delegate:

    protocol HeadingDelegate : AnyObject {
        func headingChanged(_ heading: CLLocationDirection)
    }
    
  2. Создайте подкласс MKPointAnnotation, который уведомит делегата. Свойство headingDelegate будет назначено извне от контроллера представления и будет запускаться каждый раз, когда изменяется свойство heading:

    class Annotation : MKPointAnnotation {
        weak var headingDelegate: HeadingDelegate?
        var heading: CLLocationDirection {
            didSet {
                headingDelegate?.headingChanged(heading)
            }
        }
    
        init(_ coordinate: CLLocationCoordinate2D, _ heading: CLLocationDirection) {
            self.heading = heading
            super.init()
            self.coordinate = coordinate
        }
    }
    
  3. Создайте подкласс MKAnnotationView, который реализует делегат:

    class AnnotationView : MKAnnotationView , HeadingDelegate {
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
        }
    
        override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
            super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
        }
    
        func headingChanged(_ heading: CLLocationDirection) {
            // For simplicity the affine transform is done on the view itself
            UIView.animate(withDuration: 0.1, animations: { [unowned self] in
                self.transform = CGAffineTransform(rotationAngle: CGFloat(heading / 180 * .pi))
            })
        }
    }
    
  4. Учитывая, что ваш контроллер представления реализует как CLLocationManagerDelegate, так и MKMapViewDelegate, осталось сделать очень мало (не предоставляя полный код контроллера представления здесь):

        // Delegate method of the CLLocationManager
        func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
            userAnnotation.heading = newHeading.trueHeading
        }
    
        // Delegate method of the MKMapView
        func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {        
            var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: NSStringFromClass(Annotation.self))
            if (annotationView == nil) {
                annotationView = AnnotationView(annotation: annotation, reuseIdentifier: NSStringFromClass(Annotation.self))
            } else {
                annotationView!.annotation = annotation
            }
    
            if let annotation = annotation as? Annotation {
                annotation.headingDelegate = annotationView as? HeadingDelegate
                annotationView!.image = /* arrow image */
            }
    
            return annotationView
        }
    

Наиболее важной частью является то, где свойство делегата аннотации (headingDelegate) назначается с объектом представления аннотации. Это связывает аннотацию с этим представлением так, что каждый раз, когда изменяется свойство заголовка, вызывается метод представления headingChanged().

ПРИМЕЧАНИЕ. Используемые здесь наблюдатели свойств didSet{} и willSet{} были впервые представлены в Swift 4.