How can I delegate one NSViewController from another NSViewController in ObjC on Mac? [duplicate] - objective-c

Hi I've seen this question asked a few times already but with no definite answer yet so I created it for xcode 7 and swift2 (which may have changed things a bit anyway).
I created a project using Xcode 7 and Cocoa OSX Story boards + swift2, so my project started with a NSWindowController that Connects to a NSViewController (as expected!). I added a NSToolbar to my window controller and added a NSButton to the toolbar. I changed my NSViewController to be one of the new NSSplitViewController that links to three NSViewControllers and displays their views horizontally - with vertical dividers - (similar to the layout you see in the photo app or pages in Yosemite +). My final goal will be that the button in My toolbar shows and hides the first split.
It is my understanding is, and I would expect that to achieve this I should create an action in the NSSplitViewController that changes the auto layout constrains more or less in the way they are working it out here: How to do collapse and expand view in mac application?.
And then somehow link this action to the NSButton that is in the Toolbar... which happens to be in the NSWindowController (far up and isolated in the hierarchy)...
I have already gone through other questions about NSToolbar and storyboards and failed to accomplish my goal:
The YouTube video: Cocoa Programming L17 - NSToolbar which is the closest I found to solve the problem, but his method does not work for storyboards, only creating your own xib file.
In this question: How to use NSToolBar in Xcode 6 and Storyboard? One person proposes to make the link using the first reponder and expecting everything to hook up at run-time (which looks a bit dodgy and not the way apple would implement it I think...). A second person suggested to create a view controller variable in the NSWindowController and manipulate its properties from there... but again, a bit dodgy too.
One latest comment I saw in that question which seems the best way to tackle the problem (but still not as good as I guess it could be) is to add a NSObjectController to the dock of each scene and when the scene loads, set the values of the objects to the other secene's controller. Is this really the best way to go ahead? If so, how could I achieve this one?
Apple did mention (again) in WWDC15 that they created storyboards for osx and the split-view controller that owns view-controllers so that you can move your logic and work to the specific view-controller, so I would be expecting to do everything from inside my split-view controller as this is the target that needs to change.
Does anyone know how to achieve this from the view controller itself? I really haven't been able to find a way to connect my ToolBarItem to it.

OK, I've created this question quite a few days ago and no answer so far so I've answer with what I recently did to overcome the problem.
After I created my Xcode project I did this:
Created a subclass MySplitViewController for the NSSplitViewController
Added an IBOutlet for each NSSplitViewItem. For example:
#IBOutlet weak var mySplitViewItem: NSSplitViewItem!
Created a subclass WindowController for the NSWindowController
Added an IBAction in the WindowController class that links to the NSToolbarItem (my button)
Added a property that gets the Window Controller's content as MySplitViewController
var mySplitViewController: MySplitViewController {
return self.window?.contentViewController as! MySplitViewController
}
Now I can access the split view controller's property from the Window Controller in the action I created:
mySplitViewController. mySplitViewItem.collapsed = true
I created some sample code that does this (but using a view controller and changing the text for a label here, just in case someone wants to see a working project with this behaviour. And a blog post about it too :)

One person proposes to make the link using the first reponder and expecting everything to hook up at run-time (which looks a bit dodgy and not the way apple would implement it I think...).
I think this first responder method is actually the proper way.
As an example:
Add something similar to the following, in whichever view controller makes sense.
#IBAction func doSomething(_ sender: AnyObject?) {
print("Do something.")
}
This will magically show up in the first responder:
In your storyboard, right-click the orange "first responder" icon above your window controller, and you should see doSomething in the very long list. You just need to connect that up to your toolbar button.
In the following screen capture, you can see my "Toggle Sidebar" button is connected to the toggleSidebar action in my first responder.
I didn't even have to write this method — it's provided by NSSplitViewController:
#IBAction open func toggleSidebar(_ sender: Any?)

