WebKit WebPage API 的引入尝试与自研实现

背景

现有架构的问题

工作中的 iOS 应用内浏览器一直使用 WKWebView 直接实现,但存在架构层面的担忧:

  • 基础设施职责load(_:)goBack() 等页面操作
  • UI 职责 :作为 UIView 在屏幕上显示

这两种职责都集中在 WKWebView 这一个类型中。

工作中采用 UI / 表现层 / 业务逻辑 / 基础设施 四层架构,强调各层之间不应直接依赖具体实现。直接使用 WKWebView 会导致 UI 显示和网页操作都通过同一个类型完成,与架构理念不符。

WebPage API 的出现

WebKit 在 iOS 26 推出了新的 Swift API WebPage,设计理念与 WKWebView 截然不同:

职责 承担者
Web 内容状态管理 WebPage
导航控制 WebPage
JavaScript 执行 WebPage
UI 显示 WebView(SwiftUI View)

WebPage 遵循 @Observable,在 SwiftUI 中可以自然地订阅状态变化:

swift 复制代码
@State var webPage = WebPage()

var body: some View {
    WebView(webPage)
        .toolbar {
            Button("Back") {
                webPage.goBack()
            }
            .disabled(!webPage.canGoBack)
        }
}

这种设计有效解决了之前的架构担忧。


为何无法直接引入 WebPage

尽管 WebPage 设计理想,但由于以下原因无法直接在生产环境使用:

1. 操作系统版本限制

flowchart LR subgraph 版本对比 WP[WebPage API<br/>iOS 26+] APP[应用支持<br/>iOS 18+] end WP -->|不兼容| APP

WebPage 需要 iOS 26,而应用目前仍支持 iOS 18,无法直接在生产代码中使用。

2. 与现有 UIKit 实现的兼容性

WebPage 内部持有 WKWebView,但并未将其作为属性公开,应用无法取出使用。

现有浏览器实现大量依赖 WKNavigationDelegate 和 KVO,从质量保证角度,无法一次性全部替换。需要在继续使用 WKWebView 的同时逐步迁移,这成为直接引入 WebPage 的障碍。

3. 与架构的不一致

flowchart TB subgraph 目标架构 UI[UI 层] P[表现层] BL[业务逻辑层] INF[基础设施层] end UI --> P --> BL --> INF style UI fill:#e1f5fe style INF fill:#f3e5f5

四层架构要求业务逻辑层和表现层不直接依赖 UI 层的具体类型。但如果在业务逻辑层 import WebKitWKWebView 等 UI 相关类型也会变得可用。虽然 WebPage 本身是抽象 API,但最终依赖 WebKit 模块,无法在类型层面强制分层边界。

4. 与测试策略的兼容性

大规模应用需要保持 UI 无关逻辑的可测试性。WebPage 并非为替换和模拟而设计,难以融入现有的依赖注入(DI)测试策略。

5. 无法满足现有功能需求

应用内浏览器有一些特殊需求:

URL 变化检测

  • 需要可靠检测 URL 变化并保存历史记录
  • SwiftUI 的 onChange(依赖 UI 渲染周期)或 Observation Framework(依赖事务边界)可能合并短时间内多次变化
  • 传统 UIKit 使用 KVO 在更低层面检测变化
  • WebPage 目前没有提供同等钩子

window.open 处理

  • 需要拦截 JavaScript 的 window.open(本应新开标签页)并在同一页面内打开
  • 当前 WebPage 没有提供实现此行为的机制

自研实现设计

为满足现有应用需求同时实现职责分离,参考 WebKit 官方 WebPage 的设计理念,设计了自定义 API。

核心抽象层

classDiagram class WebPageRepresentable { <<protocol>> +url: URL? +canGoBack: Bool +estimatedProgress: Double +load(request: URLRequest) +reload() +goBack() } class WebPage { <<class>> -backingWebView: WKWebView +url: URL? +canGoBack: Bool +estimatedProgress: Double } class WebPageNavigationHandling { <<protocol>> +handleNavigationCommit() } class InAppBrowserNavigationHandler { -owner: InAppBrowserViewModel? +handleNavigationCommit() } WebPageRepresentable <|.. WebPage WebPageNavigationHandling <|.. InAppBrowserNavigationHandler WebPage --> WKNavigationDelegateAdapter

定义最小化的网页操作接口 WebPageRepresentable

swift 复制代码
@MainActor
protocol WebPageRepresentable: Observable {
    var url: URL? { get }
    var canGoBack: Bool { get }
    var estimatedProgress: Double { get }
    
    func load(_ request: URLRequest)
    func reload()
    func goBack()
    // ...
}

这种抽象实现了:

  • 支持依赖注入和模拟替换
  • 各层无需直接依赖 WebKit

将 WKWebView 封装在实现内部

在 UI 层定义自定义 WebPage,内部持有 WKWebView

swift 复制代码
import WebKit

@Observable
@MainActor
final class WebPage: WebPageRepresentable {
    let backingWebView: WKWebView
    
    var url: URL? {
        backingWebView.url
    }
    
    func load(_ request: URLRequest) {
        backingWebView.load(request)
    }
    // ...
}

关键约束 :只有 UI 层 import WebKit,WebKit 类型不会泄露到其他层。

