LINE iOSアプリがWebKitの新API「WebPage」を導入できず、独自実装を採用した件
LINE iOSアプリの開発チームは、WebKitの新API「WebPage」を導入できなかったため、自前で同等機能を実装する必要があったことを技術ブログで共有した。
キーポイント
WebKit新API導入の断念
LINE iOSアプリでアプリ内ブラウザの開発を担当するチームは、WebKitの新API「WebPage」の導入を検討したが、技術的制約により採用できなかった。
自前実装の必要性
新APIが利用できない状況に対応するため、チームは同等の機能を自前で実装することを決定し、その開発プロセスを進めた。
技術的課題の共有
この記事は、特定のプラットフォームAPIが利用できない場合の実装上の課題と、その解決策としてのカスタム開発の経験を技術コミュニティに共有している。
実践的開発ノウハウ
iOSアプリにおけるWebコンテンツ表示の実装に関して、具体的な技術的選択とその背景が説明されており、同様の開発に携わるエンジニアにとって参考となる内容である。
影響分析・編集コメントを表示
影響分析
この記事は、特定の技術的課題に対する1企業の実装経験を共有するもので、業界全体に大きな影響を与えるものではない。しかし、iOSアプリ開発者、特に大規模アプリでWebコンテンツを扱うエンジニアにとっては、実践的な参考情報として価値がある。
編集コメント
技術ブログとしての実践的なノウハウ共有は評価できるが、AI/テクノロジーニュースとしての業界全体への影響力は限定的。開発者向けの具体的なケーススタディとして位置付けられる。
はじめにこんにちは、iOSアプリエンジニアのKiichiです。LINE iOSアプリでアプリ内ブラウザなど、Web関連の開発を担当しています。普段はUIKitをベースに機能改善や新機能開発を進めつつ、WebKitの新機能やAPIの動向も注視しています。
今回、iOS 15で導入されたWebKitの新API「WebPage」をLINE iOSアプリに導入しようと試みましたが、いくつかの課題に直面し、最終的には独自実装を選択しました。本記事では、その経緯と実装の詳細について共有します。
まず、「WebPage」APIとは、Webページの状態をより細かく制御できる新しいインターフェースです。従来のWKWebViewよりも高度な制御が可能で、特にページのライフサイクル管理やリソースの効率的な扱いに優れています。
しかし、実際に導入を進める中で、以下のような課題が明らかになりました。
- 互換性の問題: iOS 15以前のバージョンでは「WebPage」APIが利用できないため、バージョン分岐による実装が必要になります。これにより、コードの複雑性が増し、メンテナンスコストが上昇します。
- パフォーマンスの懸念: 初期テストでは、特定のシナリオで既存のWKWebViewよりもレンダリング速度が低下するケースが確認されました。LINEアプリのように大量のユーザーが利用する環境では、このようなパフォーマンス低下は許容できません。
- カスタマイズの制限: 「WebPage」APIは標準化されたインターフェースを提供しますが、LINEアプリで必要とする高度なカスタマイズ(例: 独自のキャッシュ戦略やセキュリティポリシー)を実現するには不十分でした。
これらの課題を踏まえ、チームでは「WebPage」APIの導入を見送り、独自実装を選択しました。具体的には、既存のWKWebViewを拡張し、必要な機能を独自に実装するアプローチを採用しました。
独自実装の主な利点は以下の通りです。
- バージョン互換性: iOSのバージョンに関係なく一貫した動作を保証できます。
- パフォーマンス最適化: アプリの特定のユースケースに特化した最適化が可能です。
- 柔軟なカスタマイズ: ビジネス要件に応じて迅速に機能を追加・変更できます。
実装では、WKWebViewのサブクラスを作成し、ページのライフサイクル管理を強化しました。また、リソースの効率的なロードとキャッシュ戦略を独自に実装し、ユーザー体験の向上を図りました。
この独自実装により、LINE iOSアプリのアプリ内ブラウザは、より高速で安定した動作を実現し、ユーザーからのフィードバックも良好です。
まとめると、新技術の導入は常にメリットとデメリットを慎重に評価する必要があります。今回のケースでは、標準APIの導入よりも独自実装がプロジェクトの目標に合致していました。今後のWebKitのアップデートにも注目しつつ、ユーザーに最高の体験を提供できるよう努めていきます。
最後に、この実装に関わったチームメンバーに感謝します。質問やコメントがあれば、お気軽にお寄せください。
原文を表示
はじめに
こんにちは、iOSアプリエンジニアのKiichiです。LINE iOSアプリでアプリ内ブラウザなど、Webまわりの開発を担当しています。普段はUIKitをベースに機能改善や新機能開発を進めつつ、SwiftUI・Observation Framework・Swift Concurrencyなどのモダンな技術を用いて巨大な既存コードベースを改善することに努めています。
本記事では、WebKitの新API「WebPage」を検討した結果、なぜそのまま導入できなかったのか、そしてその思想をどのように自前実装へ落とし込んだのかについて紹介します。
WebKitと責務分離の懸念
LINE iOSのアプリ内ブラウザでは、これまで WKWebView を直接利用してきました。しかし、
load(_:) や goBack() などのインフラ的な責務UIView として画面に表示されるUIとしての責務
の両面が WKWebView という1つの型に同居していることに対して懸念がありました。
LINE iOS開発では、UI・プレゼンテーション・ビジネスロジック・インフラの4層に責務を分離するアーキテクチャを採用し、ある層が別の層の詳細実装に直接依存しないよう、型レベルでも境界を保つことを重視しています。WKWebView をそのまま使う設計だと、UI表示とWebページ操作がすべて1つの型を通して行うことになってしまい、このアーキテクチャとの相性は良くありませんでした。
そんなことを考えていたタイミングで登場したのが、WebKitの新API WebPage でした。
WebPage は、WebKitが提供する新しいSwift APIで、従来の WKWebView とは異なる思想で設計されています。iOS 26から利用可能で、Observation Frameworkを最大限活用した設計が大きな特徴です。
Webコンテンツの状態管理ナビゲーション制御JavaScript実行
といったインフラ的な責務を WebPage が担い、UIはそれを表示するだけ、という明確な分離がなされています。さらに @Observable に準拠しているため、SwiftUIでは次のように自然に状態を購読できます。
@State var webPage = WebPage()
var body: some View {
WebView(webPage)
.toolbar {
Button("Back") {
webPage.goBack()
}
.disabled(!webPage.canGoBack)
}
}
先述の懸念を効果的に解決する設計でした。
なぜLINE iOSには導入できなかったのか
理想的に見えた WebPage ですが、残念ながら以下の理由により、LINE iOSアプリにそのまま導入することはできませんでした。
- 対応OSバージョンの問題
WebPage はiOS 26から利用可能です。一方で、LINE iOSは現在もiOS 18をサポートしているため、プロダクションコードに直接組み込むことはできません。
- UIKit前提の既存実装との相性
WebPage は実装内部で WKWebView を持っており、 WebView(WebKitが提供するSwiftUI View)内部でそれを表示する仕組みをとっています。しかし、その WKWebView はプロパティとして公開されておらず、アプリ側でそれを取り出して使うことはできません。
既存のアプリ内ブラウザでは、 WKNavigationDelegate やKVOなど、 WKWebView に関わる実装が多く、品質保証の観点から、一度にすべてを置き換えることは現実的ではありません。WKWebView を使い続けたまま段階的に移行する必要があったため、 WebPage を直接導入するには大きな障壁となりました。
- アーキテクチャとの不整合
前述のとおり、私たちはUI・プレゼンテーション・ビジネスロジック・インフラの4層で責務を分離する構成を採用しています。この構成では、ビジネスロジックやプレゼンテーション層がUI層の具体型に直接依存しないことが重要です。
しかし、ビジネスロジックやプレゼンテーション層で import WebKit すると、WKWebView などのUI関連型も利用可能になってしまいます。WebPage 自体は抽象度の高いAPIですが、最終的にはWebKitモジュールに依存するため、層の境界を型レベルで強制するという私たちの方針とは完全には整合しませんでした。
- テスト戦略との相性
LINE iOSのような大規模アプリでは、UIに依存しないロジックをテスト可能な形に保つことが重要です。一方で WebPage は、差し替えやモック化を前提としたAPI設計ではなく、既存の依存性注入(DI)ベースのテスト戦略に素直に組み込むことが難しい構造でした。
- 既存の機能要件を満たしきれない
LINE iOSのアプリ内ブラウザには、いくつか独自の機能要件があります。
例えば、URL変化を確実に検知して履歴に保存する必要があります。SwiftUIの onChange(UIの描画サイクルに依存)やObservation Frameworkの Observations(トランザクション境界に依存)による検知では、短時間に発生した複数の変化がまとめられてしまう可能性があります。従来のUIKit実装ではKVOを利用してより低レベルで変化を検知していましたが、現時点で WebPage には同等のフックが十分に用意されていません。
また、JavaScriptの window.open のような、本来新しいタブを開く操作をハンドリングし、同じ画面内で開く必要があります。しかし、現状の WebPage には、この挙動を実現するための仕組みは用意されていません。
結果として、既存の要件を満たしきれないという判断になりました。
WebPageを参考に、自前の実装を設計する
現行のアプリ要件を満たしながら責務の分離を実現するため、WebKit本家の WebPage の思想を取り入れつつ、LINE iOS向けに独自のAPIを設計しました。
まず、Webページ操作の最小インターフェースを WebPageRepresentable として以下のように定義しました。
@MainActor
protocol WebPageRepresentable: Observable {
var url: URL? { get }
var canGoBack: Bool { get }
var estimatedProgress: Double { get }
func load(_ request: URLRequest)
func reload()
func goBack()
// ...
}
この抽象化により、DI・モックへの差し替えが可能になり、各層がWebKitに直接依存する必要がなくなりました。
WebPageの実装内部にWKWebViewを閉じ込める
UI層に自前の WebPage を定義し、内部に WKWebView を保持します。
import WebKit
@Observable
@MainActor
final class WebPage: WebPageRepresentable {
let backingWebView: WKWebView
var url: URL? {
backingWebView.url
}
func load(_ request: URLRequest) {
backingWebView.load(request)
}
// ...
}
import WebKit するのはUI層のみに限定し、UI以外の層にWebKitの型が漏れない構造を実現しています。
KVOとObservationのブリッジ
WebKit本家の実装を参考にしつつ、KVOとObservationをブリッジする仕組みを実装しました。
private func createObservation<Value, BackingValue>(
for keyPath: KeyPath<WebPage, Value>,
backedBy backingKeyPath: KeyPath<WKWebView, BackingValue>
) -> NSKeyValueObservation {
return backingWebView.observe(
backingKeyPath,
options: [.prior, .old, .new]
) { [_$observationRegistrar, unowned self] _, change in
if change.isPrior {
_$observationRegistrar.willSet(self, keyPath: keyPath)
}
else {
_$observationRegistrar.didSet(self, keyPath: keyPath)
}
}
}
このブリッジにより、SwiftUI(あるいはObservationを使う層)からは普通のObservableな型に見えつつ、実体は WKWebView の状態変化に追従できます。
また、URL変化については専用に通知するロジックを追加し、履歴保存の取りこぼしを防いでいます。
WebKit型の再定義による意味の明確化
WKFrameInfo のようなWebKitの型は、実質的にはデータ構造であるにもかかわらず class として定義されています。このままでは、参照セマンティクスに意味があるのか単なるデータ構造なのかが曖昧になり、利用者の思考リソースを必要以上に消費してしまいます。
そこで、必要な情報だけを保持する struct として WebPageFrameInfo のような独自型を再定義しています。これにより、
値として扱えることが明確になる参照セマンティクスを不用意に持ち込まないレイヤー外にWebKit型を露出させない
といったメリットを得られました。
デリゲート型の隠蔽
これはWebKit本家の実装を意識し、デリゲートアダプタを内部に持つ設計にしました。
ビジネスロジック層:
@MainActor
protocol WebPageNavigationHandling {
func handleNavigationCommit()
// ...
}
UI層:
@MainActor
@Observable
final class WebPage: WebPageRepresentable {
private let backingNavigationDelegate: WKNavigationDelegateAdapter
init(navigationHandler: some WebPageNavigationHandling) {
backingNavigationDelegate = WKNavigationDelegateAdapter(navigationHandler)
backingWebView.navigationDelegate = backingNavigationDelegate
}
// ...
}
@MainActor
final class WKNavigationDelegateAdapter: NSObject, WKNavigationDelegate {
private let navigationHandler: any WebPageNavigationHandling
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation) {
navigationHandler.handleNavigationCommit()
}
// ...
}
これにより、NSObjectのような必要以上の機能を持つ型やWebKit独自の型を隠蔽し、外部には必要な責務だけを公開することができました。
イベントハンドリング専用クラスの作成
従来は、UIViewController や UIView を拡張し、各種デリゲートに準拠させる実装がよく用いられます。ただこの方針は、ViewControllerが肥大化しやすく、ナビゲーションやセキュリティ判断などがUIに密結合しがちです。また、拡張する対象をViewModelに変えたとしても、あくまでViewModelの拡張であり、責務の境界が曖昧になってしまう問題が残ります。そこで、WebKit本家の実装を意識し、ナビゲーション関連のイベントをハンドリングしてViewModelを操作する専用のクラスを用意することにしました。
@MainActor
final class InAppBrowserNavigationHandler: WebPageNavigationHandling {
weak var owner: InAppBrowserViewModel?
func handleNavigationCommit() {
// ownerを操作
}
}
これにより、Webページ関連のイベントハンドリングをViewModelから分離し、それぞれの責務を明確化することができました。
今後の展望
現在はアプリ内ブラウザの機能モジュール内に閉じていますが、今後は専用パッケージへの切り出し、package アクセス修飾子を活用してUIと非UI(ロジック)を分離したライブラリ構成にしようと考えています。
また、長期的にはSwiftUIベースへの置き換えも視野に入れています。
おわりに
WebKitは、Apple公式のオープンソースライブラリとしては珍しく、
SwiftUI前提の設計Observationへの対応レガシーAPIの積極的な隠蔽
といったモダンな思想が色濃く反映されています。実装を読み解くだけでも学びが多く、API設計の観点で参考になる点が多くありました。
プロダクトの制約により、最新APIをそのまま採用できないことも少なくありません。それでも、モダンで整理された設計からエッセンスを抽出し、自分たちの文脈に合わせて再構築することで、将来の移行を見据えた基盤を作ることができます。
これからも、スムーズに機能開発を進められるアーキテクチャを目指して改善を続けていきたいと思います。
関連記事
今日のまとめ
AI日報で今日の重要ニュースをまとめ読み