So, I was working this same issue and finding no solution as you experienced. I read your post and was trying to figure how I would implement your solution when it occurred to me to use a notification. In about 30 seconds, I had a perfectly fine working solution:
In your windowController add an IBAction to post a notification like so
-(IBAction)toggleMasterViewClicked:(id)sender
{
[[NSNotificationCenter defaultCenter] postNotificationName:#"TestNotification" object:nil];
}
Hook up that action to your NSToolbarItem, then in the viewController add self as an observer for that notification like so
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(toggleMasterView:) name:#"TestNotification" object:nil];
In your case, selector would be updateMyLabelText
I don't really see any downside here. No reference to other objects needed, no dependancies. Works flawlessly for me

While connectiong IBActions works by using either the First Responder or by adding an "Object" to the scene, then changing its class to the window's view controller class, this doesn't help with IBOutlets and delegates that you'd like to point to the view controller.
Here's a work-around for that:
Add the Toolbar to the View Controller, not to its Window. That way, you can make all the IBOutlet connections in the View Controller Scene easily. I've done that for years and found no issues with it, even when using Tabs.
You'll have to assign the window's toolbar in code, then. E.g. like this:
#interface ViewController ()
#property (weak) IBOutlet NSToolbar *toolbar; // connect this in your storyboard to the Toolbar that you moved to the View Controller Scene
#end
- (void)viewWillAppear {
[super viewWillAppear];
self.view.window.toolbar = self.toolbar;
}

Related

Same Viewcontroller pops multiple times

Required:
I want to enable iOS7 swipe to back feature with custom navigation back button item.
Current Implementation:
After researching a lot, I found the following solution to be best:
Set the delegate of the gesture recognizer as follows
self.navigationController.interactivePopGestureRecognizer.delegate = (id<UIGestureRecognizerDelegate>)self;
This, creates a lot of bugs as mentioned in this stackoverflow answer. To avoid that, subclassing the UINavigationController seems to be the only feasible option. I did that as mentioned in this blog by Keighl.
Problem:
Basic swipe to back feature is working, but the strange thing is that, sometimes, the same viewController that is being dismissed, appears again after the pop action is completed.
i.e. suppose the navigation stack looks like A -> B. Popping B will again bring up B. This keeps on happening until eventually the viewController B actually gets dismissed and A appears.
This happens to all views in all viewController objects and not just to a specific one.
Also, I have ensured that the push method is called only once at all places.
I also tried logging the navigation stack at each point, but there is only one instance of each viewController.
Point to note:
I need to disable the swipe feature in certain views. I did this by writing the code to disable and enable the swipe gesture in viewDidAppear and viewDidDisappear respectively.
Please provide your valuable suggestions or a solution to this problem. Thanks!
Short answer: You should add a UIScreenEdgePanGestureRecognizer to your view controller if you want to add a pop gesture where none exists. Modifying the existing interactivePopGestureRecognizer is probably not the right approach. Do this:
[self addGestureRecognizer:({
UIScreenEdgePanGestureRecognizer *gesture =
[[UIScreenEdgePanGestureRecognizer alloc]
initWithTarget:self action:#selector(pop)];
gesture;
})];
and
-(void)pop {
// pop your view controller here
}
Long answer: Forcing the interactivePopGestureRecognizer.delegate is what breaks your code.
If you need to cast self as such:
self.navigationController.interactivePopGestureRecognizer.delegate =
(id<UIGestureRecognizerDelegate>)self;
...it is because self is not a UIGestureRecognizerDelegate. The following should compile, link, build and run or you are setting yourself up for trouble:
self.navigationController.interactivePopGestureRecognizer.delegate = self;
Note that being a UIGestureRecognizerDelegate specifically allows you to tweak a gesture's behavior at runtime, assuming you are implementing one of the following and ensuring that the tweak applies to a gesture you own:
gestureRecognizerShouldBegin:
gestureRecognizer:shouldReceiveTouch:
gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:
gestureRecognizer:shouldRequireFailureOfGestureRecognizer:
gestureRecognizer:shouldBeRequiredToFailByGestureRecognizer:
By constantly changing the delegate of that interactivePopGestureRecognizer you did not create, all you are doing is preventing the iOS behavior to take place.
From the documentation
UINavigationController -interactivePopGestureRecognizer
The gesture recognizer responsible for popping the top view controller off the navigation stack. (read-only)
In plain English: Use this value is you need to combine that gesture with your own gesture. But you are not supposed to modify its behavior:
...You can use this property to retrieve the gesture recognizer and tie it to the behavior of other gesture recognizers in your user interface...

Get Tab Selection Event from UITabBar in a ViewController

The structure of my MainStoryboard is:
->Tab Bar Controller -> Navigation Controller -> View Controller (Search)
The behaviour I want to have is that when the user re-selects the Search tab, the UIScrollView on it scrolls to the top. I am unsure how to get the event from the TabBarController, however.
I've been looking at a lot of stuff about UITabBarDelegate, particularly:
- (void)tabBar:(UITabBar *)tabBar didSelectItem:(UITabBarItem *)item
I have, not quite managed to get this to work properly though. I am very unsure about how to go about setting the delegate (assuming that is the way it's done). I've tried hooking it up in IB, but it wouldn't let me. I also tried to get the UITabBar from the AppDelegate (after looking at some seemingly-related answers).
Any pointers will be greatly appreciated (unless they're null).
UITabBar *aTabBar = [UITabBarItem alloc] init];
....Any other modifications you want to make to aTabBar....
[aTabBar setDelegate:self]
Don't forget to add "<UITabBarDelegate>" to the "#interface" part of whatever object you're trying to designate as the delegate.
For my own code, I usually use some object that isn't the application delegate (as the app delegate is usually meant for application level events like "application is suspending" or "application is coming back into foreground"). If you add "<UITabBarDelegate>" to your Search view controller, make sure that whatever you do with the "didSelectItem" method is applicable only to the Search view controller. Otherwise instantiate some different object if you want to do actions on various view controllers based on which tab bar item is being displayed.

UIViewController within a UIViewController

So I have a viewControllerA, and I want to add another View managed by viewControllerB to it. There is only one UISlider activating a simple action in viewControllerB. It won't crash if I don't touch this UISlider, it will once I use UISlider. I am using ARC. I am using:
[self.view addSubView: viewControllerB.view];
to add viewControllerB to viewControllerA. Am I missing something? Thanks.
OK. It looks like a really simple situation. I just added one view controller and one action. Here is the demo project code on github: https://github.com/randomor/Demo
The reason why I want this to work is because I have another app that will create a view controller on the spot and add it to anther view. And I don't want to do it modally, because I don't want the new view controller to cover the whole screen. Thanks.
SOLUTION: So I'm now just using the latest ViewController containment API:
[self addChildViewController:viewControllerB];
It works! as long as I added this line, the event will be passed to its own controller and it stopped crashing.
i recommend you, to use the following code
in ViewControllerA.h
#import "ViewControllerB.h"
in ViewControllerA.m (where you want to push the new controller)
ViewControllerB *newController = [[ViewControllerB alloc]init];
[self presentModalViewController:newController animated:YES];
in ViewControllerB.m you will need
[self.presentingViewController dismissModalViewControllerAnimated:YES];
to make it vanish again.
concerning multiple controllers for one open screen (Apple ViewController Programming Guide):
Each custom view controller object you create is responsible for managing exactly
one screen’s worth of content. The one-to-one correspondence between a view controller
and a screen is a very important consideration in the design of your application.
You should not use multiple custom view controllers to manage different portions
of the same screen. Similarly, you should not use a single custom view controller
object to manage multiple screens worth of content.
You should try and avoid the practice of nesting UIViewControllers. While it is technically supported in iOS5, it is ill-advised, for many reasons, including the type of problem that you're having (you have a dangling pointer to a UIViewController, which is why you are crashing).
http://blog.carbonfive.com/2011/03/09/abusing-uiviewcontrollers/
Although this question is extremely vague, I imagine that you are not keeping a reference to View Controller B, and so when view B tries to interact with it, it causes EXC_BAD_ACCESS.
What's the object that is set as the target for the slider? If it's a EXC_BAD_ADDRESS, then you may not be retaining the target, most probably the view controller for the slider.

Where to put Root Controller?

I'm getting better and better with doing things for the iPhone in xcode, but here I have ran into a dead end.
I'm trying to build an app with two views: the main one and one for settings. I want to be able to switch between the two of them. Since I need every pixel in the main view I have built a switchView class that simply switch between the two views when I press a button (so much smaller than a tabView), which is working fine.
Now I'm a bit deeper in development and want the settings view to be a table view from where I can navigate to the next level of detail. I have done this before, but without the switch view.
My problem is that I get the table view (in settings) to work, but once I try to push my view controller nothing happens. While debugging I can see that it works through the code (eg didSelectRowAtIndexPath is working) but no new view pops up. Neither any error message.
I have the switchView added in my MainWindow.xib and then do a
[window addSubview:switchViewController.view]; in my AppDelegate to load the main view.
Where should I put the root controller for the navigation for the table view? Because I guess that's the problem I have?
Below the code that results in nothing...
ViewsAppDelegate *delegate =[[UIApplication sharedApplication] delegate];
[delegate.settingsNavController pushViewController:settingsDetailViewController animated:YES];
Happy for any suggestions that could lead me to the right track. Spent way too much time to solve this.
I might be misunderstanding what you're after, but let me try...
When you created your project you should have gotten your MainViewController and an AppDelegate both spun up and created for you. From the snippet you've provided it looks to me like your instantiating a new (and possibly second) app delegate from within your MainViewController.
Rather than doing this, I would suggest that you move your NavigationController up one level to the AppDelegate supplied by the SDK. Then in your MainViewController, add the existing AppDelegate as an IBOutlet and tie them all together in IB. Once you've done that, using the navigation controller should go much more smoothly.
For example, my MainWindow.xib has several objects, and I've highlighted the two that I think you're probably concerned with...
http://www.freeimagehosting.net/uploads/f50268da03.png
*Sorry for the link - I can't post images yet.
Then, from my AppDelegate I customize my applicationDidFinishLaunchingWithOptions like so...
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
navController.viewControllers = [NSArray arrayWithObject:mainMenuController];
// Override point for customization after app launch
[window addSubview:navController.view];
[window makeKeyAndVisible];
return YES;
}
That keeps the navController up at the highest level (where I think it belongs) and allows you to push and pop views throughout the entire app while keeping that history of what's on the stack.
Hope that helps - I might have completely misunderstood what you were after.
Good Luck!
Bob

UITabBarController, MoreNavigationController and the Holy Grail of Device Rotation

UPDATE: See my answer to this question first. This appears to be a bug. A minimal test case has been created and a report has been filed with Apple. (Fixed as of iPhone OS 3.1.)
Here's a puzzler from the "I'm so close!" department.
I have a Tab Bar-based iPhone app. Each tab features a UINavigationController with the usual suspects (nav bar, table view ... which in turn can lead to another VC, etc.).
Now, one of those lower-level VCs is to be used in portait and landscape modes. But there's a problem. Our landscape-friendly VC's shouldAutorotateToInterfaceOrientation: won't get called out-of-the-box! What to do?
Here's what we do. In my Tab Bar Controller, which I have implemented in its own file, I have this:
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
return [self.selectedViewController shouldAutorotateToInterfaceOrientation:interfaceOrientation];
}
This ends up propagating the request to my landscape-friendly VC, which also responds to this message. All my other VCs don't implement this method, so they simply go with the default portrait orientation.
Problem solved!!! Yay!
Well, not quite. :(
Seems like things don't go so well when my landscape-friendly VC is invoked from within the depths of the tab bar controller's MoreNavigationController.
I decided to compare/contrast between a VC called from within one of the first four tab bar UINavigationControllers ... and that same VC called from within the MoreNavigationController. This is going to be a bit ultra-detailed, so bear with me. Hopefully the play by play proves useful for sleuthing things out.
When the app loads, there are several initial calls to the tab bar controller's shouldAutorotate... method. In these early cases, selectedViewController is nil. However, we eventually finish loading, the initial tab item is selected, and all is well.
Right. First, let's pick one of the first four tab bar items and drill down to our VC.
We'll pick third nav bar item, so that's the third nav controller. We drill down to our VC that supports rotation. A quick inspection confirms that the parent is indeed the third nav controller from our tab bar's view controller list. Good!
Let's rotate the device. The tab bar controller is asked to autorotate (see the above code). We observe that selectedViewController is also that third nav controller, plus the nav controller's top and visible view controllers are both set to our trusty VC that supports rotation.
Thus, the tab bar controller will forward the shouldAutorotate message over to the third nav controller ... but our rotation-friendly VC ultimately gets the message. (I'm not doing anything special here. Maybe the desired VC gets the message because it's the top and/or visible VC?) In any event, we rotate to landscape, things are resized, and all is well. "Great Success!"
Now let's hit that back button and pop the VC stack, leaving landscape mode in the process. The tab bar controller is queried again.
Time for a little aside here. The topViewController for our nav controller is still that rotation-friendly VC, but visibleViewController is now set to UISnapshotModalViewController! Heh. Never saw this one before ... but Erica Sadun has. Looks like it's for "disappearing view controllers" (which in this case is certainly true - it's disappearing alright).
As I keep stepping through, the visible VC stays as Snapshot, but the top VC eventually changes to the next VC on the stack, since my special VC is eventually gone. Fair enough.
So that's the scenario where everything works well.
Now let's try the same test, only this time we're going to go to the MoreNavigationController (the More tab bar item) and drill down to the same VC class as before. In my case it happens to be the 7th one in the tab bar controller's VC list.
We enter the rotation-aware VC and ... this time it gets asked to rotate directly! The Tab Bar Controller is not asked for permission to rotate at all. Hmm.
A quick check of the parent VC shows it is a MoreNavigationController. OK, that makes sense.
Now let's try to rotate the device. NOTHING GETS CALLED. None of our breakpoints get hit. Not in our VC. Not in our tab bar controller. (Huh?!?!)
O-kaaaay. Let's pop the stack, go back into the same VC and try to rotate again. Weird. NOW we get a call in the Tab Bar Controller asking for autorotation permission. Here, the selected Controller is our trusty Nav controller (#7), but this time its visibleViewController and topViewController are SET TO NIL!
Once we continue from here, a mysterious message appears in the debugger console:
Using two-stage rotation animation. To
use the smoother single-stage
animation, this application must
remove two-stage method
implementations.
Mysterious because I'm not using two-stage rotation animation! No SecondHalf method variants are in play anywhere in my source code.
Alas, my rotation-aware VC is never told that rotation is occurring (even though rotation does occur on-screen), so of course my view is all fouled up as a result. Mayhem and sadness ensue. :(
We won't even bother popping the stack at this point.
I think the View Controller doc hints at the possible problem:
If you want to perform custom
animations during an orientation
change, you can do so in one of two
ways. Orientation changes used to
occur in two steps, with notifications
occurring at the beginning, middle,
and end points of the rotation.
However, in iPhone OS 3.0, support was
added for performing orientation
changes in one step. Using a one-step
orientation change tends to be faster
than the older two-step process and is
generally recommended for any new
code.
I wonder if MoreNavigationController is still responding to the two-step process, and is thus tripping up any attempts to use the one-step process? Note that, if you respond to the two-step messages, the one-step variant will not work (again, per the docs). I'm not responding to them, but I have a sneaking suspicion something behind-the-scenes IS.
In fact, if I comment out the single-step method, and try to respond to willAnimateSecondHalfOfRotationFromInterfaceOrientation:duration:, I do get the memo! But it still doesn't pop off the stack very cleanly (in terms of visuals). Even stranger: willAnimateFirstHalfOfRotationFromInterfaceOrientation:duration: is NOT called, even when I tried to sneak a call to self (using the FirstHalf message) in shouldAutorotateToInterfaceOrientation:. It returns immediately during a trace, as if I never even defined it. Sigh.
So that's the play-by-play.
In summary, has anyone successfully handled one-step device rotation for a VC invoked from within a Tab Bar Controller's MoreNavigationController? Inquiring minds want to know!
Apple advises against subclassing UITabBarController, so I found an easy way to handle autorotation using categories instead. It doesn't fix your bug with the More... view controllers, but I think it's a more Apple-friendly way of getting the job done (and means less subclassing for you).
To make every tab in my application autorotate properly, I've defined -shouldAutorotateToInterfaceOrientation: in my custom view controllers, but they are all inside UINavigationControllers within a UITabBarController, so the message won't get sent down the chain to my VC until those two also respond. So I added the following lines to my app delegate files:
Added to the bottom of MyAppDelegate.h
#interface UITabBarController (MyApp)
#end
#interface UINavigationController (MyApp)
#end
Added to the bottom of MyAppDelegate.m
#implementation UITabBarController (MyApp)
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation {
return YES;
}
#end
#implementation UINavigationController (MyApp)
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation {
return YES;
}
#end
It appears we have a bug. I have created a reproducible, minimal test case, and reported it via Apple Bug Reporter (RADAR Problem 7139857).
Update: This has been fixed as of iPhone OS 3.1.
The essential problem is:
View controllers already on a
Navigation Controller stack do not
receive
willAnimateRotationToInterfaceOrientation:duration:
messages when a Tab Bar Controller's
'More Navigation Controller' is in
use.
This problem does not occur when the tab bar item view controllers are basic view controllers. Only when they are navigation controllers and only when the "More" navigation hierarchy is in use.
The console message (regarding two-stage rotation animation) suggests that something within the framework (the More Navigation Controller?) is still using a two-stage animation, even though single stage is now recommended as of iPhone OS 3.0.
That could explain why willAnimateRotationToInterfaceOrientation: is not called in that particular case. Per Apple's view controler documentation, this message will NOT be invoked when two-stage, first/second-half orientation messages are being responded to instead.
A slightly modified version of Victorb's answer which allows every single view controller to decide if it allows rotation.
Here as a gist for easier copying & forking
AppDelegate.h
#interface UITabBarController (MyApp)
#end
#interface UINavigationController (MyApp)
#end
AppDelegate.m
#implementation UITabBarController (MyApp)
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation {
UIViewController *selectedVC = [self selectedViewController];
if ([selectedVC respondsToSelector:#selector(shouldAutorotateToInterfaceOrientation:)]) {
return [selectedVC shouldAutorotateToInterfaceOrientation:toInterfaceOrientation];
}
//optimistic return - if you want no rotation, you have to specifically tell me!
return YES;
}
#end
#implementation UINavigationController (MyApp)
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation {
UIViewController *visibleVC = [self visibleViewController];
if ([visibleVC respondsToSelector:#selector(shouldAutorotateToInterfaceOrientation:)]) {
return [visibleVC shouldAutorotateToInterfaceOrientation:toInterfaceOrientation];
}
//optimistic return - if you want no rotation, you have to specifically tell me!
return YES;
}
#end