はじめに
iOS 14からアプリをデフォルトのウェブブラウザとして実装/配布することができます。
ただし、デフォルトのウェブブラウザとして動作させるには特別なEntitlementが必要で、Appleに申請して付与してもらう必要があります。
実装手順については公式のドキュメントがあります。
が、具体的なソースコードベースでの実装例が載っているわけではないため、要件を読解して実装に落とし込む必要がありました。具体的な実装例として参考になるのはOSSである Firefox for iOS なのですが、デフォルトブラウザとしての要件以外の実装が膨大すぎて真に求める最小限の実装がなんなのか調査するのにかなり苦労しました。ということで、時々モチベーションが高まったらちょっと進めるという方針でコツコツやってたら2年かかってしまいました。
おそらく日本語で実装方法について解説している文献はないのと思うので、具体的な実装例を踏まえながら作り方をまとめようと思います。
実装例
ウェブブラウザと連携するアプリを開発する上で一般的なブラウザの挙動を確認するために作った最小限機能を搭載したブラウザアプリです。
宣伝ポイント
WebViewについては cybozu/WebUI を利用しており、SwiftUIライクのインターフェースで実装しています。アーキテクチャについては LUCA を用いており、実践的なiOSアプリの教科書になるような実装を目指して開発しています。
配布までの流れ
- 要件を満たしたアプリを実装しAppStoreでリリースする
- AppleにEntitlementを付与してもらう
- Entitlementを含めたプロビジョニングプロファイルに更新してアップデートをリリースする
要件ごとの実装方法
1. HTTP
およびHTTPS
スキームをInfo.plist
に指定する
晴れてデフォルトブラウザになった際にhttp://
とhttps://
から始まるURLを onOpenURL(perform:) で受け取れるようにするために必要です。つまり、デフォルトブラウザのEntitlementが付与されるまでは、実際に動作確認することができないけど必要な要件です。
plist version="1.0">
dict>
key>CFBundleURLTypeskey>
array>
dict>
key>CFBundleTypeRolekey>
string>Editorstring>
key>CFBundleURLNamekey>
string>$(PRODUCT_BUNDLE_IDENTIFIER)string>
key>CFBundleURLSchemeskey>
array>
string>httpstring>
string>httpsstring>
array>
dict>
array>
dict>
plist>
2. UIWebView
を使用してはダメ
そのままです。代わりにWKWebView
を使いましょう。
SwiftUIベースのアプリでiOS 26以上のサポートで良いなら公式のWebView
とWebPage
が使えます(WebKit for SwiftUI)。iOS 26未満のサポートが必要ならUIViewRepresentable
でWKWebView
をラップして使う必要がありますが、メモリリークや無限ループしないように作るのは結構難しいので cybozu/WebUI を使うのがおすすめです。
3. アプリの起動後に提供すべき必須機能
URLを入力するためのテキストフィールド
なんといっても、まずはWebサイトにアクセスする手段として、URLのフルパスを入力できるテキストフィールドを実装する必要があります。普通にTextField
で自作でOKです。
var body: some View {
TextField(text: $store.inputText) {
Text("Search…")
}
.keyboardType(.webSearch)
.disableAutocorrection(true)
.textInputAutocapitalization(.never)
.onSubmit {
}
}
いくつか付けておいた方が良いModifierがあります。
-
keyboardType(.webSearch)
:Web検索用のキーボードが表示される -
disableAutocorrection(true)
:ユーザーの入力を勝手に変更しない方が良いかも -
textInputAutocapitalization(.never)
:同上
インターネット上の関連リンクを見つけるための検索ツールまたはブックマークの厳選リスト
URLのフルパスを入力しないとネットサーフィンできないようでは不便ですよね。そのため、手頃にWebサイトへアクセスできる手段の提供が必要です。
私は上記のURLを入力するためのテキストフィールド
をみてURLっぽくない時は自然言語での検索とみなして、ユーザーが設定した検索エンジンでキーワード検索させるようにしています。
func search(with text: String) {
let url: URL? = if text.isEmpty {
URL(string: "https://www.google.com")
} else if let url = URLComponents(string: text)?.url {
url
} else {
URLComponents(string: "https://www.google.com/search?q=\(text)")?.url
}
if let url {
webView.load(URLRequest(url: url))
}
}
ブックマーク機能がある場合、デフォルトで一般的なサイトをいくつか登録しておくという方法でも良いようです。AppleのトップページとかGoogleのトップページとかが無難かもしれません。
4. HTTP
やHTTPS
スキームのURLをハンドリングする
1でも書きましたが、デフォルトブラウザのEntitlementが付与されて初めて機能する実装を予めしておく必要があります。これに気づくのに1年かかりました(lol)。
指定された宛先に直接移動し、期待されるウェブコンテンツをレンダリングする
Info.plist
にhttp
とhttps
のスキームが追加されていることが前提ですが、アプリに渡ってきたWeb URLをonOpenURL
などでハンドリングして、直接Webサイトがロードされるようにする必要があります。
func openURL(url: URL) {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
return
}
switch components.scheme {
case "http", "https":
webView.load(URLRequest(url: url))
case "カスタムスキーム":
return
case .some, .none:
return
}
}
Entitlementが付与されるまでは、上記の実装だと本当にちゃんとWebサイトのロードがハンドリングされるか確認が取れません。そのため、適当なカスタムスキーム(例:my-https
)をInfo.plist
に追加して、アドレス帳のURL欄にmy-https://example.com
などを貼っておき、アプリではmy-
を抜いてハンドリングするようにして動作確認を行います。
なお、予期しないページにリダイレクトしたり、指定されていないコンテンツをレンダリングするような実装だとリジェクトされます。
ペアレンタルコントロールまたはロックダウンモードに準拠するならちゃんとやる
これは任意機能要件なのでやっていません。ペアレンタルコントロールやロックダウンモードを提供するなら、ちゃんとナビゲーションを制限するように実装してないとリジェクトされるんだと思います。
フィッシングなどの問題が疑われるコンテンツに対して警告を表示する
これも任意機能要件なのでやっていません。危ないサイト(それこそHTTPのサイトとかは対象かも)にアクセスしようとしたら警告を表示する機能があるとベターだよってことだと思います。
サインインフローを提供するサイトに対してネイティブの認証UIを提供する
これも任意機能要件なのでやっていません。Webページで認証が完結する場合でも、フックしてネイティブの認証UIを提供した方がベターだよってことでしょう。ただ、サイトによって必要な認証UIが異なる気もするので、具体的にどう提供すべきなのかは未研究です。Firefoxの実装をよく見たら何かわかるかもしれません。
5. ブラウザの制限に従う
特定のドメインに対するユニバーサルリンクへの応答はできない
例えば、https://hoge.com/...
というURLをSafariなど別のアプリで開こうとした時に、自分のブラウザアプリで開くように主張することはできないという制限を受けます。
具体的には、𝕏がインストールされている端末上でhttps://x.com/Kyomesuke
をSafariで開くとユニバーサルリンクとしてハンドルされて「𝕏で開きますか?」みたいなバナーが出ると思いますが、そういう行為はブラウザアプリでは許されません。
逆に、ブラウザアプリ内で別アプリのユニバーサルリンクをロードした時は普通に別アプリを開けるっぽいです。
個人データへの不要なアクセスを避ける
Info.plist
に以下のDescriptionキーがある場合はリジェクトされます。
NSPhotoLibraryUsageDescription
NSLocationAlwaysUsageDescription
NSLocationAlwaysAndWhenInUseUsageDescription
NSLocationAlwaysUsageDescription
NSLocationAlwaysAndWhenInUseUsageDescription
NSHomeKitUsageDescription
NSBluetoothAlwaysUsageDescription
NSHealthShareUsageDescription
NSHealthUpdateUsageDescription
いくつかは代替手段が提示されているので、そちらの利用で収まる範囲の機能実装を検討してください。
AppleにEntitlement付与のリクエストをする
1年前くらいまではメール([email protected]
)でリクエストを送る形式だったのですが、EU圏からの要請でWebKit以外の代替ブラウザエンジンを搭載したブラウザの提供を可能とする取り組みに伴い専用のフォームが設置されました。
必要事項の入力と、要件を満たしてることのチェックリストを埋めればOKです。1日2日くらいでメールで結果が来ます。
Entitlementを含めたプロビジョニングプロファイルに更新する
晴れてEntitlementを付与されたらアプリに反映させます。
Apple Developerの「証明書、ID、プロファイル」のページでIdentifiers
のタブを開き、指定したBundle IDの詳細を開くとEntitlementが付与されていることが確認できます。
そしたら、Capabilitiesのタブを開いてDefault Web Browser
にチェックを入れて保存します。
Profilesタブを開き、該当アプリのプロビジョニングプロファイルを更新します。
すると、Enabled Capabilities
にDefault Web Browser
が含まれるようになると思います。
ここまでできたら、更新されたプロビジョニングプロファイルをXcodeのプロジェクトに反映させるのですが、どうやったら確実に反映させられるのかよくわかりません。Xcodeの設定でアカウントのプロファイルの読み込み直しをさせてみたり、Signing & Capabilities
のAutomatically manage signing
をON/OFFしたり、Teamを選び直したり、Xcodeを再起動したり、Macを再起動したり、無効になっていたDistribution CertificateをKeyChainから削除したり色々やっていたらいつの間にか反映されていました。(誰か確実にプロビジョニングプロファイルの更新を反映させる方法教えてください。)
閑話休題
デフォルトブラウザの申請が通ったことを𝕏で投稿したら予想に反してたくさんいいねされました。なぜバズったのかわかりません。そんなにデフォルトブラウザってみんな意識してるものなんですか?
おわりに
今回は実装要件と手順に焦点を当てて解説しましたが、ちゃんとしたブラウザアプリを作るという面では実装における要点がもっと沢山あります。追々紹介できたらいいかなと思います。
審査を通すには根気が要りますが、達成の喜びはひとしおです。
興味が湧いたら挑戦してみてはいかがでしょうか?
関連記事
Views: 0