背景
现有架构的问题
工作中的 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 中可以自然地订阅状态变化:
@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 WebKit,WKWebView 等 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:
@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:
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: 触发视图更新
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 官方实现,在内部持有委托适配器:
业务逻辑层:
@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()
}
// ...
}
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 特有类型,只向外部公开必要的职责。
事件处理专用类
传统做法常扩展 UIViewController 或 UIView 来遵循各种委托,但这容易导致 ViewController 臃肿,导航和安全判断与 UI 紧密耦合。即使改为扩展 ViewModel,也只是 ViewModel 的扩展,职责边界仍然模糊。
因此参考 WebKit 官方实现,创建了专门处理导航相关事件并操作 ViewModel 的类:
@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
目前实现封闭在应用内浏览器功能模块内,未来计划:
- 提取为独立 Package:使用
package访问修饰符分离 UI 和非 UI(逻辑)的库结构 - 长期 SwiftUI 化:逐步迁移到 SwiftUI 基础实现
总结
WebKit 作为 Apple 官方开源库,罕见地体现了现代设计理念:
- SwiftUI 优先的设计
- Observation 支持
- 积极隐藏 legacy API
即使由于产品限制无法直接采用最新 API,也可以从中提取设计精髓,根据自研上下文重新构建,为未来的迁移打下基础。
flowchart TB
subgraph 核心收获
A[WebPage 设计理念]
B[职责分离架构]
C[现代 Swift 特性应用]
D[可测试性保障]
end
A --> B --> C --> D
通过这种方式,既能满足当前版本兼容性要求,又能为未来向官方 API 迁移做好准备。