Summary: I want to replicate the accessibility behaviour of a UIAlertView, where the background view is still visible but VoiceOver does not interact with it.
Detail: I have implemented accessibility for an iPhone app, but have one problem remaining. In some cases I display a large view on top of all others (partially transparent, covering most of the original view) containing labels and a close button. i.e. basically a custom popup/alert view. The problem is, VoiceOver continues to reveal the views/controls underneath it.
One method to prevent the hidden views from being revealed by VoiceOver is to set the whole custom view background to be accessible. However, this isn't really what we want as this containing view shouldn't really be interacted with by the user, only its subviews (labels/buttons) should.
I think you should use this on your top laying view:
Objective-C
- (BOOL)accessibilityViewIsModal {
return YES;
}
Swift
accessibilityViewIsModal = true
This makes every element of the View Controller that is hidden unaccessible.
An implementation could be to set it to true when you show the view and set it to false when you dismiss that view.
More info
Note: Requires iOS5 and up
Swift 4
In swift try this:
Before your view is presented setup your viewController’s view like this:
yourViewController.view.accessibilityViewIsModal = true
Also try setting the self.view.accessibilityViewIsModal to true in viewWillAppear
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
view.accessibilityViewIsModal = true
}
It also might help if you send a screen chances notification when your modal or pop up view is appearing by adding this to the viewWillAppear:
UIAccessibility.post(notification: .screenChanged, argument: nil)
You can set the following properties on the view overlaying the background:
view.isAccessibilityElement = false;
view.isAccessibilityViewModal = true;
Does this work?
In obj-c:
view.isAccessibilityElement = NO;
view.accessibilityViewIsModal = YES;
When you hide the item, you can set isAccessibilityItem to NO.
Related
I have a ScrollView with 3 collection views within it. I implemented a refresh control. The refresh control correctly functions in the sense that it triggers the required function - and even pulls down the scroll view with a delay.
However, setting any of the refreshControls properties (such as text/background colour) have 0 effect when I add the refresh control to my scrollView like so:
scrollView.refreshControl = refreshControl
When I use the old method:
scrollView.insertSubview(refreshControl, at: 0)
I am able to see the text/spinner, but it's buggy.
Is there a reason that the refreshControl's properties might be hidden from view when not using insertSubview?
Thanks to a user named Matt over on this question:
UIRefreshControl not showing in landscape when in a navigation controller with large titles
I was able to identify that this indeed was caused by Large Title Preferred: Automatic set in Storyboards for my navigation titles. Even though my nav bar is hidden on the view controller, the undesired effect of it hiding my refreshControl still existed. To fix this, as Matt says:
"If you turn off Prefers Large Titles everything is fine. I've played with some other settings, but no effect."
I've included a link to a video that shows what problem I'm having:
https://dl.dropboxusercontent.com/u/39330138/Bug_Demo1.mov
There are two View Controllers, the first is non blurred and less important. When the plus button is clicked, the app segues to a new controller (without animating) and in prepareForSegue() I use UIGraphicsBeginImageContext and UIGraphicsGetImageFromCurrentImageContext to capture a UIImage from the current view and pass it on to the next one.
When the new view appears I use UIVisualEffectView to create a blur view and add it as a subview to the Image View that is the 'background'. Then, its opacity is animated at the same time the 2 views and 2 buttons are animated on screen with UIView animation and springWithDamping, giving the illusion of the view blurring over and items animating over the top.
The top view has a UITextField embedded in it which, when tapped calls becomeFirstResponder() and makes all overlaid (New Session, Tag & Button) views including the Visual Effect View imbedded in the background Image View disappear.
The reason I go into so much detail is because I'm not sure what exactly the problem is. However, I have a suspicion that it is to do with the AutoLayout/Size Classes in Xcode 6.
Does anyone know why this might be happening and how to fix it?
If you need additional information, just let me know.
Thanks!
EDIT:
When I log the views after I click on the TextField, all the frames seem the same.
EDIT 2:
Here's a link to a demo project will all the functionality from the video:
https://dl.dropboxusercontent.com/u/39330138/DEMO%20APP.zip
There are a couple of things happening here, but the main culprit is your use of viewDidLayoutSubviews(). This is called any time the system has to reevaluate the layout. I see you're setting your UIVisualEffectView's alpha to 0 in that method:
if !returningFromTagView {
blurView.alpha = 0
}
I think you're intending this to be called just once before the view appears because I see you animate the alpha to 1 in viewDidAppear(animated: Bool). However, any time the system reevaluates layout for any reason, viewDidLayoutSubviews() is called and the alpha on blurView is going back to 0 if returningFromTagView is false. That's why when you summon the keyboard (triggering a layout reevaluation), this view disappears. Xcode also warns you about making the alpha 0 in the console (it breaks the visual effect until the opacity returns to 1). Put the code above in the viewDidLoad() method instead, and you'll see blurView come back. The alpha only needs to be set to 0 once when the view loads.
The issue with the other views is a bit tougher to see, but the culprit again is your use of viewDidLayoutSubviews(). I imagine that you're puzzled why the views don't appear even after you've been very thorough in your keyboardNotfication() method to set the frames, bring the views to the front, make sure they aren't hidden, and then log this all. But after the keyboardNotification() method finishes, the layout system once again is triggered, and I see that you're nudging the views' frames here and there:
if returningFromTagView {
setX(-titleView.frame.size.width, v: titleView)
setX(-tagView.frame.size.width, v: tagView)
setX(-(cancelButton.frame.size.width + 20 + nextButton.frame.size.width), v: cancelButton)
setX(-nextButton.frame.size.width, v: nextButton)
} else {
setX(-titleView.frame.size.width, v: titleView)
setX(view.frame.size.width, v: tagView)
setX(-cancelButton.frame.size.width, v: cancelButton)
setX(view.frame.size.width, v: nextButton)
}
You're moving the views offscreen every time a layout change is made! Pause the program after you summon the keyboard and look at your view hierarchy using Xcode 6's great new Capture View Hierarchy ability. It's in Debug > View Debugging > Capture View Hierarchy. Those views are just hiding off to the side.
I image you're trying to do this just once when the view appears in order to support your transition animations, but it gets called whether the view is just appearing or if a small change like the keyboard is appearing. I suggest that you implement these animations another way, like using the views' transforms or using autolayout constraints (though you have a lot of missing constraints in the storyboard) to do your animation. viewDidLayoutSubviews() is really a place to fudge things here and there in your layout after the layout system has done its work. You should have a good reason for using it. It has the nice feature of overriding your autolayout constraints and letting you animate those views without toying with the constraints (because the method happens after the updateConstraints() and layoutSubviews() methods), and that's why we can't put the above code in a method like viewWillAppear(animated: Bool) instead (because autolayout constraints would counter the animation during layout later), but viewDidLayoutSubviews() just is not a method that's meant to support basic animations.
In spite of that, here's something simple to get your app going again and for you to see what's going on:
Make a property var comingFromSessionView: Bool property for your NewSessionVC view controller. In the prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) of SessionVC, add nextVC.comingFromSessionView = true
Then change the code block from viewDidLayoutSubviews() above to this:
if returningFromTagView {
setX(-titleView.frame.size.width, v: titleView)
setX(-tagView.frame.size.width, v: tagView)
setX(-(cancelButton.frame.size.width + 20 + nextButton.frame.size.width), v: cancelButton)
setX(-nextButton.frame.size.width, v: nextButton)
} else if comingFromSessionView {
setX(-titleView.frame.size.width, v: titleView)
setX(view.frame.size.width, v: tagView)
setX(-cancelButton.frame.size.width, v: cancelButton)
setX(view.frame.size.width, v: nextButton)
}
We'll switch these Bools to false during viewDidAppear after it's done with them:
override func viewDidAppear(animated: Bool) {
...
if returningFromTagView {
...
returningFromTagView = false
} else if comingFromSessionView {
...
comingFromSessionView = false
}
}
Now when the keyboard is summoned, your views are right where you left them!
The code above isn't great. I'd rather stay away from viewDidLayoutSubviews() for doing these animations. But hopefully you can see what's going on now. Your viewsDidLayoutSubviews() has been whisking away your views.
I have UIView that open with a UIButton click. I want to disable user interaction of all other superviews except to this specific view and his subviews, how can I do that? Just to make this view the only view that will response to user touch.
Thanks!
Agree with the comment, you probably want to disable all siblings of a view... (edited so you can set them back to enabled at some point)
- (void)setSiblings:(UIView *)view enabled:(BOOL)enabled {
for (UIView *sibling in view.superview.subviews) {
if (sibling != view) sibling.userInteractionEnabled = enabled;
}
}
I know you already accepted an answer but a better (and easier) approach is to display the new view full screen. Make the new view with a clear background. Then add the real view as a subview to this full screen view. This way you don't have to mess with any existing views to display this new view. You can still see everything behind it but touch events are blocked by the clear, fullscreen view.
Then when you remove this full screen view (fade out animation?) you don't have to mess with all the existing views again.
You shouldn't have to modify existing views just to display another. And what happens if one of those existing views really should have its interaction disabled? You will end up enabling the interaction when you dismiss your "modal" view.
Many of the table searches I see actually dim (change alpha?) of the actual tableView when the search bar gets focused. I am trying to implement this.
When I overload
- (void)searchBarTextDidBeginEditing:(UISearchBar *)searchBar
I am trying to do something like
self.tableView.alpha = .5f
But it is changing the alpha of the actual searchBar (if I have searchBar on its own) or changing the alpha of the entire table (including searchBar) if I have searchBar underneath the tableView in IB.
What am I doing wrong here. How can I just Dim the table and not everything.
I guess what I am really failing to understand is how all this stuff gets layered on the screen.
EDIT: The contacts application is a good example of what I am trying to do.
Most such apps just throw a black-background UIView on top of the table and fade it in (to an alpha of .5 or whatever) when the search bar gains focus.
If you use UISearchDisplayController, you get this behavior for free.
Since iOS 8 (and up to at least iOS 9.2) you would use a UISearchController. Unfortunately, currently the interface builder supports only the now deprecated UISearchDisplayController. So, this is how to add it programmatically to your table view controller, including the dim effect:
let searchController = UISearchController(searchResultsController: nil)
override func viewDidLoad() {
super.viewDidLoad()
// Use the current view controller to update the search results:
self.searchController.searchResultsUpdater = self
self.searchController.dimsBackgroundDuringPresentation = true
self.tableView.tableHeaderView = searchController.searchBar
}
I would like to dynamically hide a button in one of my views depending on a certain condition.
I tried adding some code to the view controller's -viewWillAppear method, to make the button hidden before displaying the actual view, but I still don't know how to do that.
I have a reference to the button through an IBOutlet, but I'm not sure how to move forward from here. For reference, this is a UIBarButtonItem instance.
If you're trying to hide a UIBarButtonItem, you'll actually have to modify the contents of the parent bar. If it's a UIToolBar, you'll need to set the bar's items array to an array that doesn't include your item.
NSMutableArray *items = [[myToolbar.items mutableCopy] autorelease];
[items removeObject: myButton];
myToolbar.items = items;
Set the bar item to nil.
For example:
self.navigationItem.leftBarButtonItem = nil;
So I tried Ben's winning approach but in the end I found it to be wrong for my purposes - though I'm sure it depends upon what you're trying to do. I was trying to show a nav bar button under certain conditions only and then hide it as soon as the condition was no longer met (in my case it's a "Done" button used to hide the keyboard associated with a UITextView. It should only be displayed when the user is typing in the text view). My steps were as follows:
I added a UIBarButtonItem as a
property in my UIViewController
class. I instantiate it in the
initWithNibName method.
I assigned the UIBarButtonItem property as the
rightBarButtonItem in the nav bar as
soon as the user starts typing in
the text view.
I set the UIBarButtonItem property
to nil when the user is done typing.
It's working like a charm. I'm adding some code samples below.
First to instantiate the button in my view controller init method:
barButtonItemDone = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:#selector(done:)];
Then I set it as the right bar button in the delegate method that is called as soon as the user starts to edit the text view:
self.navigationItem.rightBarButtonItem=[self barButtonItemDone];
Finally, when the button itself is clicked, a method called "done" is called and I just set the rightBarButtonItem to nil inside that method:
self.navigationItem.rightBarButtonItem=nil;
If all that one is trying to hide is the back button in the navigation bar, there is an easier way:
self.navigationItem.hidesBackButton = YES;
Quote from developer documentation:
hidesBackButton
A Boolean value that determines whether the back button is hidden.
#property(nonatomic, assign) BOOL hidesBackButton
Discussion
YES if the back button is hidden when this navigation item is the top
item; otherwise, NO. The default value
is NO.
Availability
Available in iPhone OS 2.0 and later.
This is a bit of a hack, but it works in my case (and it properly handles dynamic spacing):
To hide:
myButton.width = 0.1;
To show:
myButton.width = 0.0;
A width of 0.0 is "auto width", and with a width of 0.1, the button totally disappears (not even a "sliver" of a button, though I haven't tried this on a retina display).
Another hacky solution:
myButton.customView = [[UIView alloc] init];
The best solution to this is less technical. All you need to do is create your normal navigation bar (top) or toolbar (bottom), but without the optional button. Then create another identical, but shorter bar which you then place at the part you want the optional button and create your optional button on this second shorter bar.
Now you can call hidden = YES on the whole additional bar.
The bars seamlessly overlap for me, your mileage may vary.
This answer is regarding text-based UIBarButtonItems, however, the same concept could be applied to other types of buttons as well. Note that this will allow one to both hide and show the item again. Many of the answers above (those setting the button's value to nil, for example, do not allow the button to be re-shown if that's desired).
TL;DR:
if (shouldShowMyBarButtonItem) {
self.myBarButtonItem.title = nil;
self.myBarButtonItem.action = nil;
} else if (!shouldShowMyBarButtonItem) {
self.myBarButtonItem.title = #"Title";
self.myBarButtonItem.action = #selector(mySelector:);
}
The long version:
The UIBarButtonItem I was trying to hide is in a UIToolbar, not a UINavigationBar so all the suggestions that access the left (or right) barButtonItem properties on the navigation item don't work for me. Also, as stated above, I wished to re-show the button when circumstances change.
Michael's suggestion came closest to working, however, with iOS 7 at least, there was still a very small sliver of the button displayed that was tappable. In my app, tapping the item when it's not supposed to be available was unacceptable. The above code both hides and, crucially, deactivates the button.
I call the above code in a private refresh method, which is called when a user event occurs.
This is what I did for button items that weren't part of the navigation bar (where Blank.png is a blank image I created that's the same size of the image it replaces):
theButton.enabled = NO;
theButton.image = [UIImage imageNamed: #"Blank.png"];
Ben's answer is technically correct, though when I try it on my custom UIToolbar, the items space out in a way that I don't like, because I use UIBarButtonSystemItemFlexibleSpace items.
If you want your other items to stay in the same place, you'll either have to set your flexible spaces to fixed spaces, or try what I did:
[filterBarButton.customView setHidden:YES];
note: this only works if your UIBarButtonItem uses custom views.
If you add a UIButton to the UIBarButtonItem rather than just use the UIBarButtonItem.
You can then assign UIButton.hidden to TRUE or YES and it (and the UIBarButtonItem) will not be visible or active.
Hope that helps.
Just set the button's hidden property to true:
myButton.hidden = YES;