Стрелка заголовка iOS 10 для точки MKUserLocation
Приложение Maps в iOS 10 теперь включает стрелку направления курса вверху MKUserLocation
MKAnnotationView
. Есть ли способ добавить это к MKMapView
в моих собственных приложениях?
![введите описание изображения здесь]()
Изменить: Я был бы рад сделать это вручную, но я не уверен, возможно ли это? Могу ли я добавить аннотацию к карте и следить за ее местоположением, включая анимированные ходы?
Ответы
Ответ 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.
- Убедитесь, что класс соответствует 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)
}
}
Ответ 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
.
Создайте протокол 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.