はじめに (対象読者・この記事でわかること)
この記事は、Swiftを用いたiOSアプリケーション開発において、WKWebViewのプログレス表示が予期せず二重に表示されてしまう問題に直面している開発者の方々を対象としています。特に、UIWebViewからWKWebViewへ移行した際や、独自のプログレス表示を実装しようとした際にこの問題に遭遇しやすいかと思います。
この記事を読むことで、WKWebViewのプログレス表示が二重になる原因を理解し、その具体的な解決策をSwiftコードと共に習得できます。これにより、ユーザー体験を損なうことなく、洗練されたWebコンテンツ表示を実現するための知識と実装スキルを身につけることができるでしょう。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。
* SwiftによるiOSアプリケーション開発の基本的な知識
* WKWebViewの基本的な使い方(WKNavigationDelegateの理解)
* UI要素の配置と管理に関する基本的な理解(StoryboardまたはコードでのUI構築)
WKWebViewにおけるプログレス表示の課題
なぜプログレス表示が二重になるのか?
WKWebViewは、Webコンテンツの読み込み状況を監視し、その進捗をestimatedProgressプロパティを通じて提供します。このプログレス表示をUIに反映させる際、多くの開発者はWKNavigationDelegateのwebView(_:didFinish:)やwebView(_:didFailProvisionalNavigation:withError:)などのメソッドと組み合わせて実装します。
しかし、WKWebViewのプログレス表示が二重になってしまう主な原因は、以下の2つのシナリオが同時に発生してしまうことにあります。
-
WKWebView標準のプログレス表示:WKWebView自体が、内部的にWebコンテンツの読み込み進捗を計算し、それをestimatedProgressとして外部に公開しています。この値は0.0から1.0まで変化し、読み込み完了とともに1.0になります。 -
開発者自身によるカスタムプログレス表示: 開発者が
WKWebViewのestimatedProgressを監視し、UIProgressViewなどのUI要素にこの値をバインドして表示している場合。
問題は、WKWebViewのestimatedProgressが1.0になった後も、何らかの理由(例えば、JavaScriptによるDOM操作やリソースの追加読み込みなど)でWebコンテンツの「実際の」読み込みが完全に完了していないと判定され、再度プログレスが開始されるかのように振る舞う場合があることです。あるいは、didFinishコールバックが呼ばれた後も、estimatedProgressがまだ1.0に達していない(または、通信が完全に停止していない)状態から、再度プログレスが進んでしまうケースです。
特に、loadRequestメソッドを呼び出した際に、WKWebViewは内部的にプログレス表示の初期化を行いますが、もし画面遷移や再利用などで同一のWKWebViewインスタンスに対して複数回loadRequestが発行されると、それぞれの読み込みプロセスが重なり、結果としてプログレス表示が二重に重ねて表示されてしまうことがあります。
具体的な発生シナリオ
- 画面遷移からの復帰: ユーザーが別の画面に遷移し、その後元の画面に戻ってきた際に、
WKWebViewが再表示され、それに伴いプログレス表示も再初期化される。 - ページ内リンクのクリック: ページ内のリンクをクリックし、同じページ内でコンテンツが更新される場合。
- JavaScriptによるDOM操作: ページ読み込み後にJavaScriptが実行され、コンテンツが動的に変更・追加される場合。
UIWebViewからの移行: 以前のUIWebViewでは問題なかった実装が、WKWebViewの挙動の違いにより顕在化する。
これらのシナリオで、estimatedProgressが1.0になった後も、UI上のプログレスバーがリセットされずに、新しい読み込みのように表示されてしまう、あるいは最初のプログレス表示が完了する前に次のプログレス表示が開始してしまう、という現象が起こり得ます。
Swiftでの解決策:プログレス表示の重複を防ぐ実装
この問題を解決するためには、WKWebViewのプログレス表示のライフサイクルを正確に管理し、不要なプログレス表示を抑制することが重要です。以下に、具体的な解決策をコード例と共に示します。
解決策1:estimatedProgressの監視と制御
WKNavigationDelegateのwebView(_:didFinish:)メソッドは、Webコンテンツの主要な読み込みが完了したことを示しますが、必ずしも全てのリソース(画像、JavaScriptなど)の読み込み完了を保証するものではありません。estimatedProgressが1.0になったタイミングでプログレス表示を非表示にし、かつ、didFinishが呼ばれた際にプログレス表示がまだ表示されているようであれば、それを強制的に非表示にする、というロジックを組み込みます。
Swiftimport UIKit import WebKit class ViewController: UIViewController, WKNavigationDelegate { var webView: WKWebView! var progressView: UIProgressView! var isPageLoaded = false // ページが完全に読み込まれたかどうかのフラグ override func viewDidLoad() { super.viewDidLoad() setupWebView() setupProgressView() loadWebsite() } func setupWebView() { let webConfiguration = WKWebViewConfiguration() webView = WKWebView(frame: .zero, configuration: webConfiguration) webView.navigationDelegate = self // デリゲートを設定 webView.addObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: [.new], context: nil) webView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(webView) NSLayoutConstraint.activate([ webView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), webView.leadingAnchor.constraint(equalTo: view.leadingAnchor), webView.trailingAnchor.constraint(equalTo: view.trailingAnchor), webView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) } func setupProgressView() { progressView = UIProgressView(progressViewStyle: .default) progressView.trackTintColor = .lightGray // プログレスバーの背景色 progressView.progressTintColor = .systemBlue // プログレスバーの色 progressView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(progressView) NSLayoutConstraint.activate([ progressView.topAnchor.constraint(equalTo: webView.topAnchor), // WebViewのトップに配置 progressView.leadingAnchor.constraint(equalTo: view.leadingAnchor), progressView.trailingAnchor.constraint(equalTo: view.trailingAnchor), progressView.heightAnchor.constraint(equalToConstant: 2.0) ]) progressView.isHidden = true // 初期状態では非表示 } func loadWebsite() { guard let url = URL(string: "https://www.example.com") else { return } let request = URLRequest(url: url) webView.load(request) progressView.isHidden = false // 読み込み開始時に表示 isPageLoaded = false } // KVOによるestimatedProgressの監視 override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { if keyPath == #keyPath(WKWebView.estimatedProgress) { if let progress = webView.estimatedProgress { progressView.progress = Float(progress) // estimatedProgressが1.0になったら、プログレス表示を一時的に表示したままにする // (didFinishまで待つため) } } } // WKNavigationDelegateメソッド func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { print("Page finished loading") isPageLoaded = true // didFinishが呼ばれたら、プログレス表示を非表示にする UIView.animate(withDuration: 0.3, animations: { self.progressView.alpha = 0.0 }) { _ in self.progressView.isHidden = true self.progressView.alpha = 1.0 // 次回表示のためにアルファ値をリセット } } func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { print("Failed to load page: \(error.localizedDescription)") isPageLoaded = true // エラー時もプログレス表示を非表示にする UIView.animate(withDuration: 0.3, animations: { self.progressView.alpha = 0.0 }) { _ in self.progressView.isHidden = true self.progressView.alpha = 1.0 } } // ページ遷移が開始された際に、プログレス表示をリセット・表示 func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { print("Page started loading") isPageLoaded = false progressView.progress = 0.0 progressView.isHidden = false progressView.alpha = 1.0 // 明示的に表示 } // 画面が破棄される前にKVOの監視を解除 deinit { webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress)) } }
コード解説:
setupWebView():WKWebViewを初期化し、navigationDelegateをselfに設定します。estimatedProgressの変更を検知するためにKVO(Key-Value Observing)をwebView.addObserver(...)で設定します。setupProgressView():UIProgressViewを初期化し、WebViewのトップに配置します。初期状態では非表示にしておきます。loadWebsite(): Webサイトを読み込む際に、プログレスビューを再表示し、isPageLoadedフラグをfalseにリセットします。observeValue(forKeyPath:of:change:context:):estimatedProgressの値が変更されるたびに呼び出されます。このメソッドでprogressView.progressを更新します。webView(_:didFinish:): Webコンテンツの読み込みが完了した際に呼び出されます。ここでisPageLoadedフラグをtrueにし、アニメーションを伴ってプログレスビューを非表示にします。alphaを0にするのは、アニメーションを滑らかにするためです。webView(_:didFailProvisionalNavigation:withError:): 読み込みに失敗した場合も、同様にプログレスビューを非表示にします。webView(_:didStartProvisionalNavigation:): 新しいナビゲーションが開始された際に、プログレスビューをリセットし、再表示します。これにより、二重表示の起点となる可能性のある状態をクリアします。deinit: オブザーバーを解除し、メモリリークを防ぎます。
この実装では、didStartProvisionalNavigationでプログレス表示をリセットし、estimatedProgressの更新でプログレスバーを進めます。そして、didFinishで全ての読み込みが完了したと判断し、プログレスバーを非表示にすることで、一度の読み込みに対してプログレス表示が一度だけ表示されるように制御します。
解決策2:estimatedProgressが1.0になった後の明示的な制御
estimatedProgressが1.0になったとしても、didFinishがすぐに呼ばれない場合があるため、didFinishが呼ばれるまでプログレス表示を非表示にしない、という考え方もあります。上記のコード例では、estimatedProgressが1.0になった後も、didFinishが呼ばれるまでプログレスビューを非表示にせず、そのまま表示し続けるような挙動になっています。これは、didFinishで最終的に非表示にするための「待機」状態と捉えることができます。
もし、estimatedProgressが1.0になった時点で即座にプログレス表示を終了させたい場合は、observeValueメソッド内で以下のような条件分岐を追加することも考えられます。
Swift// observeValueメソッド内の一部 if keyPath == #keyPath(WKWebView.estimatedProgress) { if let progress = webView.estimatedProgress { progressView.progress = Float(progress) if progress == 1.0 { // estimatedProgressが1.0になったら、少し遅延させて非表示にする(didFinishを待つ) // または、didFinishで確実に非表示にするロジックに任せる // ここで直接非表示にすると、didFinishより早く消えてしまう可能性もあるため注意 } else if progress < 1.0 && !isPageLoaded { // isPageLoadedはdidFinishでtrueになる progressView.isHidden = false progressView.alpha = 1.0 } } }
ただし、このアプローチはdidFinishのタイミングとの兼ね合いで、一貫性のない表示になる可能性も否定できません。そのため、一般的にはdidFinishでの処理を主軸にし、estimatedProgressの1.0到達は「完了への目処」として捉え、最終的な表示制御はdidFinishに委ねるのが堅牢な方法と言えます。
解決策3:WKUIDelegateとの連携(応用)
WKUIDelegateは、JavaScriptのアラートやコンファームなどのUI表示を扱うためのプロトコルですが、直接的にプログレス表示の制御には関わりません。しかし、Webページ側のJavaScriptがプログレス表示に影響を与える場合、WKUIDelegateのメソッド(例: webView(_:runJavaScriptAlertPanelWithMessage:initiatedByFrame:completionHandler:))内で、プログレス表示の更新やリセットを行うといった高度な制御が必要になるケースも理論上は考えられます。ただし、この問題の主な原因はWebView自体の内部的な挙動にあるため、通常はWKNavigationDelegateとKVOによるestimatedProgressの監視で十分解決できます。
まとめ
本記事では、SwiftにおけるWKWebViewでプログレス表示が二重になってしまう問題の原因と、その解決策について解説しました。
WKWebViewのestimatedProgressの挙動と、開発者自身によるプログレス表示実装の組み合わせが二重表示の原因となることを理解しました。WKNavigationDelegateのdidStartProvisionalNavigation、didFinish、didFailProvisionalNavigationメソッドと、KVOによるestimatedProgressの監視を組み合わせることで、プログレス表示のライフサイクルを適切に管理する方法を学びました。- 具体的には、読み込み開始時にプログレス表示をリセット・表示し、読み込み完了または失敗時にプログレス表示を非表示にする、というフローを実装しました。
この記事を通して、WKWebViewのプログレス表示をより一層洗練させ、ユーザーにストレスのないWebコンテンツ表示を提供できるようになるでしょう。今後は、Webコンテンツの表示パフォーマンス改善や、より高度なJavaScript連携についても記事にする予定です。
参考資料
- WKWebView - Apple Developer Documentation
- WKNavigationDelegate - Apple Developer Documentation
- iOS WKWebView Progress Bar Example (※上記コードはAppCodaの記事などを参考に、より汎用的に改良しています)
