Стрелка заголовка iOS 10 для точки MKUserLocation
Приложение "Карты" в iOS 10 теперь содержит стрелку направления в верхней части MKUserLocation
MKAnnotationView
, Есть ли способ, которым я могу добавить это к MKMapView
в моих собственных приложениях?
Изменить: Я был бы рад сделать это вручную, но я не уверен, если это возможно? Могу ли я добавить аннотацию на карту, чтобы она соответствовала местоположению пользователя, включая анимированные движения?
3 ответа
Я решил это, добавив подпредставление к 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
}
Я также столкнулся с этой же проблемой (мне нужен индикатор ориентации без вращения карты, как в приложении Apple Maps). К сожалению, Apple пока не выпустила API "синий значок для заголовка".
Я создал следующее решение, полученное из реализации @alku83.
- Убедитесь, что класс соответствует MKViewDelegate
Добавьте метод делегата, чтобы добавить значок с синей стрелкой к точке расположения карты
func mapView(_ mapView: MKMapView, didAdd views: [MKAnnotationView]) { if views.last?.annotation is MKUserLocation { addHeadingView(toAnnotationView: views.last!) } }
Добавьте метод для создания "синей стрелки".
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 } }
добавлять
var headingImageView: UIImageView?
в ваш класс. Это в основном необходимо для преобразования / поворота изображения с синей стрелкой.(В разных классах / объектах в зависимости от вашей архитектуры) Создайте экземпляр менеджера местоположений с классом, соответствующим
CLLocationManagerDelegate
протоколlazy var locationManager: CLLocationManager = { let manager = CLLocationManager() // Set up your manager properties here manager.delegate = self return manager }()
Убедитесь, что ваш менеджер местоположения отслеживает данные о пользователях
locationManager.startUpdatingHeading()
и что он останавливает отслеживание, когда это уместноlocationManager.stopUpdatingHeading()
добавлять
var userHeading: CLLocationDirection?
который будет держать значение ориентацииДобавьте метод делегата, чтобы получать уведомления об изменении значений заголовка, и измените значение 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) }
Теперь в вашем классе, соответствующем 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) } }
Да, вы можете сделать это вручную.
Основная идея состоит в том, чтобы отслеживать местоположение пользователя с 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
недвижимость с КВО.
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
Call решает эту крошечную проблему.
Правильное обращение с coordinate
Значение обновления уже реализовано в MKPointAnnotation
, MKAnnotationView
а также MKMapView
классы для нас, поэтому мы не должны делать это сами.
Интересно, почему никто не предложил delegate
решение. Он не полагается наMKUserLocation
но скорее использует подход, предложенный @Dim_ov, по большей части, т.е. подклассифицирует оба MKPointAnnotation
а также MKAnnotationView
(самый чистый и универсальный способ ИМХО). Единственная разница в том, что теперь наблюдатель заменен наdelegate
метод.
Создать
delegate
протокол:protocol HeadingDelegate : AnyObject { func headingChanged(_ heading: CLLocationDirection) }
Создайте
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 } }
Создайте
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)) }) } }
Учитывая, что ваш контроллер представления реализует оба
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.