Search code examples
objective-cmacoscocoaappdelegatensviewcontroller

Correct method to present a different NSViewController in NSWindow


I am developing an app that is a single NSWindow and clicking a button inside the window will present a NSViewController, and a button exists in that controller that will present a different NSViewController. I know how to swap out views in the window, but I ran into an issue trying to do this with the multiple view controllers. I have resolved the issue, but I don't believe I am accomplishing this behavior in an appropriate way.

I originally defined a method in the AppDelegate:

- (void)displayViewcontroller:(NSViewController *)viewController {
    BOOL ended = [self.window makeFirstResponder:self.window];
    if (!ended) {
        NSBeep();
        return;
    }
    [self.box setContentView:viewController.view];
}

I set up a target/action for an NSButton to the AppDelegate, and here's where I call that method to show a new view controller:

- (IBAction)didTapContinue:(NSButton *)sender {
    NewViewController *newVC = [[NewViewController alloc] init];
    [self displayViewcontroller:newVC];
}

This does work - it presents the new view controller's view. However if I then click any button in that view that has a target/action set up that resides within its view controller class, the app instantly crashes.

To resolve this issue, I have to change didTapContinue: to the following:

- (IBAction)didTapContinue:(NSButton *)sender {
    NewViewController *newVC = [[NewViewController alloc] init];
    [self.viewControllers addObject:newVC];
    [self displayViewcontroller:[self.viewControllers lastObject]];
}

First of all, can you explain why that resolves the issue? Seems to be related to the way the controller is "held onto" in memory but I'm not positive.

My question is, how do I set this up so that I can swap out views from within any view controller? I was planning on getting a reference to the AppDelegate and calling displayViewcontroller: with a new controller I just instantiated in that class, but this causes the crash. I need to first store it in the array then send that reference into the method. Is that a valid approach - make the viewControllers array public then call that method with the lastObject, or how should this be set up?


Solution

  • What is interesting in your code is that you alloc/init a new view controller every time that you call the IBAction. It can be that your view its totally new every time you call the IBAction method, but I would think that you only have a limited number of views you want to show. As far as my knowledge goes this makes your view only to live as long as your IBAction method is long. That the view still exists, is because you haven't refreshed it. However, calling a method inside a view controller that is not in the heap anymore (since you left the IBAction method and all local objects, such as your view controller are taken of the heap thans to ARC) makes the app crash, because you reference a memory space that is not in use or used by something else.

    Why does the app work when you ad the view to the viewcontrollers array? I assume this array is an array that has been initiated in the AppDelegate and now you add the view controller with a strong reference count to the viewcontrollers array. When you leave the IBAction method, the view controller still has a strong reference and ARC will not deallocate the view controller.

    Is this the proper way? Well, it works. I would not think it is considered very good programming, since you don't alloc/init an object in a method that needs to stay alive after leaving the method. It would be better practice to allocate and initialize your view controller(s) somewhere in an init, awakeFromNIB or a windowDidLoad method of your AppDelegate. The problem with your current solution is that you are creating an endless array of view controllers of which you only use the last. Somewhere your program will feel the burden of this enormously long array of pretty heavy objects (view controllers) and will run out of memory.

    Hope this helps.

    By the way, this is independent of whether you use Mavericks or Yosemite. I was thinking in a storyboard solution, but that wouldn't answer your question.

    Kind regards, MacUserT