RxWebKitがいい感じだったよ
RxSwiftの関連ライブラリーを調べていたところRxWebKitなるものを発見。
要はWebKitのRxSwiftラッパーです。
いい感じにWKWebViewを扱えてとても楽しい!
結構素敵な感じに使えたので、シンプルなサンプルアプリを作ってみます。
導入
まずは普通に
pod 'RxWebKit'
ってな感じにしてpod install。 Carthageでもできますが、例のごとく割愛いたします。
普通にViewを構築
WKWebViewはコードで追加するので、タイトルやロードビュー、進む戻るボタンなどをテキトーに配置します。
IBOutletでViewControllerに引っ張ってきます。
import UIKit import WebKit class ViewController: UIViewController { @IBOutlet weak var progressbar: UIProgressView! @IBOutlet weak var refreshButton: UIBarButtonItem! @IBOutlet weak var forwardButton: UIBarButtonItem! @IBOutlet weak var titleLabel: UILabel! @IBOutlet weak var backButton: UIBarButtonItem! @IBOutlet weak var toolbar: UIToolbar! @IBOutlet weak var containerView: UIView! }
次に、viewDidLoad内で、WKWebViewを追加します。 navigationDelegateはぶっちゃけ今回いりませんが書いちゃってます。
setUpConstraints(for webView: WKWebView )では、WKWebViewをcontainerViewにぴったり貼り付ける制約を加えています。 webview.loadで、githubをロードし始めます。
override func viewDidLoad() { super.viewDidLoad() // set up WKWebView let webview = WKWebView() webview.navigationDelegate = self webview.translatesAutoresizingMaskIntoConstraints = false containerView.addSubview(webview) setUpConstraints(for: webview) // load github.com let request = URLRequest(url: URL(string: "https://www.github.com")!) webview.load(request) } private func setUpConstraints(for webview: WKWebView) { webview.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 0.0).isActive = true webview.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 0.0).isActive = true webview.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 0.0).isActive = true webview.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: 0.0).isActive = true }
これでViewパーツの配置は終わりです。
webviewのプロパティをバインディングしていく
ここからRxWebKitを活用する作業に入っていきます。
webview.rx
からWKWebViewに拡張されたRX系のプロパティにアクセスできます。
まずは、RxSwift、RxWebKitをimportします。
import RxWebKit import RxSwift
そして、webviewのtitleをtitleLabelのtextにbindします。 こうすることで、webviewが遷移する度に表示されているページのタイトルがtitleLabelに自動的に反映されます。 driveを使うか迷ったのですが、どっちが良いんでしょうねこれ。
// bind title to title label webview.rx.title.bind(to: titleLabel.rx.text).addDisposableTo(disposeBag)
次に、進む戻るボタンのenabledを制御します。
WKWebViewには、canGoForwardとcanGoBackというプロパティがあり、それぞれ、進めるページがあるか、戻れるページがあるかを取得できます。
丁度ブラウザの進む戻るボタンと同じです。
これもRX化されているので、こちらで用意した進む戻るボタンのisEnabledにbindします。(こちらはdriveを使いました)
ただし、UIBarButtonのisEnabledはRxのobserverが用意されていません。
なので、自分で実装することになります。
今回は、RxSwift公式のサンプルに倣い、BindingExtension.swiftというファイルを作って実装しました。
import UIKit import RxSwift import RxCocoa extension Reactive where Base: UIBarButtonItem { var isEnabled: UIBindingObserver<Base, Bool> { return UIBindingObserver(UIElement: base) { button, isEnable in button.isEnabled = isEnable } } }
こんな具合に、送られてきたBoolをUIBarButtonItemのisEnableに対応させるようなObserverを作ります。 これに、先程のcanGoForwardとcanGorBackをbindすることで、自動的にUIBarButtonItemが有効化、無効化するようになります。
// handle back and forward buttons availablity webview.rx.canGoForward.asDriver(onErrorJustReturn: false).drive(forwardButton.rx.isEnabled).addDisposableTo(disposeBag) webview.rx.canGoBack.asDriver(onErrorJustReturn: false).drive(backButton.rx.isEnabled).addDisposableTo(disposeBag)
実際にこれらがタップされたときの処理と、 リフレッシュボタンがタップされたときの処理は、RxWebKitの機能を用いていないので割愛します。
最後に、プログレスバーの処理を設定します。
RxWebKitではページの読み込みに関係するプロパティとして、loadingプロパティとestimatedProgressが存在します。
前者は現在読み込みを行っているかを表しており、
後者は読み込みがどの程度完了しているか推定値で表しています。
プログレスバーにはUIProgressViewを用い、loadingプロパティを表示非表示に、estimatedProgressを進行度合いにbindしました。 なお、UIProgressViewのisHiddenと進行度合いを表すprogressはRX対応されていないので、またも自作observerを実装します。
先程のBindingExtensionに下記を追加します。 isProgressingは進行中かどうかisHiddenは消えているかを表しているため、 notを入れることでちょうど進行中に表示されるようになります。
extension Reactive where Base: UIProgressView { var progress: UIBindingObserver<Base, Float> { return UIBindingObserver(UIElement: base) { progressbar, progressRate in progressbar.progress = progressRate } } var isProgressing: UIBindingObserver<Base, Bool> { return UIBindingObserver(UIElement: base) { progressbar, isProgressing in progressbar.isHidden = !isProgressing } } }
これらに前述のプロパティをbindします。
コメントの下の行では、WKWebViewより前に来るように、bringSubViewで最前線に持ってきています。
// set up progressbar containerView.bringSubview(toFront: progressbar) webview.rx.loading.asDriver(onErrorJustReturn: false).drive(progressbar.rx.isProgressing).addDisposableTo(disposeBag) webview.rx.estimatedProgress.map { progress in return Float(progress) }.asDriver(onErrorJustReturn: 0.0).drive(progressbar.rx.progress).addDisposableTo(disposeBag)
ここで気をつけなければいけないのはestimatedProgressのbindです。 estimatedProgressはDouble型、UIProgressViewのprogressはFloat型なので、一度mapを噛ませるなりして型を揃えてやる必要があります。
以上で必要な処理は全てです。
結果
すっきりとしたコードでこんな感じのwebviewが実装できちゃいます!素敵!!
今回のサンプルのコードです。