OSX-Document-Modal-Dialogs-Sheets

When developing iOS Apps it’s very rare to come up against a technical challenge that hasn’t already been solved and the solution shared on Stack Overflow or in Apple’s sample code in the iOS Dev Center. Unfortunately I haven’t experienced the same luxury with Mac OS X Development.

In a Mac App that I’m currently developing for a client I need to be able to create modally presented windows as sheets as described here in Apple’s Human Interface Guidelines.

sheet_example_2x

Document Modal Dialogs or “Sheets” are instances of NSWindow. According to Apple’s documentation to present a custom NSWindow sheet we use the following code:

[NSApp beginSheet: myCustomSheet
            modalForWindow: window
            modalDelegate: self
            didEndSelector: @selector(didEndSheet:returnCode:contextInfo:)
            contextInfo: nil];

Problem is beginSheet:modalForWindow:modalDelegate:didEndSelector:contextInfo: was deprecated a few years ago and appears to no longer work in Yosemite and Mavericks :-? With out-of-date documentation and all Googling resulting in out-of-date solutions to presenting custom modal windows, I went about figuring out the modern solution to this challenge!

The replacement for beginSheet:modalForWindow:modalDelegate:didEndSelector:contextInfo: is to now call beginSheet:completionHandler: on the NSWindow that you want to present from, passing in reference to the instance of your custom NSWindow subclass.

I also discovered a few important steps that you need to implement when presenting sheets…

Step 1: Retain a reference to the NSWindow or NSWindowController that you are creating when presenting a sheet:

@interface MainWindowViewController ()

@property (strong) CustomModalWindowController *myCustomModalWindowController;

@end

@implementation MainWindowViewController


- (IBAction)didTapOpenButton:(id)sender {
    self.myCustomModalWindowController = [[CustomModalWindowController alloc] initWithWindowNibName:@"CustomModalWindowController"];
   

    [self.window beginSheet:self.myCustomModalWindowController.window  completionHandler:^(NSModalResponse returnCode) {
       
    }];

   
}

@end

Step 2: Deselect the Visible at Launch window attribute for your custom NSWindow or it’ll fail to present modally

Visible-At-Launch

Step 3: Call the window “sheetParent” in order to close the sheet and trigger the sheet completion block in the presentation code:

[self.window.sheetParent endSheet:self.window returnCode:NSModalResponseOK];

I’ve created a demo Xcode project that you can grab from my github repository to see this all in action:

Download source example from Github

Happy Coding!

It's only fair to share...Tweet about this on TwitterShare on FacebookShare on Google+Share on LinkedInShare on StumbleUpon


11 Comments

  1. Wayne Cochran

    Thanks Nick.
    I see you are placing all your view components in a NIB file, but there is no way to add a bare NSWindow to a storyboard. I tried throwing in a NSWindowController into the storyboard and using a modal segue, but I can’t figure out how to get it to present as an action sheet (you’re right as to Stack Overflow not being much help).

  2. Wayne Cochran

    As I submitted the last comment it dawned on me to use an NSViewController — you can have the segue present as a “sheet” in that case. Actually easier now with storyboards.

  3. Gordon Apple

    Thanks for the comment about deselecting “visibleAtLaunch”. At least, it appears attached to the window now. However, I still don’t see a good way to make it send the return code without a controller.

  4. Shaan van Inde

    Superb! Thanks.

    Please post this to StackOverflow where it will help others searching for an answer to the same problem.

  5. Guillaume Laurent

    Thanks for your post, it just helped me save time today. One thing though, you can do without the CustomWindowController, and instantiate the sheet directly from the ViewController.

    For this to work, you first need to set the FileOwner in the CustomModalSheet.xib to CustomModalViewController, and then you can directly bind any actions or outlet from the xib to CustomModalViewController itself. You can then instantiate the sheet as follows :

    NSArray* topLevelObjects;
    [[NSBundle mainBundle] loadNibNamed:@”CustomModalSheet” owner:self topLevelObjects:&topLevelObjects]

    with ‘self’ being your ViewController.

  6. Martijn

    Thank you for this interesting post. However, whatever I did, the custom Window was not shown at all, although ‘windowDidLoad’ was called on my custom NSWindowController.
    When I enabled the option “visibleAtLaunch”, the window was shown modally, at a random position on the screen.
    Finally, based on this post, I was able to show the custom Window as an embedded sheet:
    https://mikeash.com/pyblog/friday-qa-2013-04-05-windows-and-window-controllers.html

    The key was to connect in IB the controller’s window outlet to the “window” property on the File’s Owner.

    Hope this might help others.

  7. Maury Markowitz

    Just like to say thanks to Wayne, who helped me implement his solution. I’d recommend his storyboard-based solution to anyone who’s happy working with >= OSX 10.10. Once he explained the basic concept, I was able to wire up all of my sheets in a few minutes. My only tip would be that if you want to trigger the segue into the sheet from a menu item, link the segue to the root window’s Files Owner icon (the blue box) and then link the menu item to First Responder. That will ensure the right NSDocument gets the action message, and it will fire the segue.

  8. Colin Masters

    Ha, thanks bro, you saved my skin with this one! I kind of feel like we should write a faux Stackoverflow Question/Answer so this blog can be referenced. Biggest annoyance was not really knowing what I was looking for!

  9. Matthew Self

    I am using a sheet to display a progress bar for a long operation. The sheet closes itself when the task is done. It works fine, except in the case that the user hides the app while the sheet is up. If the sheet closes itself while the app is hidden, then when the user unhides the app the sheet is still there (blank) and can’t be dismissed.

    Any ideas?

  10. Tom Corwine

    Thank you so much for this!


Leave a reply