通知設定状態をリアルタイムで取得し、UIに反映させる with RxSwift

# 概要

- 画面の変更をあまり意識することなく、通知設定を取得・UIに反映する方法を書く
- アプリ開発をしているとき、通知設定画面で通知設定状態を取得し、 `通知設定がOFFになっています` みたいな画面を実装すること、よくあるのではないでしょうか？　今回はその実装をRxSwiftを用いて作りたいと思います

# イメージ

![z.gif](https://qiita-image-store.s3.amazonaws.com/0/126441/8ae9c095-02e1-d1f4-ea50-30586ef9e122.gif)

- 画面を開くたびに通知設定を取得し、UIを更新する

# サンプルリポジトリ

- [https://github.com/ryo-takahashi/RxNotificationExample](https://github.com/ryo-takahashi/RxNotificationExample)

# 環境

- Xcode9.4
- Swift4.1
- RxSwift4.2
- RxCocoa4.2

# 導入

```ruby
pod install 'RxSwift'
pod install 'RxCocoa'
```

# 画面の作成

|ViewController|DisableNotificationCoverView|
|:-:|:-:|
|![スクリーンショット 2018-08-19 3.00.27.png](https://qiita-image-store.s3.amazonaws.com/0/126441/a3432055-fca8-9a41-2414-a858c1ff1380.png)|![スクリーンショット 2018-08-19 3.00.46.png](https://qiita-image-store.s3.amazonaws.com/0/126441/33241754-a5c9-bfc4-aa97-7cf53f97ce80.png)|
|DisableNotificationCoverViewを１番上の階層に配置し、isHidden = trueにしておく|一応使い回しできるように、Viewを切り出す|

# 通知状態を良い感じに取得してくれるクラスの作成

```swift:RxNotificationCenter.swift
import RxSwift
import RxCocoa
import UserNotifications

class RxNotificationCenter {
    static let shared = RxNotificationCenter()

    let authorizationStatus = PublishRelay<UNAuthorizationStatus>()

    private init() {}

    func updateAuthorizationStatus() {
        // 通知設定状態を取得し、オブザーバに流す
        UNUserNotificationCenter.current().getNotificationSettings { [weak self] settings in
            self?.authorizationStatus.accept(settings.authorizationStatus)
        }
    }
}
```

- シングルトンインスタンスとして使えるように定義

# 通知オフですよのカバーViewを作成

```swift:DisableNotificationCoverView.swift
class DisableNotificationCoverView: UIView {

    @IBOutlet weak var openNotificationSettingButton: UIButton!

    private let disposeBag = DisposeBag()

    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        commonInit()
    }

    override func awakeFromNib() {
        openNotificationSettingButton.rx.tap
            .subscribe(onNext: { [weak self] in
                // ボタンがタップするたびにここの処理が呼ばれる
                if let url = URL(string: UIApplicationOpenSettingsURLString), UIApplication.shared.canOpenURL(url) {
                    // 本体設定のこのアプリの設定画面を開く処理
                    UIApplication.shared.open(url, options: [:], completionHandler: nil)
                }
            })
            .disposed(by: disposeBag)
    }

    private func commonInit() {
        let bundle = Bundle(for: type(of: self))
        let nib = UINib.init(nibName: "DisableNotificationCoverView", bundle: bundle)
        let view = nib.instantiate(withOwner: self, options: nil).first as! UIView
        self.addSubview(view)
    }
}

```

- 通知を取得する処理と、通知設定へ移動させる画面ができたので、ここから通知設定画面（仮）を作っていく

# 通知設定画面

- ここでやっていることは以下
    - 最初に画面を作成した時
        - RxNotificationCenterの通知設定状態のオブザーバを監視、変更があったらUIを変更する
        - 通知設定状態を取得させてオブザーバに流すように命令
    - 画面を再表示したとき（一度離れ、別の画面からまた遷移してきたときなど）
        - 通知設定状態を取得させてオブザーバに流すように命令

```swift:ViewController.swift
import UIKit
import UserNotifications
import RxSwift

class ViewController: UIViewController {

    @IBOutlet weak var disableNotificationCoverView: UIView!

    private let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()
        navigationItem.title = "通知設定"
        setupViewController()
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        // 通知設定状態を取得させてオブザーバに流すように命令
        RxNotificationCenter.shared.updateAuthorizationStatus()
    }

    private func setupViewController() {
        // RxNotificationCenterの通知設定状態のオブザーバを監視、変更があったらUIを変更する
        RxNotificationCenter.shared.authorizationStatus
            .map { status -> Bool in
                switch status {
                case .authorized:
                    return true
                case .denied, .notDetermined:
                    return false
                }
            }
            .bind(to: disableNotificationCoverView.rx.isHidden)
            .disposed(by: disposeBag)
        // 通知設定状態を取得させてオブザーバに流すように命令
        RxNotificationCenter.shared.updateAuthorizationStatus()
    }
}
```

# 最後に、通知設定画面からアプリに戻ってきたときに通知設定状態を取得する

```swift:AppDelegate.swift

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    func applicationWillEnterForeground(_ application: UIApplication) {
        RxNotificationCenter.shared.updateAuthorizationStatus()
    }

}
```

- ホーム画面や、別アプリから戻ってきた時、`ViewController` の `viewWillAppear`, `viewDidAppear` が呼ばれるのではないかと思いがちだが、実は呼ばれません。
- `AppDelegate` の `applicationWillEnterForeground` が呼ばれるので、そこに通知取得処理を挟んでおく

# まとめ

- RxSwift/RxCocoaを使い、通知状態をView`bind`することで、そこまでUIの変化を意識せずに通知設定取得処理を書くことができた
- ホーム画面、別アプリからアプリへ戻ってきたときは `AppDelegate の `applicationWillEnterForeground` が呼ばれる

# Notes

- こう書くともっとスマートになるよ、の声、待ってます 🙏