Создать собственный VoiceOver Rotor для навигации по MKAnnotationViews?
Я строю несколько MKAnnotations
на MKMapView
, Я бы хотел, чтобы пользователи VoiceOver могли продолжать панорамирование / масштабирование карты, как обычно, но я также хотел бы, чтобы они могли быстро и легко перемещаться по моим MKAnnotations
если они выберут. Я чувствую, что нестандартный ротор - идеальное решение для этого.
1 ответ
Самостоятельно отвечая здесь, потому что я потратил безумное количество времени, чтобы понять это правильно, и подумал, что кому-то еще это может понадобиться. В то время, когда мне нужно было это разработать, в Интернете практически не было примеров, которые бы подробно описывали создание пользовательских роторов, а документация Apple очень скудна. Я наконец понял это, хотя, после просмотра и приостановки (и паузы на экранах кода) сеанса WWDC 202 (начинается в 24:17).
Самое сложное, что мне нужно было выяснить, это как надежно вернуть UIAccessibilityCustomRotorItemResult
, Для MKMapView
хочешь вернуться MKAnnotationView
s, но у аннотации не гарантируется наличие связанного вида (они перерабатываются, и если аннотация за кадром, есть хороший шанс, что вид был повторно использован), поэтому мои первые попытки не учитывали некоторые или большинство из них мои аннотации.
Волшебство заключается в установке свойства animated: false в значение false:
self.mapView.setCenter(requestedAnnotation.coordinate, animated: false)
Вы не можете использовать вид (для:MKAnnotation) по причинам, указанным выше, поэтому линия, приведенная выше, перемещает карту, так что ваша булавка находится в центре. Поскольку это не анимация, у аннотации сразу создается представление, и в следующей строке кода в моем тестировании гарантированно возвращается MKAnnotationView
,
YVVM, но, пожалуйста, не стесняйтесь добавлять предложения по улучшению, поскольку я считаю, что навигация по карте таким образом имеет решающее значение для пользователей VoiceOver.
func configureCustomRotors() {
let favoritesRotor = UIAccessibilityCustomRotor(name: "Bridges") { predicate in
let forward = (predicate.searchDirection == .next)
// which element is currently highlighted
let currentAnnotationView = predicate.currentItem.targetElement as? MKPinAnnotationView
let currentAnnotation = (currentAnnotationView?.annotation as? BridgeAnnotation)
// easy reference to all possible annotations
let allAnnotations = self.mapView.annotations.filter { $0 is BridgeAnnotation }
// we'll start our index either 1 less or 1 more, so we enter at either 0 or last element
var currentIndex = forward ? -1 : allAnnotations.count
// set our index to currentAnnotation's index if we can find it in allAnnotations
if let currentAnnotation = currentAnnotation {
if let index = allAnnotations.index(where: { (annotation) -> Bool in
return (annotation.coordinate.latitude == currentAnnotation.coordinate.latitude) &&
(annotation.coordinate.longitude == currentAnnotation.coordinate.longitude)
}) {
currentIndex = index
}
}
// now that we have our currentIndex, here's a helper to give us the next element
// the user is requesting
let nextIndex = {(index:Int) -> Int in forward ? index + 1 : index - 1}
currentIndex = nextIndex(currentIndex)
while currentIndex >= 0 && currentIndex < allAnnotations.count {
let requestedAnnotation = allAnnotations[currentIndex]
// i can't stress how important it is to have animated set to false. save yourself the 10 hours i burnt, and just go with it. if you set it to true, the map starts moving to the annotation, but there's no guarantee the annotation has an associated view yet, because it could still be animating. in which case the line below this one will be nil, and you'll have a whole bunch of annotations that can't be navigated to
self.mapView.setCenter(requestedAnnotation.coordinate, animated: false)
if let annotationView = self.mapView.view(for: requestedAnnotation) {
return UIAccessibilityCustomRotorItemResult(targetElement: annotationView, targetRange: nil)
}
currentIndex = nextIndex(currentIndex)
}
return nil
}
self.accessibilityCustomRotors = [favoritesRotor]
}