LoginSignup
5

More than 1 year has passed since last update.

iOS向けの最小限機能のブラウザーを開発中🛠

Posted at

Webサービスを先行してリリースした後に後発でモバイル向けアプリをリリースする場合、WebViewベースのアプリ(いわゆるガワネイティブ)となることが結構あると思います。このガワネイティブの実装では、WebView上でのイベントをフックしてネイティブのUIを表示したり、ネイティブ側で特定の処理を実行したりといった要件がありますよね。

また、SwiftUIではまだWebViewが提供されていないためWKWebViewUIViewRepresentableでラップして利用する必要があります。ただ、WebViewのインタラクション(検索・戻る・進む・リロードなど)までラップしているサンプルはGitHubなどを調査してもあまり見当たりません(あったとしても多機能すぎたり実装がよくない)でした。

このような事情から、WebViewの仕様を学び、扱う練習を目的としてブラウザを自作してみることにしました。

要件

MinBrowser

MIT Licenseのオープンソースプロジェクトとして開発しています。
https://github.com/Kyome22/MinBrowser

1-top 2-open-link 4-bookmark 6-share-view

環境

  • Swift 5+
  • iOS 15.0+
  • Xcode 13.4.1+

現状の機能

  • HTTP/HTTPSのリンクを開く
  • キーワード検索(Google/Bing/DuckDuckgo)
  • フルスクリーン(検索バーとツールバーを隠す)
  • Pull to Refresh(スクロールダウンするとリロードするやつ)
  • ブックマーク機能
  • 別アプリからリンクをMinBrowserで開く
  • ライトモード/ダークモード対応
  • 他言語対応(英語/日本語)

実装ポイント

WKWebViewUIViewRepresentableでラップする

  • UIViewRepresentableの中でViewModelを@StateObjectを持つとインタラクションがバグるので、@ObservedObjectで持つようにする。
  • SwiftUI側からのインタラクションを受け取るために、actionをBindingしてupdateUIViewで処理を捌く。ただし、ここで上手に実装しないと半無限ループが起き挙動がかなり怪しいブラウザーになってしまうため注意が必要。
  • UIViewRepresentablestructのため、WebViewのプロパティ変化を監視するならclassであるCoordinatorの中で購読するようにする。
    • Pull to RefreshのためのUIRefreshControlも同様。
  • UserAgentでモバイルと認識させるために、WKNavigationDelegateに準拠してWKWebpagePreferences.preferredContentMode.mobile指定する。
  • カスタムスキームやドメインでの挙動を分岐するために、WKNavigationDelegateに準拠してdecidePolicyFor navigationActionを上手に実装する。
  • JavaScriptのalert confirm promptをネイティブで表示するために、WKUIDelegateに準拠してそれぞれのイベントをフックする。
    • ユーザーの選択や入力結果を待つため、コールバックによる非同期処理を上手に捌く。
    • SwiftUIの.alert()はiOS 16未満ではTextFieldの付きのアラート表示に対応していないので、こちらもUIAlertControllerViewModifier経由で呼び出す実装が必要。

検索バーやツールバーの実装

SwiftUIのNavigationViewのViewModifierである.searchable.toolbarがそのまま使えれば実装が楽だったのですが、検索バーやツールバーのレイアウトやデザインを自由に設定できない(かなり自由度が低い)ため断念して自前実装しました。NavigationViewは独特な仕様があるので意外と使い勝手が悪いです。

ブックマークの実装

本当はハーフモーダルで表示したかったのですが、SwiftUIの.sheetはまだハーフモーダルに対応していないので諦めました。

外部アプリ経由でリンクを開く実装

  • Share Extensionを実装する。
    • SwiftUIをなるべく使いたいなら、デフォルトで用意されているShareViewControllerSLComposeServiceViewControllerを継承せずにUIViewControllerを継承して、内部でUIHostingControllerでSwiftUIのViewを呼び出す。
    • Share Extensionから本体アプリを起動したい場合は黒魔術が必要です。
      Share Extensionから本体アプリを開く
  • SwiftUIの場合は、本体アプリ側でonOpenURL()を使ってカスタムスキームURLを受け取ることができます。
    • クエリ文字列があればそれも取得できるので、開くべきリンクも受け取れます。

最後に

UIViewRepresentableの実装において、UIKitのView側からSwiftUIのViewにイベントを伝達するのは簡単ですが、逆にSwiftUIのViewからUIKitのViewにイベントを送るのがかなり手こずりました。structであるViewのBindingプロパティが更新されると、Viewそのものが作り直されてしまうケースがあり、WebViewの状態を維持しながら表示を更新するのが難しかったです。一度は諦めたのですがちゃんとやり切れてよかったです。

ソースは公開してあるので、WkWebViewをSwiftUIでどうラップするかの参考にしてもらってもいいですし、野良のブラウザーアプリが必要なデバッグに使っていただけると嬉しいです。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5