2

My environment is Yosemite 10.10.5 with Xcode 7.2 using ARC.

In a simple test program, I am attempting various ways to dismiss a NSViewController and all of them are showing problems with memory handling.

In my primary view controller, I have the following code. (The notification pieces are there to test various ways of dismissing the presented controller.)

- (IBAction)showFirstReplacement:(id)sender {
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(dismissWithNotification:) name:@"removeFirst" object:nil];
    NSStoryboard *sb = [self storyboard];
    FirstReplacement *controller = [sb instantiateControllerWithIdentifier:@"first_replacement"];
    [self presentViewControllerAsSheet:controller];
}

- (void)dismissWithNotification:(NSNotification *)notification {
    NSViewController *controller = [notification object];
    [self dismissViewController:controller];
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

Inside FirstReplacement, I have:

- (IBAction)dismiss:(id)sender {
    [self dismissViewController:self];
//  [[NSNotificationCenter defaultCenter] postNotificationName:@"removeFirst" object:self];
//  [[self presentingViewController] dismissViewController:self];
}

Uncommenting any one of the three lines in this method produces the correct visual results but.... Depending on which of the calls I enable inside dismiss:, I get different results when profiling. Using self dismissViewController:, I see no leaks but FirstReplacement objects are not deallocated. Using either of the other two approaches gets rid of the dismissed FirstReplacement but leaks one 16-byte malloc block and one NSMutableArray every time a view controller is dismissed.

According to Instruments, the leaks are related to a method called [NSViewController _addPresentedViewController:].

Are there other clean-up steps necessary to prevent these leaks (or memory bloat in the non-leak case)?

Phillip Mills
  • 30,888
  • 4
  • 42
  • 57
  • Where is FirstReplacement::dismiss being called from? – rocky Feb 24 '16 at 23:24
  • A button that's connected to the IBAction in the storyboard. – Phillip Mills Feb 25 '16 at 00:03
  • I was able to reproduce this bug on 10.11.6 using Swift and storyboards. However, it appears the bug is fixed as of 10.13.2. On 10.13.2 I did not see the leak within Xcode's Memory Graph Debugger (under runtime issues) or using the Leaks Instrument in the Instruments app. – Andrew Jan 26 '18 at 06:39

1 Answers1

0

The view controller that presents another view controller is also responsible for dismissing it. So none of the lines in FirstReplacement's dismiss method are correct. Instead, you should be creating a delegate in FirstReplacement so it can notify its delegate (the primary view controller) that it should be dismissed.

FirstReplacement.h

@class FirstReplacement;

@protocol FirstReplacementDelegate <NSObject>
- (void)firstReplacementShouldDismiss:(FirstReplacement *)controller;
@end

@interface FirstReplacement : NSViewController
@property (nonatomic, weak) id<FirstReplacementDelegate> delegate;
@end

FirstReplacement.m

- (IBAction)dismiss:(id)sender {
    [self.delegate firstReplacementShouldDismiss:self];
}

Then in your primary view controller:

- (IBAction)showFirstReplacement:(id)sender {
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(dismissWithNotification:) name:@"removeFirst" object:nil];
    NSStoryboard *sb = [self storyboard];
    FirstReplacement *controller = [sb instantiateControllerWithIdentifier:@"first_replacement"];
    controller.delegate = self;
    [self presentViewControllerAsSheet:controller];
}

- (void)firstReplacementShouldDismiss:(FirstReplacement *)controller {
    [self dismissViewController:controller];
}

While it may seem like posting a notification is the same as a delegate, it is not. The difference is that when dismissWithNotification fires, you are still executing the code from FirstReplacement::dismiss. NSNotificationCenter::postNotificationName does not finish executing until all observers have finished executing their selectors. So even though the dismissal code is executing in the primary view controller, it still being run from the dismiss method.

If you are still not convinced, override FirstReplacement::dealloc to print a log statement. You will see that dealloc is not called using any of your methods, but will be called using delegation.

rocky
  • 3,521
  • 1
  • 23
  • 31
  • That's basically what I'm doing in the notification example, telling the primary controller to dismiss when the button is clicked. Also, `[self presentingViewController]` is the primary if I enable that line. – Phillip Mills Feb 25 '16 at 11:37
  • I did your "override dealloc" test in `FirstReplacement`. It's called for the notification approach, the `presentingViewController` one, and the delegate one. The only one that didn't call dealloc was the `self` call (which is obviously not functional). More to the point, all three methods that called dealloc leaked the same two items. – Phillip Mills Feb 25 '16 at 23:03
  • Hmm it didn't get called for me when I tested it. If using delegates still leaks memory, then I'm out of ideas. – rocky Feb 25 '16 at 23:50