LINEヤフー Tech Blog

LINEヤフー株式会社のサービスを支える、技術・開発文化を発信しています。

LINE iOSアプリにWebKitの新API「WebPage」を導入できず、自前で実装した件

はじめに

こんにちは、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とは

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アプリにそのまま導入することはできませんでした。

1. 対応OSバージョンの問題

WebPage はiOS 26から利用可能です。一方で、LINE iOSは現在もiOS 18をサポートしているため、プロダクションコードに直接組み込むことはできません。

2. UIKit前提の既存実装との相性

WebPage は実装内部で WKWebView を持っており、 WebView(WebKitが提供するSwiftUI View)内部でそれを表示する仕組みをとっています。しかし、その WKWebView はプロパティとして公開されておらず、アプリ側でそれを取り出して使うことはできません。

既存のアプリ内ブラウザでは、 WKNavigationDelegate やKVOなど、 WKWebView に関わる実装が多く、品質保証の観点から、一度にすべてを置き換えることは現実的ではありません。WKWebView を使い続けたまま段階的に移行する必要があったため、 WebPage を直接導入するには大きな障壁となりました。

3. アーキテクチャとの不整合

前述のとおり、私たちはUI・プレゼンテーション・ビジネスロジック・インフラの4層で責務を分離する構成を採用しています。この構成では、ビジネスロジックやプレゼンテーション層がUI層の具体型に直接依存しないことが重要です。

しかし、ビジネスロジックやプレゼンテーション層で import WebKit すると、WKWebView などのUI関連型も利用可能になってしまいます。WebPage 自体は抽象度の高いAPIですが、最終的にはWebKitモジュールに依存するため、層の境界を型レベルで強制するという私たちの方針とは完全には整合しませんでした。

4. テスト戦略との相性

LINE iOSのような大規模アプリでは、UIに依存しないロジックをテスト可能な形に保つことが重要です。一方で WebPage は、差し替えやモック化を前提としたAPI設計ではなく、既存の依存性注入(DI)ベースのテスト戦略に素直に組み込むことが難しい構造でした。

5. 既存の機能要件を満たしきれない

LINE iOSのアプリ内ブラウザには、いくつか独自の機能要件があります。

例えば、URL変化を確実に検知して履歴に保存する必要があります。SwiftUIの onChange(UIの描画サイクルに依存)やObservation Frameworkの Observations(トランザクション境界に依存)による検知では、短時間に発生した複数の変化がまとめられてしまう可能性があります。従来のUIKit実装ではKVOを利用してより低レベルで変化を検知していましたが、現時点で WebPage には同等のフックが十分に用意されていません。

また、JavaScriptの window.open のような、本来新しいタブを開く操作をハンドリングし、同じ画面内で開く必要があります。しかし、現状の WebPage には、この挙動を実現するための仕組みは用意されていません。

結果として、既存の要件を満たしきれないという判断になりました。

WebPageを参考に、自前の実装を設計する

現行のアプリ要件を満たしながら責務の分離を実現するため、WebKit本家の WebPage の思想を取り入れつつ、LINE iOS向けに独自のAPIを設計しました。

WebPageRepresentableによる抽象化

まず、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独自の型を隠蔽し、外部には必要な責務だけを公開することができました。

イベントハンドリング専用クラスの作成

従来は、UIViewControllerUIView を拡張し、各種デリゲートに準拠させる実装がよく用いられます。ただこの方針は、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をそのまま採用できないことも少なくありません。それでも、モダンで整理された設計からエッセンスを抽出し、自分たちの文脈に合わせて再構築することで、将来の移行を見据えた基盤を作ることができます。

これからも、スムーズに機能開発を進められるアーキテクチャを目指して改善を続けていきたいと思います。