iOS
UIScrollView
WKWebView
ios11

WKWebViewのScrollView内にHeaderViewを追加したときのスクロール位置補正対策

はじめに

WKWebViewのScrollView内に自作のHeaderViewを追加してみたところ、
戻る、進む、更新などの操作で毎回HeaderViewの高さ分だけスクロール位置がずれてしまう問題が発生しました。
(iOS11以降の問題です。)

その問題の対策を纏めます。

開発環境

Category Version
Swift 4.1
Xcode 9.3 (9E145)

対策前のコード

とりあえず、HeaderViewを作ってWKWebViewのScrollViewにaddSubviewしてみました。

CustomView.swift
final class CustomView: UIView {

    private let headerViewHeight: CGFloat = 50.0

    var webView: WKWebView?
    var headerView: UIView?

    // ...
    // init時にWKWebViewのインスタンス生成(割愛)
    // ...

    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        super.traitCollectionDidChange(previousTraitCollection)

        // headerViewをscrollView内に追加
        setHeaderView()
    }

    func setHeaderView() {

        headerView?.removeFromSuperview()

        // 画面外に作成
        headerView = UIView(frame: CGRect(x: 0,
                                          y: -headerViewHeight,
                                          width: superview?.frame.height ?? frame.height,
                                          height: headerViewHeight))
        headerView?.backgroundColor = .blue

        // スクロール領域の拡張
        webView?.scrollView.contentInset = UIEdgeInsetsMake(headerViewHeight, 0, 0, 0)
        webView?.scrollView.addSubview(headerView ?? UIView())

        // スクロール開始位置を変更
        webView?.scrollView.setContentOffset(CGPoint(x: 0, y: -headerViewHeight), animated: false)
    }
}

発生した問題

iOS11以降で戻る、進む、更新などの操作をすると、HeaderViewの高さ分だけスクロール位置がずれてしまいます。
スワイプによる戻る、進むも同様でした。

before.gif

対策

UIScrollViewDelegateでフラグを駆使して対応してみました。

CustomView.swift
final class CustomView: UIView {

    private let headerViewHeight: CGFloat = 50.0
    private var scrollingToTop = false
    private var updatingContentOffsetY = false
    private var isOverTheContentSizeHeight = false
    private var isUnderTheStartingPosition = false

    var webView: WKWebView?
    var headerView: UIView?

    // ...
}

// MARK: - UIScrollViewDelegate

extension CustomView: UIScrollViewDelegate {

    func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
        scrollingToTop = true
        return true
    }

    func scrollViewDidScrollToTop(_ scrollView: UIScrollView) {
        scrollingToTop = false
    }

    func scrollViewDidScroll(_ scrollView: UIScrollView) {

        if scrollView.frame.height + scrollView.contentOffset.y > scrollView.contentSize.height {
            // bouncing
            isOverTheContentSizeHeight = true
        }

        if scrollView.contentOffset.y < -headerViewHeight {
            // bouncing
            isUnderTheStartingPosition = true
        }

        // 以下の全てを満たす場合、webView.scrollViewのContentOffsetYを更新する
        // - scrollView内部のドラッグをしていない
        // - ステータスバータップによる一番上へのスクロールをしていない
        // - webView.scrollViewのContentOffsetY更新中でない
        // - scrollviewの一番下より下にいない (not bouncing)
        // - scrollviewの一番上より上にいない (not bouncing)

        let isUpdateableContentOffsetY = !scrollView.isDragging
            && !scrollingToTop
            && !updatingContentOffsetY
            && !isOverTheContentSizeHeight
            && !isUnderTheStartingPosition

        if isUpdateableContentOffsetY {

            updatingContentOffsetY = true

            if #available(iOS 11.0, *) {
                var newContentOffsetY: CGFloat
                if scrollView.contentOffset.y < 0 {
                    newContentOffsetY = -headerViewHeight
                } else {
                    newContentOffsetY = scrollView.contentOffset.y - headerViewHeight
                }
                print("newContentOffsetY: \(newContentOffsetY)")
                webView?.scrollView.setContentOffset(CGPoint(x: 0, y: newContentOffsetY), animated: false)
            }

            updatingContentOffsetY = false
        }
    }

    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        isOverTheContentSizeHeight = false
        isUnderTheStartingPosition = false
    }
}

iOS11未満では、そもそもこの問題が発生していなかったので余計なスクロール位置補正を実行しないように
if #available(iOS 11.0, *) { }
で囲っています。

対策後

iOS11以降で戻る、進む、更新やスワイプによる戻る、進むなどの操作をしたとき
HeaderViewの高さ分だけスクロール位置のずれが補正されて、想定のスクロール位置になりました。

after.gif

さいごに

webView.scrollViewのbouncesを切ってしまうという選択肢がある場合は、
bouncingのチェックを無くすことができるのでもう少しシンプルに実装できます。

作成したサンプルプロジェクトをGitHubにあげています。
https://github.com/stv-yokudera/ios-wkwebview-demo