NSControl subclass shouldn't change firstResponder - objective-c

I implemented a tabbar view as an NSControl subclass.
When I click it, AppKit will try to update the firstResponder of my NSWindow. Because I don't want the tabbar to become firstResponder (acceptsFirstResponder returns NO) the NSWindow itself will become the firstResponder. My responder chain will consist of the window and it's controller and no Action Messages are delivered to the subview below the tabbar (an NSOutlineView). I'd like the NSOutlineView to keep receiving Action Messages and Events when I click the tabbar.
What's the appropriate way to do this? Is there no way to stop an NSView from trying to change the firstResponder when being clicked on?
I thought about setting the toolbar's nextResponder to the NSOutlineView, but manually changing the nextResponder of an NSView is not recommended by Apple.

I ended up manually resetting the firstResponder to the outline view in my tabbar's delegate method implementation.
An other solution I came up with is implementing this in the tabbar to reset firstResponder to the original object, if the tabbar becomes the firstResponder.
- (void)awakeFromNib {
[self.window addObserver:self
forKeyPath:#"firstResponder"
options:NSKeyValueObservingOptionOld
context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change context:(void *)context {
NSResponder *oldResponder = [change objectForKey:NSKeyValueChangeOldKey];
if (self.window.firstResponder == self) {
[self.window makeFirstResponder:oldResponder];
}
}

Related

tvOS preferredfocusedview is not always called

After a viewcontroller has been presented modally, the initial preferredfocusedview is called. However, after we dismiss the viewcontroller and it has been dealloc. preferredfocusedview is not called after presenting the viewcontroller again. Running on tvOS 9.2.
Even adding the following did not help:
-(void) viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self setNeedsFocusUpdate];
[self updateFocusIfNeeded];
}
Anyone know what's going on? Or if there's anyways to debug this?
Edit:
the way I am adding the viewcontroller:
viewController = [[UIViewController alloc] init];
[viewController addChildViewController:self];
[viewController.view addSubview:self.view];
[self didMoveToParentViewController:viewController];
If you are using a container view, having multiple ViewControllers or adding only one View Controller, the preferredFocusEnvironments method must be called from the rootView Controller indicating which View Controller to focus.
For eg.
View Controller A has a container View having ViewControllers B and ViewController C inside the Container.
View Controller A should have preferredFocusEnvironments returning which ViewController to focus.
This way, preferredFocusEnvironments on ViewController B or ViewController C will be called whenever the view becomes visible.
If the ViewController A doesn't have preferredFocusEnvironments, then it won't be called on the containerView ViewControllers.
Implementing custom focus behavior in tvOS 9 is disaster. Apple already mentioned that there is a limitation on redirecting focus specially when presenting/ dismissing a viewcontroller in WWDC.
tvOS10 will handle munch better with preferredFocusEnvironments.
https://developer.apple.com/videos/play/wwdc2016/215/
When I needed to fix this focus redirection issues in viewDidAppear in tvOS 9, I had exactly same issues. Sometimes it works, sometimes not. No clue what so ever. But after I put split second delay on setNeedsFocusUpdate / updateFocusIfNeeded in viewDidAppear it was way better in terms of consistency. preferredFocusedView get called all the time.
-(void) viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self setNeedsFocusUpdate];
[self updateFocusIfNeeded];
});
}
Do this in both presented and presenting view controllers, if you are manually changing focus. This is all from my observation and I don't think there is a proper way to achieve some focus behavior because tvOS API is kind of new and premature. Sorry about not being able to give you good explanation why this might work. Good luck.

How do I prevent a UINavigationBar from animating when I push and pop UIViewControllers?

I have a custom UINavigationBar set in a UINavigationController. I would like to prevent the NavigationBar from animating when new viewcontrollers are pushed and popped from the NavigationController. Normally, the UINavigationBar animates the title and back button right to left during a push, and left to right during a pop. I would like to stop this functionality, keeping the NavigationBar more or less static.
I can prevent the popping animation by overriding the following in my custom UINavigationBar:
-(UINavigationItem *)popNavigationItemAnimated:(BOOL)animated{
return [super popNavigationItemAnimated:NO];
}
However, if I override
- (void)pushNavigationItem:(UINavigationItem *)item animated:(BOOL)animated
the UINavigationBar still animates whenever I push a new viewcontroller onto the NavigationController.
EDIT: I still want the pushed or popped viewController to animate in or out. It is only the NavigationBar that should be stopped.
This works for me - override this method on UINavigationBar:
- (void)pushNavigationItem:(UINavigationItem *)item {
NSMutableArray* items = [[self items] mutableCopy];
[items addObject:item];
self.items = items;
}