KVO 与 Observation 的桥接

参考 WebKit 官方实现,构建了 KVO 与 Observation 的桥接机制:

sequenceDiagram participant KVO as WKWebView (KVO) participant Bridge as Observation Bridge participant Obs as Observation Registrar KVO->>Bridge: 属性变化通知 Bridge->>Obs: willSet(keyPath) Bridge->>KVO: 更新值 Bridge->>Obs: didSet(keyPath) Obs->>SwiftUI: 触发视图更新
swift 复制代码
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):

flowchart LR subgraph 类型语义明确化 WK[WKFrameInfo<br/>class - 语义模糊] WP[WebPageFrameInfo<br/>struct - 值语义明确] end WK -->|重定义| WP

收益:

  • 明确可作为值处理
  • 不会意外引入引用语义
  • 不向层外暴露 WebKit 类型

委托类型的隐藏

参考 WebKit 官方实现,在内部持有委托适配器:

业务逻辑层

swift 复制代码
@MainActor
protocol WebPageNavigationHandling {
    func handleNavigationCommit()
    // ...
}

UI 层

swift 复制代码
@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()
    }
    // ...
}
flowchart TB subgraph 委托隐藏 UI[UI 层<br/>WebPage] Adapter[WKNavigationDelegateAdapter<br/>内部类] Handler[WebPageNavigationHandling<br/>协议] VM[业务逻辑层<br/>NavigationHandler] end UI --> Adapter --> Handler VM ..> Handler style Adapter fill:#fff3e0

这样隐藏了 NSObject 等功能过剩的类型和 WebKit 特有类型,只向外部公开必要的职责。

事件处理专用类

传统做法常扩展 UIViewControllerUIView 来遵循各种委托,但这容易导致 ViewController 臃肿,导航和安全判断与 UI 紧密耦合。即使改为扩展 ViewModel,也只是 ViewModel 的扩展,职责边界仍然模糊。

因此参考 WebKit 官方实现,创建了专门处理导航相关事件并操作 ViewModel 的类:

swift 复制代码
@MainActor
final class InAppBrowserNavigationHandler: WebPageNavigationHandling {
    weak var owner: InAppBrowserViewModel?
    
    func handleNavigationCommit() {
        // 操作 owner
    }
}
flowchart LR subgraph 职责分离 VM[InAppBrowserViewModel] Handler[InAppBrowserNavigationHandler] WebP[WebPage] end Handler -->|持有弱引用| VM Handler -->|处理导航事件| WebP VM -->|使用| WebP style Handler fill:#e8f5e9

这样将网页相关事件处理从 ViewModel 中分离,明确了各自的职责。


未来展望

flowchart TB subgraph 演进路线 A[当前: 功能模块内实现] B[中期: 独立 Package] C[长期: SwiftUI 化] end A --> B --> C subgraph Package 结构 P1[UI 模块<br/>public] P2[逻辑模块<br/>package 访问级别] end B --> P1 B --> P2

目前实现封闭在应用内浏览器功能模块内,未来计划:

  1. 提取为独立 Package :使用 package 访问修饰符分离 UI 和非 UI(逻辑)的库结构
  2. 长期 SwiftUI 化:逐步迁移到 SwiftUI 基础实现

总结

WebKit 作为 Apple 官方开源库,罕见地体现了现代设计理念:

  • SwiftUI 优先的设计
  • Observation 支持
  • 积极隐藏 legacy API

即使由于产品限制无法直接采用最新 API,也可以从中提取设计精髓,根据自研上下文重新构建,为未来的迁移打下基础。

flowchart TB subgraph 核心收获 A[WebPage 设计理念] B[职责分离架构] C[现代 Swift 特性应用] D[可测试性保障] end A --> B --> C --> D

通过这种方式,既能满足当前版本兼容性要求,又能为未来向官方 API 迁移做好准备。

相关推荐
健了个平_244 小时前
iOS 27 适配笔记
ios·xcode·wwdc
Tr2e4 小时前
🐱 从 0 到 1:用 Swift 手搓一个 macOS 桌面宠物(附源码)
macos·ios·swift
iOS开发上架哦7 小时前
Jenkins 自动上传 IPA 到 App Store 把发布步骤融入 CI/CD
后端·ios
ZJPRENO8 小时前
2026 苹果 WWDC 完整总结
ios
REDcker9 小时前
WWDC2026系统更新综述
macos·ios·开发者·apple·wwdc·ipados·wwdc2026
星星电灯猴10 小时前
全面解决Charles抓取HTTPS请求响应中文乱码问题的方法与技巧
后端·ios
人月神话-Lee11 小时前
【WWDC】Core AI:iOS 端侧大模型新纪元
人工智能·ios·ai·swift·wwdc·core ai
2501_9160074712 小时前
iOS 开发工具选择指南 从编辑器、编译器到自动化构建
ide·vscode·ios·objective-c·个人开发·swift·敏捷流程
库奇噜啦呼12 小时前
【iOS】源码学习-YYModel源码学习
学习·ios·cocoa
风华圆舞13 小时前
一个 Flutter 项目同时保留 Android、iOS、HarmonyOS 支持的实践
android·flutter·ios