NSNotificationを使って設定パネルを実装する

# 概要

![-w821](https://i.imgur.com/KjmCGL8.jpg)

- 例えば設定を変更した際に、複数のウィンドウのUIをまとめて更新したい場合、`delegate`では限界があります。
    - [iOSアプリエンジニアのための面接質問集100選 \- NSBlogger](http://nsblogger.hatenablog.com/entry/2016/12/24/ios_interview)

    >Q25. NSNotificationCenterとDelegateの使い分けを説明してください。
>NotificationCenterはブロードキャスト、Delegateは指定されたクラスへ通知。

- そういうときに便利なのが`NSNotification`です。
- `通知名`をキーとして、通知を監視するクラスと通知を送信するクラス間で情報をやり取りします。
- （欠点としては飛び道具なので、適当に使うと構造が不透明になったり、クラスが密結合になってしまいかねない所でしょうか。）

# GitHub

https://github.com/pommdau/Qiita_NSNotification01.git

# 参考
- ヒレガス本

# コード全体
## メイン画面（通知を受け取る側）

```objc:AppDelegate.m
#import "AppDelegate.h"
#import "PreferencesController.h"
@interface AppDelegate ()
@property (weak) IBOutlet NSWindow *window;
@property PreferencesController *preferencesController;
// 本来はAppDelegateに書かず、MainMenuウィンドウのためのWindowControllerを建てる
@property (weak) IBOutlet NSTextField *needsSendingMailLabel;
@property (weak) IBOutlet NSTextField *mailAddressLabel;
@end

@implementation AppDelegate

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
    // Insert code here to initialize your application
    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
    [notificationCenter addObserver:self
                           selector:@selector(handleSendingMailChange:)
                               name:IKESendingMailChangedNotification
                             object:nil];
    
    [notificationCenter addObserver:self
                           selector:@selector(handleMailAddressChange:)
                               name:IKEMailAddressChangedNotification
                             object:nil];
    
    PreferencesController *preferencesController = [PreferencesController new];
    [preferencesController showWindow:self];
    _preferencesController = preferencesController;
}

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


// Notification Receive Methods
- (void)handleSendingMailChange:(NSNotification *)notification {
    NSLog(@"(RECEIVE)メール送信有無の変更通知");
    BOOL needsSendingMail = [notification.userInfo[IKENeedsSendingMailKey] boolValue];
    if (needsSendingMail) {
        [_needsSendingMailLabel setStringValue:@"必要"];
    } else {
        [_needsSendingMailLabel setStringValue:@"不要"];
    }
}

- (void)handleMailAddressChange:(NSNotification *)notification {
    NSLog(@"(RECEIVE)メールアドレスの変更通知");
    [_mailAddressLabel setStringValue:notification.userInfo[IKEMailAddressKey]];
}

@end
```

## 設定画面（通知を送る側）

```objc:PreferencesController.h
#import <Cocoa/Cocoa.h>

NS_ASSUME_NONNULL_BEGIN

// 通知名には衝突を避けるためにPrefixをつけると良い。
extern NSString *const IKESendingMailChangedNotification;
extern NSString *const IKEMailAddressChangedNotification;

// userinfo用のキー
extern NSString *const IKENeedsSendingMailKey;  // メール有無
extern NSString *const IKEMailAddressKey;       // メールアドレス

@interface PreferencesController : NSWindowController
@end

NS_ASSUME_NONNULL_END
```

```objc:PreferencesController.m
#import "PreferencesController.h"

NSString *const IKESendingMailChangedNotification = @"IKESendingMailChanged";
NSString *const IKEMailAddressChangedNotification = @"IKEMailAddressChanged";
NSString *const IKENeedsSendingMailKey            = @"needsSending";
NSString *const IKEMailAddressKey                 = @"mailAddress";

@interface PreferencesController () <NSTextFieldDelegate>
@property (weak) IBOutlet NSTextField *mailAddressTF;

@end

@implementation PreferencesController

- (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)sendingMailCheckBoxClicked:(NSButton *)sender {
    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
    NSLog(@"(POST)メール有無の変更通知");
    NSNumber     *needsSendingMail = [NSNumber numberWithBool: sender.state];
    NSDictionary *userinfoDic      = @{IKENeedsSendingMailKey : needsSendingMail};
    [notificationCenter postNotificationName:IKESendingMailChangedNotification
                                      object:self
                                    userInfo:userinfoDic];
}

// MARK:- NSTextField Notification Methods
- (void)controlTextDidChange:(NSNotification *)aNotification {
    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
    NSLog(@"(POST)メールアドレスの変更通知");
    NSDictionary *userinfoDic = @{IKEMailAddressKey : _mailAddressTF.stringValue};
    [notificationCenter postNotificationName:IKEMailAddressChangedNotification
                                      object:self
                                    userInfo:userinfoDic];
}
@end
```

# コード詳細
## 設定画面（通知を送る側）
### PreferencesController.h

```objc
// 通知名には衝突を避けるためにPrefixをつけると良い。
extern NSString *const IKESendingMailChangedNotification;
extern NSString *const IKEMailAddressChangedNotification;

// userinfo用のキー
extern NSString *const IKENeedsSendingMailKey;  // メール有無
extern NSString *const IKEMailAddressKey;       // メールアドレス
```

- 通知名のグローバル定数を作成する
- 同様に通知内容の`(NSDictionary *)userinfo`を受け取るときのために、キーのグローバル定数を作成する

### PreferencesController.m

```objc
NSString *const IKESendingMailChangedNotification = @"IKESendingMailChanged";
NSString *const IKEMailAddressChangedNotification = @"IKEMailAddressChanged";
NSString *const IKENeedsSendingMailKey            = @"needsSending";
NSString *const IKEMailAddressKey                 = @"mailAddress";
```

- `PreferencesController.h`で宣言した定数に具体的な値をつける
    - 通知名には衝突を避けるためにPrefixをつけると良い。

```objc
@interface PreferencesController () <NSTextFieldDelegate>
```

- `NSTextField`の値変更を監視するため`NSTextFieldDelegate`を採用する

```objc
- (IBAction)sendingMailCheckBoxClicked:(NSButton *)sender {
    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
    NSLog(@"(POST)メール有無の変更通知");
    NSNumber     *needsSendingMail = [NSNumber numberWithBool: sender.state];
    NSDictionary *userinfoDic      = @{IKENeedsSendingMailKey : needsSendingMail};
    [notificationCenter postNotificationName:IKESendingMailChangedNotification
                                      object:self
                                    userInfo:userinfoDic];
}
```

- **通知を送るときの手順**は以下の通り。
- `NSNotificationCenter`オブジェクトを作成する
- 通知する際に送りたい情報を`(NSDictionary *)userinfoDic`に登録する
- `NSNotificationCenter`オブジェクトのメソッド`postNotificationName`で通知を送信する
    - `object`に文字列を指定しているコードを見たことがあるが、別に`userinfo`があるので、ここには`self`を指定するのが筋だと思う。
    - `object`は例えば、通知先で`self`が何か、つまり通知元がどこかを判別するのに使ったりするのではないかと思う。

```objc
- (void)controlTextDidChange:(NSNotification *)aNotification {
```

- やっていることは同じなので割愛

## メイン画面（通知を受け取る側）
### AppDelegate.m

```objc
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
    // Insert code here to initialize your application
    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
    [notificationCenter addObserver:self
                           selector:@selector(handleSendingMailChange:)
                               name:IKESendingMailChangedNotification
                             object:nil];
```

- **通知を受け取る設定の手順**は以下の通り。
- `NSNotificationCenter`オブジェクトを作成
- `addObserver...`で登録
    - addObserver:`監視する主体`
    - selector:`通知があった際に実行する関数`
    - name:`通知名`
    - object:`通知送信元の指定`
- [詳細：addObserver:selector:name:object:](https://developer.apple.com/documentation/foundation/nsnotificationcenter/1415360-addobserver?language=objc)

```objc
// Notification Receive Methods
- (void)handleSendingMailChange:(NSNotification *)notification {
    NSLog(@"(RECEIVE)メール送信有無の変更通知");
    BOOL needsSendingMail = [notification.userInfo[IKENeedsSendingMailKey] boolValue];
    if (needsSendingMail) {
        [_needsSendingMailLabel setStringValue:@"必要"];
    } else {
        [_needsSendingMailLabel setStringValue:@"不要"];
    }
}
```

- 上記の`selector`で指定した関数が、通知を受け取った際に呼ばれる
- 通知の際に送った`(NSDictionary *)userinfo `は`notification.userInfo[IKENeedsSendingMailKey]`のように受け取ることができる。