How to Stop a UIScrollView from Swallowing Touches

I have UIScrollView that has many subviews. When I scroll, I want to tap on a subview which I want to be dragged. Is there a possible way to make the UIScrollView stop from swallowing touches? Or is it possible to start new touch when you cancel the scrolling (like what it scrolls and I tapped on it, the subview will be tapped as well so I can drag it out)?
Subclass UIScrollView and override the - (BOOL)touchesShouldCancelInContentView:(UIView *)view method. Here's an example to allow uibutton touches to pass through:
#import "scrollViewWithButtons.h"
#implementation scrollViewWithButtons
- (BOOL)touchesShouldCancelInContentView:(UIView *)view
{
return ![view isKindOfClass:[UISlider class]];
}
#end

How to trigger a pageturn transition programmatically without the gesture?

I am using the iBook like page turn transition (UIPageViewControllerTransitionStylePageCurl) to navigate a couple of UIViewController pages.
Therefore I implemented UIPageViewControllerDataSource delegate functions
- (UIViewController *)pageViewController:
(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController
{
and
- (UIViewController *)pageViewController:
(UIPageViewController *)pageViewController viewControllerBeforeViewController:
(UIViewController *)viewController
These get called correctly when doing the page turn by gesture. So far, so good.
In addition to this default behavior I'd like to add a long press gesture recognizer to both sides of the page as shortcut to move to the very last page or the very first page quickly. I already managed to add the gesture recognizer to the pages but I don't know how make the page move without the built-in swipe.
How can I trigger the page turn to a specific page (UIViewController) programmatically?
When the user performs the long swipe you can send the desired viewController (first or last page in your case) to the viewControllers argument of the method:
- (void) setViewControllers:(NSArray*)viewControllers
direction:(UIPageViewControllerNavigationDirection)direction
animated:(BOOL)animated
completion:(void (^)(BOOL finished))completion;
for more details see Graham's answer at:
Is it possible to Turn page programmatically in UIPageViewController?
If I read you right, your UIViewControllers that are being displayed contain buttons and you'd like them to also be able to call this function.
To call the method programatically from WITHIN the UIViewControllers being displayed, you could make the UIPageViewController a delegate of your UIViewControllers that are being displayed, then call a delegate method when the buttons are pressed.
A bad way to do it would be to send the UIViewControllers a back-pointer (stored as a WEAK property!) to your UIPageViewController and call respondsToSelector: on it.
Have a look at this UIPageViewController method:
- (void)setViewControllers:(NSArray *)viewControllers
direction:(UIPageViewControllerNavigationDirection)direction
animated:(BOOL)animated
completion:(void (^)(BOOL finished))completion;
UIPageViewController Class Reference
Make sure you did not set self.pageViewController.datasource to self. Then you can use this UIPageViewController function along with gesture recognizers.
-[self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:NULL];
You can set the delegate of a gesturerecognizer and implement this:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
Or you can just add a gesture recognizer like UISwipeGestureRecognizer

How do I make the keyboard go away when a user clicks on the background of the view?

I have a UITextField in my iOS app. When a user enters text and clicks Return, the keyboard goes away due to a call to an IBAction with "resignFirstResponder."
However, XCode does not let me drag a line from the UIView itself to File Owner. How do I associate touching the background of a UIView with an IBAction that makes the keyboard go away?
You can use UITapGestureRecognizer. see: Dismiss keyboard by touching background of UITableView
so instead of tableview, just add it to your view instead:
UITapGestureRecognizer *gestureRecognizer = [[[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(hideKeyboard)] autorelease];
gestureRecognizer.cancelsTouchesInView = NO; //so that action such as clear text field button can be pressed
[self.view addGestureRecognizer:gestureRecognizer];
and have a method to hide your keyboard
- (void) hideKeyboard {
[self.view endEditing:YES];
}
You've already noticed that you can't drag from the UIView to the file's owner to assoctiate an action with a touch.
The way to work around this is to change the class of the background view from UIView to UIControl and hook up an action from there to a method in your controller to stop editing.
That's because a UIControl can respond to touch events, and a UIView does not, but a UIControl subclasses UIView, and so it can be used in place of a UIView.
I wrote an example project a while ago that uses this technique. Have a look at the secondViewController's xib file and see how I've change the class of the background view and hooked it up to a an action in the controller to dismiss the keyboard.
Use the touchesBegan with Event and end editing on the view:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self.view endEditing:YES];
}
One easy way to do it is to create a big transparent UIButton behind the view.