Meet WebKit for SwiftUI
Safari & Web 入门 1m

认识 WebKit for SwiftUI

Meet WebKit for SwiftUI

2025年6月9日

在 Apple 官方观看视频

一句话判断

如果你之前在 SwiftUI 里用 UIViewRepresentable 包装 WKWebView,现在可以把那坨胶水代码删掉了——WebKit for SwiftUI 的原生 API 把 web 内容集成简化到了”传个 URL 就行”的程度。

这场 Session 讲了什么

WebKit 团队发布了一套完整的 SwiftUI 原生 API,核心是两个类型:WebViewWebPageWebView 是一个 SwiftUI 视图,传入 URL 就能直接显示 web 内容,支持所有 WebKit 平台。WebPage 是一个 Observable 类,负责加载、控制和与 web 内容通信,可以独立使用也可以和 WebView 组合。

几个关键能力:自定义 URL scheme handler(URLSchemeHandler 协议)让你可以直接从 app bundle 加载本地资源,不需要再走 file:// 协议;callJavaScript API 可以直接在 SwiftUI 中执行 JavaScript 并获取返回值;WebPage.NavigationDeciding 协议让你控制导航策略;新增 webViewScrollPositiononScrollGeometryChange 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 结构化并发的模型,取消传播是自动的。

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 服务器或预加载到临时目录。

还有什么值得关注

  • findNavigator modifier 直接支持 WebView 的页面内搜索,iOS 上显示在键盘区域,macOS 上显示在视图顶部。
  • visionOS 新增 lookToScroll 支持,通过 webViewScrollInputBehavior modifier 一行代码启用。
  • WebPage 暴露了 estimated progress、theme color、当前 URL 等属性,全部是 Observable 的,可以直接绑定到 SwiftUI 视图。
Safari与Web SwiftUI 应用服务