I want to disable/enable all UIViews in a IBOutletCollection.
However the UIViews differ in class, so I can't call the setEnabled directly.
Then I thought I would use the performSelector method to do it, however I can only send an Object as parameter.
I read both on this site and on other sites that I could just use [NSNumber numberWithBool YES/NO], however the enabled state doesn't change when sending a NSNumber with either bool YES or NO.
I got the disabled part to work by using nil, however I couldn't find a way to set it them enabled:
-(void) setControlsState: (BOOL) enabled
{
for(UIView *subview in controls)
{
NSNumber *boolObject = enabled? [NSNumber numberWithBool: YES]: nil;
if([subview respondsToSelector: #selector(setEnabled:)])
{
[subview performSelector: #selector(setEnabled:) withObject: boolObject];
}
else if([subview respondsToSelector: #selector(setEditable:)])
{
[subview performSelector: #selector(setEditable:) withObject: boolObject];
}
subview.alpha = enabled? 1: 0.5;
}
}
Where controls is a IBOutletCollection consisting of UISliders, UIButtons, UITextViews and UITextfields. (#property (strong, nonatomic) IBOutletCollection(UIView) NSArray *controls;)
Note: The UITextViews works fine wit the above code, it is only the other type of UIViews, which uses setEnabled.
Is there a specific reason your not using userInteractionEnabled which disallows touch events?
-(void) setControlsState: (BOOL) enabled
{
for(UIView *aView in controls)
{
if ([aView isKindOfClass:[UIView class]]){
aView.userInteractionEnabled = enabled;
aView.alpha = (enabled)?1.0:0.5;// Be mindful of this it doesn't seem to respect group opacity. i.e. sliders look funny.
}
}
}
If there is you can simply cast the aView pointer after checking it's class, like so: (Of course you'll have to enumerate through all the classes you use)
-(void) setControlsState: (BOOL) enabled
{
for(UIView *aView in controls)
{
if ([aView isKindOfClass:[UISlider class]]){
[(UISlider *)aView setEnabled:enabled];
}
if ([aView isKindOfClass:[UITextView class]]){
[(UITextView *)aView setEditable:enabled];
}
// and so forth
}
}
I came up against this problem myself yesterday. In my case, setting userInteractionEnabled wasn't sufficient, because I wanted the controls to appear greyed out, not just stop responding to touch events. I also had some UIView subclasses with custom enabled/disabled behavior, and I didn't want to have to enumerate all classes as #NJones suggested in case I add more controls in the future.
As the OP noted, using NSNumber does not work. The solution is to use NSInvocation as explained in TomSwift's answer to this question. I decided to wrap this in a convenience function:
void XYPerformSelectorWithBool(id obj, SEL selector, BOOL boolean)
{
NSMethodSignature *signature = [[obj class] instanceMethodSignatureForSelector:selector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setSelector:selector];
[invocation setArgument:&boolean atIndex:2];
[invocation invokeWithTarget:obj];
}
Then disabling all controls is as simple as:
for (UIView *view in controls)
if ([view respondsToSelector:#selector(setEnabled:)])
XYPerformSelectorWithBool(view, #selector(setEnabled:), NO);
Related
I'm using UIKeyCommand to map certain shortcuts (for example "b", arrow keys, "t", "p", etc.) to a functionality inside my UIViewController subclass. The app is kind of a vector graphics software, which allows addition of text objects inside the canvas. The problem arises when a textView or textField inside the view controller is being edited. While it gets the first responder status, it doesn't receive the shortcut keys (for example writing "beaver" will result in "eaver").
Is there a correct way to handle shortcut keys AND use text objects inside a single view controller?
The solution I found to work best is to go through the responder chain to find the active responder and then check whether it is a UITextField/UITextView or something else. In case it is, return nil from the - (NSArray *)keyCommands method, otherwise return the shortcuts.
Here's the code itself:
#implementation UIResponder (CMAdditions)
- (instancetype)cm_activeResponder {
UIResponder *activeResponder = nil;
if (self.isFirstResponder) {
activeResponder = self;
} else if ([self isKindOfClass:[UIViewController class]]) {
if ([(UIViewController *)self parentViewController]) {
activeResponder = [[(UIViewController *)self parentViewController] cm_activeResponder];
}
if (!activeResponder) {
activeResponder = [[(UIViewController *)self view] cm_activeResponder];
}
} else if ([self isKindOfClass:[UIView class]]) {
for (UIView *subview in [(UIView *)self subviews]) {
activeResponder = [subview cm_activeResponder];
if (activeResponder) break;
}
}
return activeResponder;
}
#end
And this goes inside the keyCommands method:
- (NSArray *)keyCommands {
if ([self.cm_activeResponder isKindOfClass:[UITextView class]] || [self.cm_activeResponder isKindOfClass:[UITextField class]]) {
return nil;
}
UIKeyCommand *brushTool = [UIKeyCommand keyCommandWithInput:#"b"
modifierFlags:kNilOptions
action:#selector(brushToolEnabled)
discoverabilityTitle:NSLocalizedString(#"Brush tool", #"Brush tool")];
UIKeyCommand *groupKey = [UIKeyCommand keyCommandWithInput:#"g"
modifierFlags:UIKeyModifierCommand
action:#selector(groupKeyPressed)
discoverabilityTitle:NSLocalizedString(#"Group", #"Group")];
UIKeyCommand *ungroupKey = [UIKeyCommand keyCommandWithInput:#"g"
modifierFlags:UIKeyModifierCommand|UIKeyModifierShift
action:#selector(ungroupKeyPressed)
discoverabilityTitle:NSLocalizedString(#"Ungroup", #"Ungroup")];
return #[groupKey, ungroupKey, brushTool];
}
My solution was to override canPerformAction:withSender: and return false if the the view controller (that has the shortcut keyCommands) is not the first responder. This makes the walk down the responder chain unsuccessful in finding a target that accepts the key command and instead the key press is sent to the first responder as UIKeyInput as normal and the character appears in the text field. e.g.
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender{
if(action == #selector(brushKeyCommand:)){
return self.isFirstResponder;
}
return [super canPerformAction:action withSender:sender];
}
Following code of mine generates crash in ARC mode:
MxTextField.m
+enableAllTextFields:(BOOL)enable InViews:(__weak UIView*) view
{
#try
{
NSArray* textFields = view.subViews;
for(int idx = 0; idx < textFields.count; idx++)
{
__weak UIView* view = [textFields objectAtIndex:idx];
if(view.subViews.count > 0)
[MxTextField enableAllTextFields:enable InView:view];
else
NSLog(#"No SubViews");
if([view class] == [MxTextField class])
[(MxTextField*) view setEnabled:enable];
}
}
#catch(NSException exception)
{
NSLog(#"%s : %#",__func__,exception);
}
}
After Some Loop on the execution of this function It crashes by showing breakpoint at the end of the function saying EXC_BAD_ACCESS. Can anyone help me out that what goes wrong in this implementation?
Any help will be thankful.
Putting aside many other problems the only reason for a crash that I can see from the posted code is that your method is supposed to return an object but does not do so.
Explanation: While it's not common to leave out the return type in Objective-C it's perfectly legal. It means that the method returns an object of type id.
Since your method lacks a return statement the returned value is undefined. This confuses ARC and probably makes it autorelease the random value in the return register which, eventually, leads to the crash.
Here's a proper version of your method:
+ (void)forAllTextFieldsIn:(UIView *)view setEnabled:(BOOL)enabled
{
if ([view isKindOfClass:[MxTextField class]])
[(MxTextField *)view setEnabled:enabled];
for (UIView *subview in view.subviews)
[self forAllTextFieldsIn:subview setEnabled:enabled];
}
The problem could be the method adopted for iteration and also try-catch is not a good practice, use fast-enumeration for faster and reliable result . The below code could resolve your problem
+(void)enableAllTextField:(BOOL)enable inView:(UIView *)contrainerView
{
for (UIView *subview in contrainerView.subviews) {
if(subview.subviews.count>0)
[MxTextField enableAllTextField:enable inView:subview];
else if ([subview isKindOfClass:[MxTextField class]]) {
MxTextField *textField = (MxTextField *)subview;
[textField setEnabled:enable];
}
}
}
I've written a category on UIView that allows me to walk the view hierarchy:
UIView+Capture.h
typedef void(^MSViewInspectionBlock)(UIView *view, BOOL *stop);
#interface UIView (Capture)
- (void)inspectViewHeirarchy:(MSViewInspectionBlock)block;
#end
UIView+Capture.m
#implementation UIView (Capture)
- (void)inspectViewHeirarchy:(MSViewInspectionBlock)block
{
BOOL stop = NO;
[self inspectViewHeirarchy:block stop:stop];
}
#pragma - Private
- (void)inspectViewHeirarchy:(MSViewInspectionBlock)block stop:(BOOL)stop
{
if (!block || stop) {
return;
}
block(self, &stop);
for (UIView *view in self.subviews) {
[view inspectViewHeirarchy:block stop:stop];
if (stop) {
break;
}
}
}
#end
Which you can use like so:
[[[UIApplication sharedApplication] keyWindow] inspectViewHeirarchy:^(UIView *view, BOOL *stop) {
if ([view isMemberOfClass:[UIScrollView class]]) {
NSLog(#"Found scroll view!");
*stop = YES;
}
}];
Everything works fine, except setting stop to YES. This appears to have absolutely no effect whatsoever. Ideally, I'd like this to halt the recursion, so when I've found the view I want to take some action on I don't have to continue to traverse the rest of the view hierarchy.
I'm pretty dense when it comes to using blocks, so it may be something completely obvious. Any help will be greatly appreciated.
The way you're using a block is exactly the same as using a C function. So there's nothing special you really need to know about blocks. Your code should work but note the difference between passing stop as a BOOL * to your block and to create a new local when you recurse.
It looks like you're expecting calls down to inspectViewHierarchy:stop: to affect the outer stop variable. That won't happen unless you pass it as a reference. So I think what you want is:
- (void)inspectViewHeirarchy:(MSViewInspectionBlock)block stop:(BOOL *)stop
...and appropriate other changes.
I assume you want to return all the way out from the top-level inspectViewHierarchy when the user sets stop to YES.
(Incidentally, you spelled “hierarchy” wrong and you should use a prefix on methods you add to standard classes.)
#implementation UIView (Capture)
- (void)micpringle_visitSubviewsRecursivelyWithBlock:(MSViewInspectionBlock)block
{
BOOL stop = NO;
[self inspectViewHierarchy:block stop:&stop];
}
#pragma - Private
- (void)micpringle_visitSubviewsRecursivelyWithBlock:(MSViewInspectionBlock)block stop:(BOOL *)stop
{
block(self, stop);
if (*stop)
return;
for (UIView *view in self.subviews) {
[view micpringle_visitSubviewsRecursivelyWithBlock:block stop:stop];
if (*stop)
break;
}
}
#end
- (BOOL) inspectViewHeirarchy:(MSViewInspectionBlock)block
{
BOOL stop = NO;
block(self, &stop);
if (stop)
return YES;
for (UIView *view in self.subviews) {
if ([view inspectViewHeirarchy:block])
return YES;
}
return NO;
}
Try this:
- (void)inspectViewHeirarchy:(MSViewInspectionBlock)block
{
__block BOOL stop = NO;
[self inspectViewHeirarchy:block stop:stop];
}
Blocks, by nature, copy the variables and context in which they are declared.
Even though you are passing the boolean as a reference, it's possible that it's using a copy of the context and not the true stop.
This is just a wild guess but, inside inspectViewHierarchy:stop: do something like:
- (void)inspectViewHeirarchy:(MSViewInspectionBlock)block stop:(BOOL)stop
{
if (!block || stop) {
return;
}
// Add these changes
__block BOOL blockStop = stop;
block(self, &blockStop);
for (UIView *view in self.subviews) {
[view inspectViewHeirarchy:block stop:stop];
if (stop) {
break;
}
}
}
This may be a long shot and I'm not 100% sure it will work without having your project, but it's worth a shot.
Also, refactor your method so "heirarchy" is actually spelled "hierarchy" :] It's good for reusability and for keeping a good code base ;)
wouldn't you want to check the status of 'stop' directly after you invoke the block? It doesn't help to invoke it after you call inspectViewHierarchy:stop: because you are passing a copy of 'stop' to that method instead of the reference.
So I have a UIScrollView that is populated with a series of MyCustomViews that are subclasses of a standard UIView. In the delegate callback "scrollViewDidScroll I am trying to loop through all the subviews and call a specific function on them but I don't think the typecasting is working. Here is my code below:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
for(UIView *subView in [scrollView subviews){
MyCustomView *customView = (MyCustomView *)subView;
[customView myMethod];
}
}
When I call "myMethod" on customView, the program crashes saying an unrecognized selector was sent to instance. I believe that my type-casting is the issue as the method myMethod works in other situations. So how do I remedy this situation?
Solution 1:
If you do the following, you don't even need to cast your object to MyCustomView *. It can be of any type, e.g. UIView.
if([subView respondsToSelector:#selector(myMethod)]) {
[subView performSelector:#selector(myMethod)];
}
Solution 2:
You can check the object type before doing the cast.
if([subView isKindOfClass:[MyCustomView class]]) {
MyCustomView *customView = (MyCustomView *)subView;
[customView myMethod];
}
For "catch" this issue, use
if([customView respondsToSelector:#selector(myMethod)]){
[customView myMethod];
}
and with this, the app don't crash.
Also in your for use for(MyCustomView* customView in [scrollView subviews]){
I have a view with several UIButtons. I have successfully implemented using UILongPressGestureRecognizer with the following as the selector;
- (void)longPress:(UILongPressGestureRecognizer*)gesture {
if ( gesture.state == UIGestureRecognizerStateEnded ) {
NSLog(#"Long Press");
}
}
What I need to know within this method is which UIButton received the longpress since I need to do something different, depending on which button received the longpress.
Hopefully the answer is not some issue of mapping the coordinates of where the longpress occured to the bounds of the buttons - would rather not go there.
Any suggestions?
Thanks!
This is available in gesture.view.
Are you adding the long tap gesture controller to the UIView that has the UIButtons as subviews? If so, something along the lines of #Magic Bullet Dave's approach is probably the way to go.
An alternative is to subclass UIButton and add to each UIButton a longTapGestureRecogniser. You can then get your button to do what you like. For example, it could send a message identifying itself to a view controller. The following snippet illustrates methods for the subclass.
- (void) setupLongPressForTarget: (id) target;
{
[self setTarget: target]; // property used to hold target (add #property and #synthesise as appropriate)
UILongPressGestureRecognizer* longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:button action:#selector(longPress:)];
[self addGestureRecognizer:longPress];
[longPress release];
}
- (void) longPress: (UIGestureRecognizer*) recogniser;
{
if (![recogniser isEnabled]) return; // code to prevent multiple long press messages
[recogniser setEnabled:NO];
[recogniser performSelector:#selector(setEnabled:) withObject: [NSNumber numberWithBool:YES] afterDelay:0.2];
NSLog(#"long press detected on button");
if ([[self target] respondsToSelector:#selector(longPressOnButton:)])
{
[[self target] longPressOnButton: self];
}
}
In your view controller you might have code something like this:
- (void) viewDidLoad;
{
// set up buttons (if not already done in Interface Builder)
[buttonA setupLongPressForTarget: self];
[buttonB setupLongPressForTarget: self];
// finish any other set up
}
- (void) longPressOnButton: (id) sender;
{
if (sender = [self buttonA])
{
// handle button A long press
}
if (sender = [self buttonB])
{
// handle button B long press
}
// etc.
}
If your view contains multiple subViews (like lots of buttons) you can determine what was tapped:
// Get the position of the point tapped in the window co-ordinate system
CGPoint tapPoint = [gesture locationInView:nil];
UIView *viewAtBottomOfHeirachy = [self.window hitTest:tapPoint withEvent:nil];
if ([viewAtBottomOfHeirachy isKindOfClass:[UIButton class]])