【Swift】NotificationCenterのObserverとUserInfoの型を紐付ける

# はじめに

NotificationCenterを利用しようとした際に、userInfoに入っている値を取得したりキャストしたりするのが煩わしく感じることはないでしょうか？
通知登録時に受け取りたい値の型も明記することなく、直接値を利用できる方法を紹介していきたいと思います。

# NotificationCenterの利用例

例としてキーボードが立ち上がった際に、画面に表示されているUITableViewのheightを変更する処理を記述します。
NotificationCenterを使用してNotificationを監視する際に、下記のコードのようにnotificationのuserInfoにキーを指定してオブジェクトを取得し、そのオブジェクトを任意の型にキャストすることになると思います。

```swift
NotificationCenter.default.addObserver(forName: .UIKeyboardWillShow, object: nil, queue: .main) { [unowned self] notification in
    guard
        let userInfo = notification.userInfo,
        let keyboardInfo = UIKeyboardInfo(info: userInfo)
    else { return }

    self.tableViewBottomConstraint.constant = keyboardInfo.frame.size.height
    UIView.animate(withDuration: keyboardInfo.duration, delay: 0, options: keyboardInfo.animationCurve, animations: {
        self.view.layoutIfNeeded()
    }, completion: nil)
}
```

上記のコードで利用している`UIKeyboardInfo`は、Dictionaryの中から必要な値を取得し自身のPropertyとして保持する実装になっています。

```swift
struct UIKeyboardInfo {
    let frame: CGRect
    let animationDuration: TimeInterval
    let animationCurve: UIViewAnimationOptions

    init?(info: [AnyHashable : Any]) {
        guard
            let frame = (info[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue,
            let duration = info[UIKeyboardAnimationDurationUserInfoKey] as? TimeInterval,
            let curve = info[UIKeyboardAnimationCurveUserInfoKey] as? UInt
        else {
            return nil
        }
        self.frame = frame
        self.animationDuration = duration
        self.animationCurve = UIViewAnimationOptions(rawValue: curve)
    }
}
```

[NoticeObserveKit](https://github.com/marty-suzuki/NoticeObserveKit)を利用することで、任意のオブジェクトを直接受け取れるようになりつつ、Observerに対して特定の型を紐付けることができるので、キャストをしたり型を明記する必要がなくなります。

```swift
UIKeyboardWillShow.observe { [unowned self] keyboardInfo in
    self.tableViewBottomConstraint.constant = keyboardInfo.frame.size.height
    UIView.animate(withDuration: keyboardInfo.duration, delay: 0, options: keyboardInfo.animationCurve, animations: {
        self.view.layoutIfNeeded()
    }, completion: nil)
}
```

# NoticeObserveKitの利用方法

`CocoaPods`または`Carthage`からインストールすることができます。

```ruby
pod "NoticeObserveKit" #CocoaPods
```

```ruby
github "marty-suzuki/NoticeObserveKit" #Carthage 
```

まず、`NoticeType`を採用した`struct`を定義します。
`NoticeType`によって、`typealias InfoType`と`static let name: Notification.Name`の実装が強制されているので、それらを実装します。
`InfoType`に定義するものは、実際に受け取りたいオブジェクトの型になります。

```swift
struct UIKeyboardWillShow: NoticeType {
    typealias InfoType = UIKeyboardInfo
    static let name: Notification.Name = .UIKeyboardWillShow
}
```

上記で定義したstructの`observe(queue:object:recieving:using:)`を呼ぶだけで、通知監視が完了します。

# ViewControllerでの利用例

実際にViewController内で利用する際には、下記のような実装になります。

```swift
import UIKit
import NoticeObserveKit

class ViewController: UIViewController {
    private let searchBar = UISearchBar(frame: .zero)
    private var pool = NoticeObserverPool()

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        navigationItem.titleView = searchBar

        configureObservers()
    }

    private func configureObservers() {
        UIKeyboardWillShow.observe { [unowned self] in
            print("UIKeyboard will show = \($0)")
        }.addObserverTo(pool)

        UIKeyboardWillHide.observe { [unowned self] in
            print("UIKeyboard will hide = \($0)")
        }.addObserverTo(pool)
    }
}
```

`NoticeObserverPool`にobserverを登録しておくことで、ViewControllerが破棄されたタイミングで通知監視も解除されるようになります。
それ以外の任意のタイミングで通知監視を解除したい場合は、`observe(queue:object:recieving:using:)`で返される`NoticeObserver`の`dispose()`を呼ぶことで解除が可能です。

# カスタマイズ

### Notificationを取得

おおもとの`Notification`も取得したい場合は、observe時の`recieving`にクロージャーを渡すことで取得が可能です。

```swift
UIKeyboardWillShow.observe(recieving: { notification in
    print(notification)
}) { keyboardInfo in
    print(keyboardInfo)
}.addObserverTo(pool)
```

### 任意のキーで値を直接取得

先程定義した`UIKeyboardWillShow`を`UIKeyboardInfo`を使わずに直接値を取りたい場合などは、下記のように`static let infoKey: String`に取得したい値のキーを定義することが実現できます。

```swift
struct UIKeyboardWillShow: NoticeType {
    typealias InfoType = NSValue
    static let infoKey: String = UIKeyboardFrameEndUserInfoKey
    static let name: Notification.Name = .UIKeyboardWillShow
}
```

### 自前のNotifiation

例として、UINavigationControllerでpushしたViewControllerが表示されたことをpostする実装をしていきます。実際に利用する際は、下記のようになるかと思います。

```swift
extension ViewController: UINavigationControllerDelegate {
    func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
        let content = NavigationControllerContent(viewController: viewController, animated: animated)
        NavigationControllerDidShow.post(info: content)
    }
}
```

ここで定義する`NoticeType`を採用したstructは下記のようになります。

```swift
struct NavigationControllerDidShow: NoticeType {
    typealias InfoType = NavigationControllerContent
    static var name = Notification.Name("navigationControllerDidShow")
}
```

# 最後に

[NotifwiftでNSNotification.userInfoをSwiftyに扱おう - Qiita](http://qiita.com/takasek/items/141adc4bbc58894551cd)で紹介されている、[Notifwift](https://github.com/takasek/Notifwift)の通知監視の解除方法が面白いなと思っていたので、そちらを参考にさせていただいています。