认识 WebKit for SwiftUI
Meet WebKit for SwiftUI
2025年6月9日
一句话判断
如果你之前在 SwiftUI 里用 UIViewRepresentable 包装 WKWebView,现在可以把那坨胶水代码删掉了——WebKit for SwiftUI 的原生 API 把 web 内容集成简化到了”传个 URL 就行”的程度。
这场 Session 讲了什么
WebKit 团队发布了一套完整的 SwiftUI 原生 API,核心是两个类型:WebView 和 WebPage。WebView 是一个 SwiftUI 视图,传入 URL 就能直接显示 web 内容,支持所有 WebKit 平台。WebPage 是一个 Observable 类,负责加载、控制和与 web 内容通信,可以独立使用也可以和 WebView 组合。
几个关键能力:自定义 URL scheme handler(URLSchemeHandler 协议)让你可以直接从 app bundle 加载本地资源,不需要再走 file:// 协议;callJavaScript API 可以直接在 SwiftUI 中执行 JavaScript 并获取返回值;WebPage.NavigationDeciding 协议让你控制导航策略;新增 webViewScrollPosition 和 onScrollGeometryChange modifier 实现滚动位置同步。配合 Swift 6.2 的 Observations API,可以优雅地用 for-await 循环监听导航事件变化。
值得深挖的点
URLSchemeHandler 的设计哲学
传统做法中,如果要在 WKWebView 里加载本地资源,要么用 file:// URL,要么实现复杂的 WKURLSchemeHandler 并和 UIKit 桥接。新的 URLSchemeHandler 协议直接返回 AsyncSequence<URLSchemeTaskResult>,这和 Swift 并发模型完全对齐。你可以先 yield 一个 URLResponse,然后逐步 yield Data 块——这就天然支持了流式响应,不需要手动管理 completion handler 的生命周期。
更重要的是,当 URL scheme task 被取消时(比如用户点了返回),函数内的 Task 会自动取消。这解决了老 API 中常见的内存泄漏问题——以前你得手动在 urlSchemeTaskDidStop 里清理资源。用 Swift 结构化并发的模型,取消传播是自动的。
NavigationDecider 与 SwiftUI 环境的结合
WebPage.NavigationDeciding 协议把导航决策分成了三个阶段:navigation action(决定是否允许导航)、response(收到响应后决定)、authentication(认证挑战)。Session 中演示的外部链接拦截是一个典型场景:在 action 阶段判断 URL 的 scheme 和 host,内部链接允许继续,外部链接取消并设置一个状态变量,然后通过 SwiftUI 的 openURL 环境值在系统浏览器中打开。
这个模式的巧妙之处在于:决策逻辑完全在 decider 类中,副作用(打开外部 URL)通过 @Observable 的状态驱动 SwiftUI 视图更新,避免了传统的 delegate 回调地狱。
代码片段
最简单的 WebView 用法,传入 URL 即可:
WebView(url: URL(string: "https://example.com")!)
.ignoresSafeArea()
使用 WebPage 加载内容并监听标题变化:
@Observable
class ArticleViewModel {
let page = WebPage()
var title: String { page.title ?? "" }
func load(_ url: URL) {
page.load(URLRequest(url: url))
}
}
// SwiftUI 中
let model = ArticleViewModel()
WebView(model.page)
.navigationTitle(model.title)
实现自定义 URL scheme handler,从 bundle 加载本地 HTML:
struct LocalHandler: URLSchemeHandler {
func reply(for request: URLRequest) async throws -> AsyncSequence<URLSchemeTaskResult> {
let data = try Data(contentsOf: bundleURL(for: request.url!))
return [
.response(URLResponse(url: request.url!, mimeType: "text/html", expectedContentLength: data.count)),
.data(data)
]
}
}
let config = WebPage.Configuration()
config.urlSchemeHandlers[URLScheme("lakes")!] = LocalHandler()
最佳实践
建议新的 SwiftUI 项目直接使用 WebKit for SwiftUI API,不要再引入 UIViewRepresentable 桥接。如果你的 app 同时支持 iOS 和 macOS,这套 API 是跨平台的,不需要写平台判断。
优先使用 WebPage 而不是裸 WebView,即使你现在不需要监听属性变化。WebPage 的 Observable 设计意味着后续添加导航监听、JavaScript 通信等功能时不需要重构。
避免在 URLSchemeHandler 中加载大文件。虽然它支持流式响应,但对于超过几 MB 的资源,考虑使用 HTTP 服务器或预加载到临时目录。
还有什么值得关注
findNavigatormodifier 直接支持 WebView 的页面内搜索,iOS 上显示在键盘区域,macOS 上显示在视图顶部。- visionOS 新增
lookToScroll支持,通过webViewScrollInputBehaviormodifier 一行代码启用。 - WebPage 暴露了 estimated progress、theme color、当前 URL 等属性,全部是 Observable 的,可以直接绑定到 SwiftUI 视图。