Why isn't UIButton's exclusiveTouch property set to YES by default? - iphone

I'm sure there are a lot of reasons why someone would like to have more than one button accept touches at the same time. However, most of us only need one button to be pressed at one time (for navigation, for something to be presented modally, to present a popover, a view, etc.).
So, why would Apple set the exclusiveTouch property of UIButton to NO by default?

Very old question, but deserves clarification IMO.
Despite the very misleading method documentation from Apple a view "A" with exclusiveTouch set will prevent other views from receiving events so long as A is processing some event itself (e.g. set a button with exclusiveTouch and put a finger on it, this will prevent other views in the window from being interacted with, but interaction with them will follow the usual pattern once the finger from the exlusiveTouch-item is removed).
Another effect is preventing view A from receiving events as long as some other view is interacted with (keep a button without exclusiveTouch set pressed, and the ones with exclusiveTouch will not be able to receive events as well).
You can still set a button in your view to exclusiveTouch and interact with the others, just not at the same time, as this simple test UIViewController will prove (once the correct bindings in the IB are set for both Outlets and Actions):
#import "FTSViewController.h"
#interface FTSViewController ()
- (IBAction)button1up:(id)sender;
- (IBAction)button2up:(id)sender;
- (IBAction)button1down:(id)sender;
- (IBAction)button2down:(id)sender;
#property (nonatomic, strong) IBOutlet UIButton *button1, *button2;
#end
#implementation FTSViewController
- (IBAction)button1up:(id)sender {
NSLog(#"Button1 up");
}
- (IBAction)button2up:(id)sender {
NSLog(#"Button2 up");
}
- (IBAction)button1down:(id)sender {
NSLog(#"Button1 down");
}
- (IBAction)button2down:(id)sender {
NSLog(#"Button2 down");
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Guarantees that button 1 will not receive events *unless* it's the only receiver, as well as
// preventing other views in the hierarchy from receiving touches *as long as button1 is receiving events*
// IT DOESN'T PREVENT button2 from being pressed as long as no event is being intercepted by button1!!!
self.button1.exclusiveTouch = YES;
// This is the default. Set for clarity only
self.button2.exclusiveTouch = NO;
}
#end
In light of this, the only good reason IMHO for Apple not to set exclusiveTouch to YES for every UIView subclass is that it would have made the implementation of complex gestures a real PITA, including probably some of the gestures we are already accustomed to in composite UIView subclasses (like UIWebView), as setting selected views to exclusiveTouch=NO (like button) is faster than doing a recursive exclusiveTouch=YES on pretty much everything just to enable multitouch.
The drawback of this is that in many cases the counter intuitive behaviour of UIButtons and UITableViewCells (among others...) might introduce weird bugs and make testing more tricky (as it happened to me like... 10 minutes ago? :( ).
Hope it helps

the UIView property exclusiveTouch means the view (button) is the ONLY thing in that window that can be interacted with if it is set to YES. As stated in the docs: Setting this property to YES causes the receiver to block the delivery of touch events to other views in the same window. The default value of this property is NO.
Therefore, it is the common behavior that you might have multiple buttons or interaction controls/views in a window and want exclusiveTouch set to NO.
If you set this property to YES for any UIView subclass in a window, you can not interact with anything else in the window for as long as that property is set to YES. That means if you initialize a button with exclusiveTouch = YES, but also have a table view or another button or a scroll view or any other view that is based on interaction, it will not respond to any touches.

exclusiveTouch simply means that any view underneath your UIButton will not receive the touch events.
It's set to no by default because you typically want the view underneath to receive these events. For example, if you have a UIButton on top of a scroll view and the user wants to scroll. You want the scrollView to scroll even if they begin with their finger on the UIButton.

I was just reading release notes for iOS 5 and from this version the exclusiveTouch will be set to YES by default. So just keep in mind that it will change with the new version of iOS.

Related

VoiceOver ignores visible views, and speaks accessibilityLabels of hidden views

I have UIView, that can contain one of two views. When I removeFromSuperview first view and addSubview second view I can still hear accessibiliyLabel of hidden view. And only in 1-2 seconds I can hear correct accessibiilityLabel.
I see that it is common situation when hidden state of view is changed, accessibility can be frustrated and still speak hidden views, and does not note visible views.
Also if in UITableViewCell UIButton is hidden and then hidden state changes to NO, VoiceOver ignores it like it is still hidden. Only manual implementation of UIAccessibilityContainer protocol for cell resolves mentioned problem
No Notifications can solve this issue. Even playing with accessibilityElementsHidden did not help. Struggling with this during several days
Please can you recommend is there any way to say Accessibility that hierarhy of views was changed
You can post a UIAccessibilityScreenChangedNotification or UIAccessibilityLayoutChanged to alert UIAccessibility that the view changed. Since you didn't post any code, I can only give you a generic example, e.g.:
UIAccessibilityPostnotification(UIAccessibilityLayoutChanged,accessibilityelement)
...where "accessibilityelement" would be a button or text field or other accessibility element that VoiceOver switches to next.
Reference: UIKIt Reference
Just ran into this myself with a third party side menu library and had to use accessibilityElementsHidden to fix it. I first tried leveraging the accessibilityViewIsModal property, but that only works on sibling views.
#pragma mark - IIViewDeckControllerDelegate
- (void)viewDeckController:(IIViewDeckController *)viewDeckController didOpenViewSide:(IIViewDeckSide)viewDeckSide animated:(BOOL)animated
{
if (viewDeckSide == IIViewDeckLeftSide) {
[self.topViewController.view endEditing:YES];
self.viewDeckController.leftController.view.accessibilityElementsHidden = NO;
}
}
- (void)viewDeckController:(IIViewDeckController *)viewDeckController didCloseViewSide:(IIViewDeckSide)viewDeckSide animated:(BOOL)animated
{
self.viewDeckController.leftController.view.accessibilityElementsHidden = YES;
}

How can my UIViewController know who put it on the stack?

This is for an addition to a legacy iPhone app with the architecture already defined (years ago, by somebody else.)
The main limitation is that the functionality of the main menu system is based on configuration files, so I can't call any specific initialisation code from the main menu.
This means that the view I am developing is stand-alone, and has to somehow manage its states with the information from the system.
Further, on each screen there is a "Settings" button, taking the user to a Settings pane, that is pushed on the navigation stack on top of "my" view. When the user closes the settings pane, my view reappears, as per normal navigation.
OK, so here is my problem:
When the user enters my view from the menu I want it to be reset so all input fields are empty.
If the user goes to the settings screen and returns to my screen, I want all previous input to be preserved, i.e., not reset to empty fields.
If the user then goes back to the main menu, and re-enters my screen, fields should be empty again.
Is there a robust, documented and preferably simple way to know if I should reset the fields in this scenario?
Can you check the navigation stack to see if the settings page is currently on the stack?
- (void)viewWillAppear:(BOOL)aAnimated
{
[super viewWillAppear:aAnimated];
NSArray* stack = [[self navigationController] viewControllers];
UIViewController* last = [stack lastObject];
}
Presumably viewDidDisappear is called when your view is hidden for some reason. You could presumably get the UIWindow and work up the view chain to find where your view is in the chain (if it is at all) and what is hiding it.
Non-trivial, though, and the sort of thing that will likely need to be "maze bright" vs robust.
(Though if a navigation controller is consistently used it becomes much simpler.)
In the view controller in question, how about you set up a delegate.
id delegate;
#property (nonatomic, assign) id delegate;
for the header, then synthesize in the implementation.
Whenever you push to this view controller, set self as the delegate from the pushing view. Then in this view controller, you can perform a check in the viewDidLoad or viewDidAppear: (or wherever you feel it would be necessary) with something like the following:
if ([self.delegate isKindOfClass:[SomeClass class]]) {
// now you can find which class sent to this view;
}
That should do the trick, so I hope it helps you out
EDIT: considering you are switching views without always using a nav controller, the above won't be valid all the time. In that case, you are probably better off using an internal property as well as an outlet to your settings pane. So in this view controller, you'll want something like this in the header:
BOOL shouldReset;
#property (readwrite) BOOL shouldReset;
In your viewDidLoad, you'll want to initialize this as shouldReset = YES. You should also put this in your viewDidDisappear: since it is your default behavior. When you present the settings pane, give the settings an outlet to the current view controller so you can, from within the settings (when you press the back button) set [self.otherViewController setShouldReset:NO]. Then in your viewDidAppear: for the original view controller in question, you can check to see if it should reset its fields or not

Preventing view from unloading in iOS SDK?

I've built an app that uses a UITableView inside a UINavigationController, inside a UITabBarController. Every entry in the UITableView opens up a view that contains some basic text, buttons, but most importantly, an MPMoviePlayerController that plays audio when started. A user can click this MPMoviePlayerController and continue to browse around the rest of the app (different tabs, or moving back in the navcontroller, opening other views from the tableview) and continue to hear the audio.
I'd like the user to be able to return to the view with the active MPMoviePlayerController at any time. I understand how I would go about allowing the user to return to a certain view from any view, but I'm struggling with how to prevent that view from being reloaded when the user tries accessing the same view.
Is there any way I can save a view in memory? Or save the active MPMoviePlayerController as some type of global object, so that I can at least access that from anywhere?
I appreciate any and all help. Thanks!
I'd recommend you create a property for the MPMoviePlayerController in your app's UIApplicationDelegate (which you can then access from anywhere in the code with [UIApplication sharedApplication].delegate but you will need to cast to your UIApplicationDelegate subclass).
When you come to enter the screen which plays content, check whether your movie player property in the app delegate is nil, if it is create it, otherwise re-use it.
Don't forget to release the reference to your MPMoviePlayerController when the media stops playing, or when the media has already stopped and you get a memory warning or when your app shuts down.
The down side of this approach is it causes coupling between most of your view controllers and your app delegate. You could mitigate this with the use of a protocol however.
You should simply retain it. Like this [myView retain] and keep a pointer to it in where you need. When you want myView to appear, just add it as a subview to current visible view like[myController.view addSubview:myView].
Hope that will help, Good luck!
I've found that even adding a retain doesn't do the trick. I've actually found the best success with overriding the setView (since part of unloading the view involves calling setView:nil. I have a BOOL that gets set the FIRST time the VC loads and once thats set it will never allow setView to be called again.
- (void) setView: (UIView*) view{
NSLog(#"MainViewController: setView");
// this is our attempt to stop iOS from unloading our view.. when iOS tries to unload your view they call setView:nil.. so, no!
if(!viewDidAppear) [super setView:view];
}
A little bit of a hack, but you can override setView: in your subclass so that it never allows to set the view to nil:
-(void)setView:(UIView *)view
{
if (view == nil) return;
[super setView:view];
}

UIView, how to determine when touches entered the view

It appears that all the touch methods of a UIView are only called if the touches began within the bounds of that view. Is there a way to have a view respond to a user who has touched outside the view, but then dragged his fingers into the view?
In case it matters, my specific application is for dragging a MKPinAnnotationView (using built-in 4.0 dragging). I want something to happen if the user drags a pin onto another view (which happens to be an AnnotationView as well, but it could be anything). No method for dragging is called until I let go of the pin; and no method no the UIView that's being dragged to seems to be called unless I started by touching from within the view.
Because the superview is a MKMapView, it is difficult to just use the touchesMoved event of that and check if the user is in the right location or not. Thanks!
So after playing around with it for a while, I found that the answer given here actually gave me what I needed, even though the question being asked was different.
It turns out you can subclass UIGestureRecognizer; and have it handle all the touches for the view that it has been added to (including an MKMapView). This allows all the normal MKMapView interactions to still behave without any problem; but also alerts me of the touches. In touchesMoved, I just check the location of the touch; and see if it is within the bounds of my other view.
From everything I tried; this seems to be the only way to intercept touchesMoved while the user is dragging an MKAnnotation.
You sure can:
(HitstateView.h)
#import <UIKit/UIKit.h>
#interface HitstateView : UIView {
id overrideObject;
}
#property (nonatomic, retain) id overrideObject;
#end
(HitstateView.m)
#import "HitstateView.h"
#implementation HitstateView
#synthesize overrideObject;
- (void)dealloc {
self.overrideObject = nil;
[super dealloc];
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *hitView = [super hitTest:point withEvent:event];
if (hitView == self) {
return overrideObject;
}
return hitView;
}
#end
Make this view the size of your touch area. Set the overideObject to the view you want the touches to go. IIRC it ought to be a subview of the HitstateView.
Every view inherits UIResponder so every view gets touchesBegan/Moved/Ended - I do not think starting the touch outside the view means the view gets no event when the touch moves over the view. If you want to get a notification that something has been dragged onto your MKMapView you should make a subclass that handles the touch but then passes the event to super, allowing the hierarchy to do whatever it needs to do with the touch. You don't need to capture or modify the event just observe it.
It depends on how your views are set up. Generally leveraging the responder chain is the best way to go. It allows you to play tricks, though it may be too specific to address your particular needs.
You can also play tricks with forward events by override hit testing:
http://developer.apple.com/library/ios/#documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/MultitouchEvents/MultitouchEvents.html%23//apple_ref/doc/uid/TP40009541-CH3-SW3
Your particular case sounds pretty exotic, so you may have to play tricks like having a parent view whose frame is large enough to contain both views in question.

How can I avoid a crash when VoiceOver encounters UIPickerView as subview of UITableViewCell contentView?

In my app I have a UIPickerView as a subview of a table cell's contentView. I disable scrolling in the table view, and the arrangement works fine under normal circumstances. However, I've found that when VoiceOver (or the Accessibility Inspector) is turned on, the app crashes as soon as the picker is due to get focus.
The error is:
-[UITableViewCellAccessibilityElement numberOfComponents]: unrecognized selector sent to instance 0xc15dc70
What seems to be happening is that VoiceOver is sending messages presumably intended for the UIPickerView (or its own UIAccessibilityElement?) to the UITableViewAccessibilityElement instead.
When I patch UIAccessibilityElement with the following category...
#implementation UIAccessibilityElement (GMPatches)
- (NSInteger)numberOfComponents {
return 0;
}
#end
... I eliminate the crash -- but now, unsurprisingly, VoiceOver cannot change the UIPickerView value.
And if I change it to the true value in this context...
#implementation UIAccessibilityElement (GMPatches)
- (NSInteger)numberOfComponents {
return 1;
}
#end
... then VoiceOver sends the next misaddressed message, bringing everything down again:
-[UITableViewCellAccessibilityElement selectedRowInComponent:]: unrecognized selector sent to instance 0x1e97b0
I'm feeling fairly sure this is an iOS bug.
I've tried setting isAccessibilityElement = NO on the picker, the table cell and the table cell's content view, none of which helps.
I guess I might be able to expand the UIAccessibilityElement category above to forward various messages to its parent cell's child UIPickerView. But this feels like unpleasantly brittle hackery.
Any better ideas how I might work around this?
I had the same crash when I just set tableView.tableHeaderView = myUITextField and turn VoiceOver on in the system setting app. Problem has been solved now when I wrap myUITextField in a UIView and then assign it to tableView.tableHeaderView.
I believe this would be a bug in iOS 9 or later, for UITextField is also a sub class of UIView.
The issue you are facing has to be solved differently. You are trying to add the UIPicker to the UITableCell, but this is not possible. It looks like it is not possible to determine if you actually scrolled / picked the table cell or the UIPicker.
To overcome this issue, push another view to the table that includes the UIPicker.