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.
Related
When I show an alert with UIAlertController, the alert itself presented in a new window. (for now at least) And when the alert window dismisses, system seems to set a random window key-window.
I am presenting a new "banner" window to render some banners over status-bar (AppStore compatibility is out of topic here), and usually, this "banner" window becomes next key window, and causes many problems on user input and first responder management.
So, I want to prevent this "banner" window to become a key window, but I cannot figure out how. For now, as a workaround, I am just re-setting my main window to be a key window again as soon as that "banner" window becomes key window. But it doesn't feel really good.
How can I prevent a window to become a key window?
As a workaround, we can set main window key again as soon as the "banner" window becomes a key like this.
class BannerWindow: UIWindow {
weak var mainWindow: UIWindow?
override func becomeKeyWindow() {
super.becomeKeyWindow()
mainWindow?.makeKeyWindow()
}
}
Faced with this too. It seems that it's enough to just make:
class BannerWindow: UIWindow {
override func makeKey() {
// Do nothing
}
}
This way you don't need to keep a reference to a previous keyWindow, which is especially cool if it might get changed.
For Objective-C it's:
#implementation BannerWindow
- (void)makeKeyWindow {
// Do nothing
}
#end
I've been trying to solve this problem for years. I finally reported a Radar for it: http://www.openradar.me/30064691
My workaround looks something like this:
// a UIWindow subclass that I use for my overlay windows
#implementation GFStatusLevelWindow
...
#pragma mark - Never become key
// http://www.openradar.me/30064691
// these don't actually help
- (BOOL)canBecomeFirstResponder
{
return NO;
}
- (BOOL)becomeFirstResponder
{
return NO;
}
- (void)becomeKeyWindow
{
LookbackStatusWindowBecameKey(self, #"become key window");
[[self class] findAndSetSuitableKeyWindow];
}
- (void)makeKeyWindow
{
LookbackStatusWindowBecameKey(self, #"make key window");
}
- (void)makeKeyAndVisible
{
LookbackStatusWindowBecameKey(self, #"make key and visible window");
}
#pragma mark - Private API overrides for status bar appearance
// http://www.openradar.me/15573442
- (BOOL)_canAffectStatusBarAppearance
{
return NO;
}
#pragma mark - Finding better key windows
static BOOL IsAllowedKeyWindow(UIWindow *window)
{
NSString *className = [[window class] description];
if([className isEqual:#"_GFRecordingIndicatorWindow"])
return NO;
if([className isEqual:#"UIRemoteKeyboardWindow"])
return NO;
if([window isKindOfClass:[GFStatusLevelWindow class]])
return NO;
return YES;
}
void LookbackStatusWindowBecameKey(GFStatusLevelWindow *self, NSString *where)
{
GFLog(GFError, #"This window should never be key window!! %# when in %#", self, where);
GFLog(GFError, #"To developer of %#: This is likely a bug in UIKit. If you can get a stack trace at this point (by setting a breakpoint at LookbackStatusWindowBecameKey) and sending that stack trace to nevyn#lookback.io or support#lookback.io, I will report it to Apple, and there will be rainbows, unicorns and a happier world for all :) thanks!", [[NSBundle mainBundle] gf_displayName]);
}
+ (UIWindow*)suitableWindowToMakeKeyExcluding:(UIWindow*)notThis
{
NSArray *windows = [UIApplication sharedApplication].windows;
NSInteger index = windows.count-1;
UIWindow *nextWindow = [windows objectAtIndex:index];
while((!IsAllowedKeyWindow(nextWindow) || nextWindow == notThis) && index >= 0) {
nextWindow = windows[--index];
}
return nextWindow;
}
+ (UIWindow*)findAndSetSuitableKeyWindow
{
UIWindow *nextWindow = [[self class] suitableWindowToMakeKeyExcluding:nil];
GFLog(GFError, #"Choosing this as key window instead: %#", nextWindow);
[nextWindow makeKeyWindow];
return nextWindow;
}
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];
}
}
}
Is there a way to know if my custom implementation of setFrame: (or an other setter of an animatable property) is being called from an animation block i.e. it will be animated or just set directly?
Example:
- (void)setFrame:(CGRect)newFrame {
[super setFrame:newFrame];
BOOL willBeAnimated = ?????
if (willBeAnimated) {
// do something
} else {
// do something else
}
}
In the above setter willBeAnimated should be YES it is called like this:
- (void)someMethod {
[UIView animateWithDuration:0.2
animations:^{view.frame = someRect;}
completion:nil];
}
and NO in this case:
- (void)someMethod {
view.frame = someRect;
}
someMethod here is a private method inside UIKit that I can't access or change, so I have to somehow determine this from the "outside".
You should be able to check the animationKeys of the layer of your UIView subclass right after changing the frame to see if it is being animated.
- (void)setFrame:(CGRect)newFrame {
[super setFrame:newFrame];
BOOL willBeAnimated = [super.layer animationForKey:#"position"] ? YES : NO;
if (willBeAnimated) {
// do something
} else {
// do something else
}
}
You can also to check if there are any animations by using animationsKeys which in this case would just return position.
In addition, if you want to force a change to not be animated you can use performWithoutAnimation:
[UIView performWithoutAnimation:^{
[super setFrame:newFrame];
}];
EDIT
Another tidbit I found by testing is that you can actually stop the animation if it is already in progress and instead making the change instantly by removing the animation from the layer and then using the above method instead.
- (void)setFrame:(CGRect)newFrame {
[super setFrame:newFrame];
BOOL willBeAnimated = [super.layer animationForKey:#"position"] ? YES : NO;
BOOL shouldBeAnimated = // decide if you want to cancel the animation
if (willBeAnimated && !shouldBeAnimated) {
[super removeAnimationForKey:#"position"];
[UIView performWithoutAnimation:^{
[super setFrame:newFrame];
}];
} else {
// do something else
}
}
(my boss says) that I have to implement a "Done" button on a navBar so that the various items in the view (that contain an edit box) will dismiss their keyboard (if they were in focus).
It seems that I must iterate through all items and then call resignFirstResponder on each on the off-chance that one of them is in focus? This seems a bit messy (and hard to maintain if e.g. someone else adds more items in future) - is there a better way to do it?
I have found it!
Thanks to this
I discovered that all I need do is this:-
-(void) done {
[[self.tableView superview] endEditing:YES];
}
// also [self.view endEditing:YES]; works fine
[remark]
Also I learn how to do the equivalent of an "eventFilter" to stop UITableViewController from swallowing background touch events by intercepting them before they get there - from the same, wonderful post on that thread - see "DismissableUITableView".
[end of remark]
You don't have to iterate through the controls since only one can be first responder at the moment.
This will reset the responder to the Window itself:
[[self window] makeFirstResponder:nil]
One solution is to use a currentTextField Object,
In .h file have an instance variable as
UITextField *currentTextField;
Now in .m file.
Note : Dont forget to set the delegates of all the textField to this class
- (void)textViewDidBeginEditing:(UITextView *)textView
{
currentTextField = textField;
}
- (void)textViewDidEndEditing:(UITextView *)textView
{
currentTextField = nil;
}
Now in your button action method
-(IBAction)buttonTap
{
if([currentTextField isFirstResponder])
[currentTextField resignFirstResponder];
}
This avoids iterating through all the text field.
I think best way to handle it by searching all subviews of main view with recursive function, check example below
- (BOOL)findAndResignFirstResponder {
if (self.isFirstResponder) {
[self resignFirstResponder];
return YES;
}
for (UIView *subView in self.subviews) {
if ([subView findAndResignFirstResponder]) {
return YES;
}
}
return NO;
}
and also you can put this method to your utility class and can use from tap gesture. All you have to do is simply adding to gesture to view.
UITapGestureRecognizer *gestureRecognizer = [[UITapGestureRecognizer alloc]
initWithTarget:self action:#selector(hideEverything)];
[self.tableView addGestureRecognizer:gestureRecognizer];
and than you can call hideEverything method;
- (void) hideKeyboard {
[self.view findAndResignFirstResponder];
...
...
}