How to set accessibility only to annotations in map view? - ios

I am trying to make my mapview accessible, but I have an isuue in doing so:
If I try to make the mapView accessible,by doing this:
self.mapView.isAccessibilityElement=YES;
Then, the map view is not read by the voice over.
If I set like this:
self.mapView.isAccessibilityElement=NO;
Then the voice over is reading everything in the map, streets, buildings, my current location and my annotations.
I have given the accessibility label and hints to my annotations,but I havent provided any other value to the mapview.
I also tried by setting the accessibility elements for map view:
[self.mapView setAccessibilityElements:#[self.mapView.annotations,self.mapView.userLocation]];
But still no luck.
Is there anyway to make voice over read only the annotations and neglect remaining elements like streets,buildings?

I think you're having difficulty because an MKMapView is a UIAccessibilityContainer, and so isAccessibilityElement if false by default.
You should look into making custom voice over rotors that allow the user to navigate your app by only your annotations.
This is a good approach, because it gives the user a custom, app specific way to navigate your map, while also not taking anything away from MKMapView's built in accessibility.
There are hardly any examples online that go into detail about creating custom rotors, but I just successfully created one doing exactly what you need it to do. I followed the WWDC Session 202 (begins at 24:17).
Here's my code:
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]
}

Related

How can I determine the index path for the currently focused UITableViewCell using Voice Over?

I have a dynamic UITableView. For each cell, I add a UIAccessibilityCustomAction. When the action fires, I need to know the index path so I can respond accordingly and update my model.
In tableView(_:cellForRowAt:) I add my UIAccessibilityCustomAction like this...
cell.accessibilityCustomActions = [
UIAccessibilityCustomAction(
name: "Really Bad Name",
target: self,
selector: #selector(doSomething)
)
]
I have tried to use UIAccessibility.focusedElement to no avail...
#objc private func doSomething() {
let focusedCell = UIAccessibility.focusedElement(using: UIAccessibility.AssistiveTechnologyIdentifier.notificationVoiceOver) as! UITableViewCell
// Do something with the cell, like find the indexPath.
}
The problem is that casting to a cell fails. The debugger says that the return value type is actually a UITableTextAccessibilityElement, which I could find no information on.
When the action fires, I need to know the index path so I can respond accordingly and update my model.
The best way to reach your goal is to use the UIAccessibilityFocus informal protocol methods by overriding them in your object directly (the table view cell class in your case): you'll be able to catch the needed index path when a custom action is fired.
I suggest to take a look at this answer dealing with catching accessibility focus changed that contains a detailed solution with code snippets if need be.😉
Example snippet...
class SomeCell: UITableViewCell
override open func accessibilityElementDidBecomeFocused() {
// Notify view controller however you want (delegation, closure, etc.)
}
}
I ended up having to solve this myself to bodge an Apple bug. You've likely solved this problem, but this is an option similar to your first suggestion.
func accessibilityCurrentlySelectedIndexPath() -> IndexPath? {
let focusedElement:Any
if let voiceOverObject = UIAccessibility.focusedElement(using: UIAccessibility.AssistiveTechnologyIdentifier.notificationVoiceOver) {
focusedElement = voiceOverObject
} else if let switchControlObject = UIAccessibility.focusedElement(using: UIAccessibility.AssistiveTechnologyIdentifier.notificationSwitchControl) {
focusedElement = switchControlObject
} else {
return nil
}
let accessibilityScreenFrame:CGRect
if let view = focusedElement as? UIView {
accessibilityScreenFrame = view.accessibilityFrame
} else if let accessibilityElement = focusedElement as? UIAccessibilityElement {
accessibilityScreenFrame = accessibilityElement.accessibilityFrame
} else {
return nil
}
let tableViewPoint = UIApplication.shared.keyWindow!.convert(accessibilityScreenFrame.origin, to: tableView)
return tableView.indexPathForRow(at: tableViewPoint)
}
What we're essentially doing here is getting the focused rect (in screen coordinates) and then translating it back to the table view's coordinate space. We can then ask the table view for the indexpath which contains that point. Simple and sweet, though if you're using multi-window you may need to swap UIApplication.shared.keyWindow! with something more appropriate. Note that we deal with the issue you faced where the element was a UITableTextAccessibilityElement when we handle UIAccessibilityElement since UITableTextAccessibilityElement is a private, internal Apple class.

Issue with pass by value vs pass by reference in Swift?

