RxSwiftを使ってWKWebViewのローディング処理をイイ感じにする

# 概要

- WKWebViewのローディング処理（プログレスバーのUI更新やNavigationBarのTitleの変更などをイイ感じにRxSwiftで書く

# イメージ

![rsw.gif](https://qiita-image-store.s3.amazonaws.com/0/126441/167e3368-cdc9-7052-f07f-c71cfca1d07c.gif)

# サンプルリポジトリ

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

# 環境

- Xcode 9.4
- Swift 4.1
- RxSwift 4.2
- RxCocoa 4.2
- RxOptional 3.5.0

# 環境構築

```ruby:Podfile
  pod 'RxSwift'
  pod 'RxCocoa'
  pod 'RxOptional'
```

# 画面

|ViewController|WKWebViewを内包したViewController|
|:-:|:-:|
|![スクリーンショット 2018-08-21 1.13.51.png](https://qiita-image-store.s3.amazonaws.com/0/126441/40f80690-f091-7ce8-bb6a-333c4b9749e0.png)|![スクリーンショット 2018-08-21 1.14.01.png](https://qiita-image-store.s3.amazonaws.com/0/126441/704d0042-cf9a-5cb5-8be3-cc77b6da0f30.png)|

- ボタンをタップしたらWKWebViewを内包したViewControllerに遷移する

# 実装していく

## まずは、Swiftのobserverを使ったパターンで実装

```swift:WKWebViewController.swift
import UIKit
import WebKit

class WKWebViewController: UIViewController {
    @IBOutlet weak var webView: WKWebView!
    @IBOutlet weak var progressView: UIProgressView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupWebView()
    }
    
    private func setupWebView() {
        webView.addObserver(self, forKeyPath: "loading", options: .new, context: nil)
        webView.addObserver(self, forKeyPath: "estimatedProgress", options: .new, context: nil)
        
        let url = URL(string: "https://www.google.com/")
        let urlRequest = URLRequest(url: url!)
        webView.load(urlRequest)
        progressView.setProgress(0.1, animated: true)
    }
    
    deinit {
        webView?.removeObserver(self, forKeyPath: "loading")
        webView?.removeObserver(self, forKeyPath: "estimatedProgress")
    }
    
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if keyPath == "loading" {
            UIApplication.shared.isNetworkActivityIndicatorVisible = webView.isLoading
            if !webView.isLoading {
                progressView.setProgress(0.0, animated: false)
                navigationItem.title = webView.title
            }
        }
        if keyPath == "estimatedProgress" {
            progressView.setProgress(Float(webView.estimatedProgress), animated: true)
        }
    }
}
```

- 画面を開くと `google.com` がロードされる
- ロードにあわせて、以下の値が変化する
    - プログレスバーの値
    - プログレスバーの表示・非表示
    - `ActivityIndicator`の表示・非表示
- ロードが終わると、`NavigationController`の`title`に表示されたURLのタイトルが設定される

### この書き方の良いところ

- なし

### この書き方の悪いところ

- `"loading"`, `"estimatedProgress"` のように、keyをベタ書きしないといけない
    - typoしてもコンパイルエラーで見つけることができない
- deinit時に`removeObserver`しないといけない
    - しないと強制終了の可能性がある
- コードが読みにくい

## では、これを踏まえてRxSwiftを使ったパターンで実装

```swift:RxWKWebViewController.swift
import UIKit
import WebKit
import RxSwift
import RxCocoa
import RxOptional

class RxWKWebViewController: UIViewController {
    @IBOutlet weak var webView: WKWebView!
    @IBOutlet weak var progressView: UIProgressView!
    
    private let disposeBag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupWebView()
    }
    
    private func setupWebView() {

        // プログレスバーの表示制御、ゲージ制御、アクティビティインジケータ表示制御で使うため、一旦オブザーバを定義
        let loadingObservable = webView.rx.observe(Bool.self, "loading")
            .filterNil()
            .share()
        
        // プログレスバーの表示・非表示
        loadingObservable
            .map { return !$0 }
            .observeOn(MainScheduler.instance)
            .bind(to: progressView.rx.isHidden)
            .disposed(by: disposeBag)
        
        // iPhoneの上部の時計のところのバーの（名称不明）アクティビティインジケータ表示制御
        loadingObservable
            .bind(to: UIApplication.shared.rx.isNetworkActivityIndicatorVisible)
            .disposed(by: disposeBag)
        
        // NavigationControllerのタイトル表示
        loadingObservable
            .map { [weak self] _ in return self?.webView.title }
            .observeOn(MainScheduler.instance)
            .bind(to: navigationItem.rx.title)
            .disposed(by: disposeBag)
        
        // プログレスバーのゲージ制御
        webView.rx.observe(Double.self, "estimatedProgress")
            .filterNil()
            .map { return Float($0) }
            .observeOn(MainScheduler.instance)
            .bind(to: progressView.rx.progress)
            .disposed(by: disposeBag)
        

        let url = URL(string: "https://www.google.com/")
        let urlRequest = URLRequest(url: url!)
        webView.load(urlRequest)
    }
}

```

- 仕様は全く同じ

### 良いところ

- 読みやすい :tada:
- 処理が簡潔
- `removeObserver` を気にしなくてもよい

### 悪いところ

- key名がまだベタ書きになっている
- （少しずれているが、）習得コストが高い

# まとめ

- RxSwiftを使うことでコードが読みやすく、実装が簡潔になった
- めんどくさい`removeObserver` を気にしなくても良くなった
- 習得コストが高い

# 所感

- 別にRxSwiftを使わなくても実装はできる。
- が、人間は忘れます。 いまはダイジョブでも`removeObserver`を忘れるときが絶対来ます
    - そのときハマらないために、RxSwiftを使った書き方を覚えておきたいですね
        - RxSwiftでまずハマりそうなきがしたがそのときはがんばろう

# 次にやりたい

- `RxWebKit` というライブラリがあり、もっと簡潔にかけそうだったので RxWebKitを使った実装サンプル記事を書く