2018-06-17

Use NSPopover instead of a sheet for small dialog boxes

I tried to use an NSSheet to display a small dialog today but it did not work. I have used sheets in earlier versions of OS X but the old methods have been deprecated. I upgrade to OS X 10.13 recently and the new methods are doing something odd. How annoying. Documentation on some methods mention 'document modal' behavior and this program is not using NSDocument, so that may be something to explore.

While I was trying to figure out that problem, I stumbled across a class that I had never used before, the NSPopover. A popover is similar to a sheet, but instead of appearing at the top of a window, it can pop up next to a designated spot on your view. For example, you could click an object in your custom view and have a popover appear next to that object. Maybe that is better. A popover is not modal though, so maybe that is worse.

To implement a popover, you need a NIB file containing your dialog controls, a view controller subclass to load that NIB, and some properties and functions in the class that creates the popover. The popover class itself does not need a file. It is constructed and configured in code.

My dialog is simple. It has two labels, two text fields for entering numeric values, and a button to close the dialog. My view controller is also simple. It has connections to the text fields, a method for the close button to call, and another weak property to link to the class that created the popover. I specify the creator class as id container below because overlapping .h files were generating compile errors.

@interface MyNumberDialog : NSViewController

@property (weak) IBOutlet NSTextField *textField1;
@property (weak) IBOutlet NSTextField *textField2;
-(IBAction) close: (id)sender;

@property (weak) id container;
@end

The view controller's -close: method just calls the container class's -closeNumberDialog: method.

-(IBAction) close: (id)sender {
    MyOtherClass *vp = (MyOtherClass *) self.container;
    [vp closeNumberDialog: self];
}

The container class header file has the important methods:

@interface MyOtherClass

@property (strong) NSPopover *numberDialogPopover;
@property (strong) MyNumberDialog *numberDialogVC;

-(IBAction) closeNumberDialog: (id) sender;
-(IBAction) showNumberDialog: (id) sender;

@end

In the -showNumberDialog: method you construct and load all objects and then call the popover's -showRelativeToRect:ofView:preferredEdge: method. The rectangle you specify here controls where the popover will appear in your original view.

-(IBAction) showNumberDialog: (id) sender {
    self.numberDialogPopover = [[NSPopover alloc] init];
    self.numberDialogVC = [[VGNumberDialog alloc] initWithNibName: @"VGNumberDialog" bundle: nil];
    self.numberDialogVC.container = self;
    self.numberDialogPopover.contentViewController = self.numberDialogVC;
    self.numberDialogPopover.delegate = self;
    [self.numberDialogPopover showRelativeToRect: NSMakeRect( ... )
                                          ofView: self
                                   preferredEdge: NSRectEdgeMaxY];
}

-(IBAction) closeNumberDialog: (id) sender {
    [self.numberDialogPopover close];
    self.numberDialogPopover.contentViewController = self.numberDialogVC;
    	// Is this necessary? Does it hurt?

    self.numberDialogPopover = nil;
    self.numberDialogVC = nil;
}

The final step is to implement some of the popover delegate messages. Use -popoverWillShow: to load the dialog controls with starting values and -popoverWillClose: to retrieve the dialog values when the the close button was clicked.

- (void)popoverWillShow:(NSNotification *)notification {
    // Load default or previous values into the dialog's text fields.
    self.numberDialogVC.textField1.intValue = ...;
    self.numberDialogVC.textField2.intValue = ...;
}

- (void)popoverWillClose:(NSNotification *)notification {
    int x = self.numberDialogVC.textField1.intValue;
    // Validate and store value here.
    
    x = self.numberDialogVC.textField2.intValue;
    // Validate and store value here.
}

A popover is a nice alternative to a sheet if you do not require a modal dialog. For my problem today, it is actually a better solution because it keeps the dialog close to the item that it modifies.