I'm new to Swift but not new to iOS dev. I'm having an issue that I believe is caused by pass by value.
Below code is in the VC and works fine when tapping each button, i.e. each button changes background to show if it's selected or not.
button.isSelected = card.isChosen // button is selected when the card is chosen
if button.isSelected {
button.setBackgroundColor(color: UIColor.darkGray, forState: UIControlState.selected)
}
I have an issue in the game logic which is a struct. Struct contains an array of type Card which is really just a deck of cards and maps back to the UIButtons. When a card(UIButton) in the VC is touched, it calls a method (chooseCard) in the Struct. Among other things, this action will toggle if the card is selected by setting the card.isSelected = !card.isSelected and maps back to the UI to indicate if the card is selected or not. All of this works fine so far.
When 3 cards have been selected, it calls some match logic, which is currently just stubbed out. If there was no match, each of the cards .isSelected is set to false so that in the UI those cards will no longer appear selected. This is what is not working and I can't figure out why. My hunch is that I'm doing something wrong in the way I'm dealing with pass by value since the logic object is a struct. After 3 cards are selected and they are not matched (they won't since it's only stubbed), each card.isSelected should be set back to false, but they are still showing selected in the UI. I set a breakpoint in the VC and it shows the card.isSelected is still set to true. VC has has a setGame property which has access to the array of cards. Here is the relevant code...any help is appreciated!
struct SetGame {
private(set) var cards = [Card]()
mutating public func chooseCard(at index: Int) {
//TODO: Assertion here for index being < cards.count
print("chooseCard(at index: \(index)")
cards[index].isChosen = !cards[index].isChosen // toggle isChosen when card is selected
if !cards[index].isMatched && cards[index].isChosen {
//print("Card not matched, so we're good to go...")
for var card in cards {
if card.isChosen {
matchingCards.append(card)
// see if we have enough cards to match
if matchingCards.count > 2 {
//TODO: Need to call match logic here and set each matched card to .isMatched = true if matched
if card.isMatched {
print("Yay, matched!")
} else {
print("Card is not matched, flipping back over")
/*** THIS LINE NOT REFLECTING IN THE UI! ***/
card.isChosen = !card.isChosen // flip the unmatched card back over
}
}
}
}
matchingCards.removeAll() // clear out all the cards from the matching
} else {
print("Card is either matched or being deselected...")
}
}
Your problem is that Card is a struct, so this line:
for var card in cards {
creates a copy of each card in cards, so setting any properties on that copy will not modify the card in your cards array.
To fix this, loop over the indices of the array and refer to the cards as cards[idx]:
struct SetGame {
private(set) var cards = [Card]()
mutating public func chooseCard(at index: Int) {
//TODO: Assertion here for index being < cards.count
print("chooseCard(at index: \(index)")
cards[index].isChosen = !cards[index].isChosen // toggle isChosen when card is selected
if !cards[index].isMatched && cards[index].isChosen {
//print("Card not matched, so we're good to go...")
for idx in cards.indices {
if cards[idx].isChosen {
matchingCards.append(cards[idx])
// see if we have enough cards to match
if matchingCards.count > 2 {
//TODO: Need to call match logic here and set each matched card to .isMatched = true if matched
if cards[idx].isMatched {
print("Yay, matched!")
} else {
print("Card is not matched, flipping back over")
/*** THIS LINE NOT REFLECTING IN THE UI! ***/
cards[idx].isChosen = !cards[idx].isChosen // flip the unmatched card back over
}
}
}
}
matchingCards.removeAll() // clear out all the cards from the matching
} else {
print("Card is either matched or being deselected...")
}
}
or consider making Card a class so that when you are referring to a Card you know you are referring to the same object.

PageControl in Xcode UITest

I have an app where a Page Control is used to indicate multiple of screens. When the user taps the page control (for example: to the right the of the current selected page), the scroll view scrolls to the following screen. It works fine.
I wanted to write a UI Test for this scenario. I found out that I cannot tap a specific page control "dot" to trigger that action. What I can check is that the Page Control exists and the current selected page.
Another issue is that even if I am able to do so, how can I check that the scroll view has scrolled already? I can only access the frame of the scroll view, not its bounds.
I have just started using Xcode UITest and it's letting me down. I thought it would be powerful by now (it was introduced by Xcode 7 and it was available before as UI Automation), but it seems it's not yet.
I will revert back to Unit Testing + manual functional testing. If anyone has other thoughts, please share it.
Don't give up so soon, Xcode UITest can be frustrating at times but once you figured out what tricks to use, they are quite powerful:
You test your UIPageControl like this:
func testPageIndicator() {
let app = XCUIApplication()
app.launch()
let scrollView = app.scrollViews.element(boundBy: 0)
let pageIndicator = app.pageIndicators.element(boundBy: 0)
// test that page indicator is updated when user scrolls to page 2
scrollView.swipeLeft()
XCTAssertEqual(pageIndicator.value as? String, "page 2 of 3")
// test that scrollview scrolls to page 3 when user taps right of the current page in page indicator
pageIndicator.coordinate(withNormalizedOffset: CGVector(dx: 0.9, dy: 0.2)).tap()
XCTAssert(app.otherElements["page_3"].waitForExistence(timeout: 2))
XCTAssertFalse(app.otherElements["page_2"].exists)
XCTAssertEqual(pageIndicator.value as? String, "page 3 of 3")
}
You can tell the test where to tap on a UIElement by using the coordinate method. You hand a CGVector object to that method that describes where to tap.
For example a CGVector(dx: 0.9, dy: 0.2) tells the test to tap the element at the point that lies at 90% of its width and 20% of its height.
You can use this to tap left of right of the current page in the UIPageIndicator(see above code example)
To test if you are on the correct page in the UIScrollView you can set each pages accessibilityIdentifier to a unique string and then assert that the UIElement with that identifier exists after you tapped the page control.
In my case the pages are 3 UIViews. I set their accessibilityIdentifier to "page_1", "page_2" and "page_3" and then assert that "page_3" exists and "page_2" doesn't.
This is a extension I have for getting the number of pages and current page. It uses regex to find two numbers in the string value and assumes the bigger one is the number of pages and the smaller one is current page.
What is nice about it is it works even if your device/simulator is not set in English.
extension XCUIElement {
func currentPageAndNumberOfPages() -> (Int, Int)? {
guard elementType == .pageIndicator, let pageIndicatorValue = value as? String else {
return nil
}
// Extract two numbers. This should be language agnostic.
// Examples: en:"3 of 5", ja:"全15ページ中10ページ目", es:"4 de 10", etc
let regex = try! NSRegularExpression(pattern: "^\\D*(\\d+)\\D+(\\d+)\\D*$", options: [])
let matches = regex.matches(in: pageIndicatorValue, options: [], range: NSRange(location: 0, length: pageIndicatorValue.count))
if matches.isEmpty {
return nil
}
let group1 = matches[0].range(at: 1)
let group2 = matches[0].range(at: 2)
let numberString1 = pageIndicatorValue[Range(group1, in: pageIndicatorValue)!]
let numberString2 = pageIndicatorValue[Range(group2, in: pageIndicatorValue)!]
let number1 = Int(numberString1) ?? 0
let number2 = Int(numberString2) ?? 0
let numberOfPages = max(number1, number2)
var currentPage = min(number1, number2)
// Make it 0 based index
currentPage = currentPage > 0 ? (currentPage - 1) : currentPage
return (currentPage, numberOfPages)
}
/// return current page index (0 based) of an UIPageControl referred by XCUIElement
func currentPage() -> Int? {
return currentPageAndNumberOfPages()?.0 ?? nil
}
/// return number of pages of an UIPageControl referred by XCUIElement
func numberOfPages() -> Int? {
return currentPageAndNumberOfPages()?.1 ?? nil
}
}
And you can use it like:
let pageCount = app.pageIndicators.element(boundBy: 0).numberOfPages() ?? 0
for _ in 0 ..< pageCount {
app.pageIndicators.element(boundBy: 0).swipeLeft()
}

Not allowing touch interaction on certain views?

I've got a class where I handle my touches functions on buttons, and those buttons are also a Post class.
The logic I'm going for is "If the post.user is equal to the current firebaseuser, don't allow the user to move the button"
Here's what I tried:
In my "DragButtonsClass" I've got a notification being sent to my view controller on TouchesBegan.
In the view I've got
func touchesStarted(notif : NSNotification) {
if let dict = notif.userInfo! as? [NSObject: AnyObject] {
let tag = dict["tag"]!
let tagInt = Int(String((tag))) //this is probably super sloppy, don't know how else to change an AnyObject to an Int though.
let post = postArray[tagInt! - 1]
let postbutton = self.view.viewWithTag(tagInt!)
if post.user == currentUser {
postbutton?.userInteractionEnabled = false
print("Should be working")
} else {
print("Didn't work")
}
}
}
Basically I'm assigning tags to the buttons as they're created and those tags line up with my Posts array. I'm trying to check if the Post's user is the same as the current user by sending it over and then shut off user interaction.
I'm getting "Should be working" to print out but the button is still draggable.
This seems like a super sloppy and roundabout way to do this and most importantly it isn't working. I've read that turning off userInteraction is a way to stop touches from being recognized.
Any ideas?
Ended up just setting a bool "canBeDragged" in my button's class, and when the button is created the current user's ID is compared to the information being pulled from firebase and the bool is set. Then I just checked if the button could be dragged before my touchesMoved.

Showing user location when button on custom UITabBarController is pressed

I have a custom UITabBar controller, where I have buttons to switch between the view controllers. Here is an Image:
http://i.stack.imgur.com/UInLI.png
What I want to do it that when the main button is pressed the view will change to the mapViewController, and when it pressed again (on the same mapViewController) it will show to user Location.
I am using mapBox as my Map API, I have a function in my mapViewController,
findUserLocation()
and this show the user location by using:
mapView.userTrackingMode = MGLUserTrackingMode.Follow
So here is My code:
class CustomTabBarViewController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
self.tabBar.hidden = true
let tabBarView = CustomTabBarView()
tabBarView.viewdidload()
self.view.addSubview(tabBarView)
tabBarView.mapButton.addTarget(self, action: #selector(self.changeToMyMap), forControlEvents: UIControlEvents.TouchUpInside)
// Do any additional setup after loading the view.
}
func changeToMyMap(){
if (self.selectedIndex != 0){
self.selectedIndex = 0
}else{
let mapViewController = myMap() // the class Name
mapViewController.mapView.userTrackingMode = MGLUserTrackingMode.Follow
}
}
}
So when I pressed the button to show the user location I get an error. I think is because the Map is not loaded into the customTabViewController, but I have no Idea how to make it work.
when I try to call a function
from my customTabBarController, my application get a fatal error.
fatal error: unexpectedly found nil while unwrapping an Optional value
If you haven't used them already, these may be some helpful mapBox resources.
I'm not sure why it would be crashing with what you have there.
I would try putting some breakpoints in to find out where it's actually crashing.
Try putting one on the line if (self.selectedIndex != 0){
to see if it's actually going inside that method.
If it is, try typing po yourPropertyName i.e. po self.selectedIndex in the debug console to try and find out what property is returning nil.
Also, if you know what properties are optional, check where you're accessing them. That's where they could be nil.
In our Swift application, we made use of showsUserLocation to toggle the user location.
This would be worth trying, if you are simply trying to show the User Location.
The MGLUserTrackingMode in Mapbox iOS SDK 3.2.3 is for how to track the user location on the map (e.g., the experience when following a driving route).
How you could use showsUserLocation
func changeToMyMap(){
// not sure if the 'if/else' logic fit with what you intended, but
// left your code intact to better highlight how to use mapView.showsUserLocation
if (self.selectedIndex != 0){
self.selectedIndex = 0
mapViewController.mapView.showsUserLocation = false
}else{
let mapViewController = myMap()
// causes the map view to use the Core Location framework to find the current location
mapViewController.mapView.showsUserLocation = true
}
}
}
Be aware of this note in the docs for Mapbox iOS SDK 3.2.3. You will need to pay attention to set new keys in the .plist.
On iOS 8 and above, your app must specify a value for
NSLocationWhenInUseUsageDescription or NSLocationAlwaysUsageDescription in
its Info.plist to satisfy the requirements of the underlying Core Location
framework when enabling this property.
For comparison of how we use showsUserLocation in Mapbox (what produces the experience in the above GIF) —
#IBAction func handleSettings(sender: AnyObject) {
var trackingLocation = UIImage(named: "TrackingLocationOffMask.png")
switch (self.mapView.userLocationVisible) {
case true:
self.mapView.showsUserLocation = false
trackingLocation = UIImage(named: "TrackingLocationOffMask.png")
break
case false:
self.mapView.showsUserLocation = true
trackingLocation = UIImage(named: "TrackingLocationMask.png")
break
}
self.navigationItem.leftBarButtonItem?.image = trackingLocation
}

Resources