[WKWebView][RxSwift] 規約ページの一番下にスクロールした時にネイティブ側の同意ボタンを有効にする


概要

 webで公開されている規約ページをWKWebViewで読み込み、ページの一番下までスクロールされたことをアプリでキャッチして同意ボタンを有効にします。
(特に受諾開発の案件でよくあるのかなーと思います)

 この仕様を振られてちょっと困ったのですが、WkWebViewのプロパティでUIScrollViewが公開されているのでdidEndDecelerating()でスクロール位置を判定できるのでした。
https://developer.apple.com/documentation/webkit/wkwebview/1614784-scrollview

 更に、読み込み途中のページでスクロール位置を判定されてはいけないので、estimatedProgressをKVOで監視してロードの判定も行います(無理矢理感あります。他に何かいい方法がありそうです)。

 「ロード完了」かつ「スクロール位置が底」ならば同意ボタンを有効・そうでなければ無効とします。

実装


import UIKit
import RxSwift
import WebKit

class ViewController: UIViewController {


    @IBOutlet weak var webView: WKWebView!
    @IBOutlet weak var agreeButton: UIButton!

    let agreementUrl = URL(string: "https://qiita.com/terms")

    let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        agreeButton.isEnabled = false
        bindScrollAgreement()

        let urlRequest = URLRequest(url: self.agreementUrl!)
        self.webView.load(urlRequest)
    }

    private func bindScrollAgreement() {

        let margin: CGFloat = 5 // 底かどうかのマージン

        Observable.combineLatest(self.webView.rx.isLoadCompleted,
                                 self.webView.scrollView.rx.didEndDecelerating) {(isLoadCompleted, _) -> Bool in

                                    let bottomEdge = self.webView.scrollView.contentOffset.y
                                                        + self.webView.scrollView.frame.size.height
                                                        + margin

                                    let position = bottomEdge - self.webView.scrollView.contentSize.height
                                    let isBottom = position > 0

                                    return isLoadCompleted && isBottom
            }
            .bind(to: self.agreeButton.rx.isEnabled)
            .disposed(by: disposeBag)
    }
}

extension Reactive where Base: WKWebView {

    /// ロード完了かどうか
    public var isLoadCompleted: Observable<Bool> {
        return self.observe(Double.self, "estimatedProgress")
            .throttle(0.1, scheduler: MainScheduler.instance)
            .map { $0 == 1.0 }
    }
}

動画

補足

1回一番下まで行ったら後でまた上にスクロールしてもずっと有効したい場合、以下のようにオペレータを追加しましょう。

    .filter { $0 }
    .take(1)
    .bind(to: self.agreeButton.rx.isEnabled)