WKWebViewで正しくPullToRefreshする

WebViewでUIRefreshControlを使用してPullToRefreshする際、少しだけ工夫しなければ汚い挙動になってしまったので覚え書き。

というより、そもそもUIRefreshControlの使い方が間違っていた。

悪い例

f:id:taku_oka:20170811185655g:plain

間違った実装で不快な動きをしてしまっていた。 ユーザーが指を離すまえにリロードしてしまうと表示が崩れる。

コード

import UIKit
import WebKit

class BadExampleViewController: UIViewController {

    let webview = WKWebView()
    let refreshControl = UIRefreshControl()

    override func viewDidLoad() {
        super.viewDidLoad()

        webview.frame = view.frame
        view.addSubview(webview)

        webview.scrollView.refreshControl = refreshControl
        refreshControl.addTarget(self, action: #selector(self.didPullToRefresh(sender:)), for: .valueChanged)

        webview.load(URLRequest(url: URL(string: "https://www.apple.com/")!))
    }

    func didPullToRefresh(sender: UIRefreshControl) {
        refreshControl.endRefreshing()
        webview.reload()
    }
}

そもそも引っ張ってる途中のタイミングでローディングをするのはタイミングとして適切ではなかった。 特にWebViewはドラッグ中にリロードしてしまうと表示がガタつくのでいけない。 本来ローディングを開始すべきタイミングはユーザーが指を離した時だった。(恥ずかしい)

そもそもUIRefreshControlの正しい使い方とは

ScrollViewに追加

if #available(iOS 10.0, *) {
    scrollView.refreshControl = refreshControl
} else {
    scrollView.addSubview(refreshControl)
}

引っ張ったタイミングのイベントを受け取る

addTarget メソッドを使って関数を登録しておきます。

ローディングを開始したことを表すアニメーションを開始

beginRefreshing() を呼ぶ。 ローディングしている間、ScrollViewの上部に隙間があき、そこでUIRefreshControlがアニメーションし続けます。

ローディングが終了したことを表すアニメーションを開始

endRefreshing() を呼ぶ。 ScrollViewの上部に隙間でアニメーションしているUIRefreshControlのアニメーションが終了し、ScrollViewの上部に隙間が縮んでレイアウトが元の状態に戻ります。

正しい例

f:id:taku_oka:20170811185656g:plain

UIRefreshControlの使い方としては、

  • ユーザーが指を離してからロードを開始
  • ロードが終わってからendRefreshing()を呼ぶ

が正しい。

実装としてはUIScrollViewDelegateWKNavigationDelegateを使う必要がある。

ユーザーが指を離したタイミング

UIRefreshControlのイベントが発火したらユーザーがPullToRefreshを開始したとみなし、このプログラムではフラグ isRefreshingByPullをtureにしている。

WebViewのドラッグ終了タイミングはUIScrollViewDelegatefunc scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool)でとれるので、 そのときにisRefreshingByPullがtrueならPullToRefeshで指を離したタイミングとみなし、webview.reload()でロードを開始した。

ロードが完了したタイミング

WKNavigationDelegateで下記のどちらかを使うといい。

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!)

WebViewのコンテンツが完全にロードされたタイミングで呼ばれる。 通信状況やコンテンのサイズ次第ではかなり遅くなることもあるので、使わないことにした。

func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!)

Webページのリクエストとレスポンスの受け取りが完了し、コンテンツのロードが始まったタイミングで呼ばれる。

コンテンツのロードが始まるタイミングまでは画面が空白になってしまい状態がわからなくなってしまうので、ここまではUIRefreshControlをアニメーションさせておきたい。 なのでこれが呼ばれたタイミングでendRefreshing()を呼んだ。

コード

import UIKit
import WebKit

class GoodExampleViewController: UIViewController {

    let webview = WKWebView()
    let refreshControl = UIRefreshControl()

    var isRefreshingByPull = false

    override func viewDidLoad() {
        super.viewDidLoad()

        webview.frame = view.frame
        view.addSubview(webview)

        webview.scrollView.delegate = self
        webview.navigationDelegate = self

        webview.scrollView.refreshControl = refreshControl
        refreshControl.addTarget(self, action: #selector(self.didStartPullRefresh(sender:)), for: .valueChanged)

        webview.load(URLRequest(url: URL(string: "https://www.apple.com/")!))
   }

    func didStartPullRefresh(sender: UIRefreshControl) {
        isRefreshingByPull = true
        refreshControl.beginRefreshing()// UIRefreshControlのアニメーションを開始
    }

    func didEndDraggingToPullRefresh() {
        webview.reload()// ユーザーが指を離したらリロード
    }

    func didEndLoadToPullRefresh() {
        refreshControl.endRefreshing()// UIRefreshControlのアニメーションを終了
        isRefreshingByPull = false
    }
}

extension GoodExampleViewController: WKNavigationDelegate {

    func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
        if isRefreshingByPull {
            didEndLoadToPullRefresh()
        }
    }
}

extension GoodExampleViewController: UIScrollViewDelegate {

    func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
        if isRefreshingByPull {
            didEndDraggingToPullRefresh()
        }
    }
}