NSWindowControllerを使って、シートとモーダルウィンドウを作成する

# 概要

- エラーを表示する汎用的なウィンドウを作成したいということで、使い回せるような`NSWindowController`を作成する
- 表示の仕方はシート表示とモーダルウィンドウ表示

- [モーダルウィンドウとは](https://www.seohacks.net/basic/terms/modal-window/)

> モーダルウィンドウとはウィンドウ内で指定された操作を完了、またはキャンセルするまで他のウィンドウを開くことができないウィンドウのことです。

# UI

- メイン画面
    - `＜Result＞`の部分に下記のウィンドウからの返り値を表示する

![-w425](https://i.imgur.com/CqxLmCX.jpg)

- シート表示

![-w428](https://i.imgur.com/E5p86j2.jpg)

- モーダルウィンドウを表示

![-w622](https://i.imgur.com/r4qB0I9.jpg)

# GitHub

https://github.com/pommdau/Qiita_ModalWindowSample

# 実装
## AppDelegate

- 最小限の処理のみ`AppDelagete`に書いて、UIの処理は別に書く。

``` ObjectiveC
#import "AppDelegate.h"
#import "MainWindowController.h"

@interface AppDelegate ()
@property (weak) IBOutlet NSWindow *window;
@property (strong) MainWindowController *mainWindowController;
@end

@implementation AppDelegate

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
    // Insert code here to initialize your application
    
    _mainWindowController = [[MainWindowController alloc] initWithWindowNibName: MainWindowController.className];
    [_mainWindowController showWindow:self];
}


- (void)applicationWillTerminate:(NSNotification *)aNotification {
    // Insert code here to tear down your application
}

@end
```

- 今回は`MainWindowController`を呼び出すだけ。具体的な処理はそこで行う。

## MainWindowController

- コード全体

``` ObjectiveC
#import "MainWindowController.h"
#import "SheetWindowController.h"
#import "ModalWindowController.h"

@interface MainWindowController ()
@property (weak) IBOutlet NSTextField *resultLabel;
@property SheetWindowController *sheetWindowController;
@property ModalWindowController *modalWindowController;
@end

@implementation MainWindowController

- (void)windowDidLoad {
    [super windowDidLoad];
    
    // Implement this method to handle any initialization after your window controller's window has been loaded from its nib file.
}

/**
 @brief シートを開く
 */
- (IBAction)openSheet:(id)sender {
    SheetWindowController *sheetWindowController = [[SheetWindowController alloc] init];
    _sheetWindowController = sheetWindowController;
    
    [self.window beginSheet:_sheetWindowController.window  completionHandler:^(NSModalResponse returnCode) {
        
        NSMutableString *resultMessage = [NSMutableString stringWithString: @"SheetResponse: "];
        
        switch (returnCode) {
            case NSModalResponseOK:
                [resultMessage appendString:@"OK"];
                break;
            case NSModalResponseCancel:
                [resultMessage appendString:@"Cancel"];
                break;
            default:
                break;
        }
        
        self->_resultLabel.stringValue = resultMessage;
    }];
}

/**
 @brief モーダルウィンドウを開く
 */
- (IBAction)openModalWindow:(id)sender {
    ModalWindowController *modalWindowController = [[ModalWindowController alloc] init];
    _modalWindowController = modalWindowController;
    
    NSModalResponse result = [NSApp runModalForWindow:_modalWindowController.window];
    NSMutableString *resultMessage = [NSMutableString stringWithString: @"ModalWindowResponse: "];
    if (result == NSModalResponseOK) {
        [resultMessage appendString:@"OK"];
        
    } else if (result == NSModalResponseCancel) {
        [resultMessage appendString:@"Cancel"];
    }
    _resultLabel.stringValue = resultMessage;
}

@end
```

- シートを開いている`(IBAction)openSheet:(id)sender`を見る。

``` ObjectiveC
SheetWindowController *sheetWindowController = [[SheetWindowController alloc] init];
_sheetWindowController = sheetWindowController;
```

- シートの`WindowController`クラスを作成しプロパティへ保存しているだけ。

- シートの呼び出しは以下の通り。

``` ObjectiveC
[self.window beginSheet:_sheetWindowController.window  completionHandler:^(NSModalResponse returnCode) {
    
    NSMutableString *resultMessage = [NSMutableString stringWithString: @"SheetResponse: "];
    
    switch (returnCode) {
        case NSModalResponseOK:
            [resultMessage appendString:@"OK"];
            break;
        case NSModalResponseCancel:
            [resultMessage appendString:@"Cancel"];
            break;
        default:
            break;
    }
    
    self->_resultLabel.stringValue = resultMessage;
}];
```

- `beginSheet`でシートを呼び出す。
- `completionHandler`のクロージャに、シートを閉じた後の処理を書く。
- 引数はシートで押されたボタンの情報で、OKやCancelをシート側で返すようにしておく

- 次はモーダルウィンドウを呼び出している`(IBAction)openModalWindow:(id)sender`を見る

``` ObjectiveC
ModalWindowController *modalWindowController = [[ModalWindowController alloc] init];
_modalWindowController = modalWindowController;
```

- `ModalWindowController`のオブジェクトを作成しプロパティへ保存

``` ObjectiveC
NSModalResponse result = [NSApp runModalForWindow:_modalWindowController.window];
```

- `runModalForWindow`を呼び出して`WindowController`の`Window`をモーダル表示する。
- これの返り値を変数で受け取り、その後任意の処理を行う。
- `esc`でウィンドウを閉じた場合は`NSModalResponseCancel`が返される

## SheetWindowController

- Xibの画面で、`Visible At Launch`をオフにすることに注意
    - [How to Modally Present Windows as Sheets in OS X (Yosemite & Mavericks)](http://www.nickkuh.com/mac-os-x/how-to-modally-present-windows-as-sheets-in-os-x-yosemite-mavericks/2015/01/)

![-w997](https://i.imgur.com/d8jp4hU.jpg)

- コード全体は以下の通り。

``` ObjectiveC
#import "SheetWindowController.h"
@interface SheetWindowController ()
@end

@implementation SheetWindowController
- (id)init {
    if (self = [super initWithWindowNibName:[self className] owner:self]) {
        
    }
    return self;
}

- (void)windowDidLoad {
    [super windowDidLoad];
    
    // Implement this method to handle any initialization after your window controller's window has been loaded from its nib file.
}

- (IBAction)okButtonPush:(id)sender {
    [self.window.sheetParent endSheet:self.window returnCode:NSModalResponseOK];

}

- (IBAction)cancelButtonPush:(id)sender {
    [self.window.sheetParent endSheet:self.window returnCode:NSModalResponseCancel];
}

@end
```

- クラスの`init`で`xib`をロードする場合、上記の通りoverrideすることを忘れないように。
- シートのParentに対して`endSheet`を呼び出してやる。このときにボタンの情報を`returnCode`で返す。

## ModalWindowController

``` ObjectiveC
#import "ModalWindowController.h"

@interface ModalWindowController ()

@end

@implementation ModalWindowController
- (id)init {
    if (self = [super initWithWindowNibName:[self className] owner:self]) {
        
    }
    return self;
}


- (void)windowDidLoad {
    [super windowDidLoad];
    
    // Implement this method to handle any initialization after your window controller's window has been loaded from its nib file.
}

- (IBAction)okButtonPush:(id)sender {
    [NSApp stopModalWithCode:NSModalResponseOK];
    [[sender window] orderOut:self];
}

- (IBAction)cancelButtonPush:(id)sender {
    [NSApp stopModalWithCode:NSModalResponseCancel];
    [[sender window] orderOut:self];
}

@end
```

- 呼び出し元の`runModalForWindow`に対して、下記を呼んでいる。
- `stopModalWithCode`はモーダルを解除するだけ（呼び出し元のウィンドウ操作を可能にする）で、ウィンドウのクローズは別に行う必要がある。
- ちなみに`orderOut`は非表示にするだけで、Windowのリリースはしていない。
    - リリースをするのは`close`である
    - [Cocoa API解説(macOS/iOS)](http://cocoaapi.hatenablog.com/entry/00000115/recID33592)

``` ObjectiveC
[NSApp stopModalWithCode:NSModalResponseOK];
[[sender window] orderOut:self];
```

# 参考
- シート
    - [How to Modally Present Windows as Sheets in OS X (Yosemite & Mavericks)](http://www.nickkuh.com/mac-os-x/how-to-modally-present-windows-as-sheets-in-os-x-yosemite-mavericks/2015/01/)
    
- モーダルウィンドウ
    - ヒレガス本のP407辺り
    - [Cocoa API解説(macOS/iOS)](http://cocoaapi.hatenablog.com/entry/NSApplication/stopModalWithCode_)
    - [How Modal Windows Work](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/WinPanel/Concepts/UsingModalWindows.html#//apple_ref/doc/uid/20000223-CJBEADBA)