WWDC25 让 WebKit 获得了面向 SwiftUI 的 WebViewWebPage。这些接口来自 Xcode 26 与 iOS 26 的测试周期,名称、行为和可用性都应在正式交付前重新核对。它们的价值不是把网页伪装成原生页面,而是让网页状态真正参与 SwiftUI 的数据流。

先分清视图与模型

WebView 负责渲染以及用户交互,WebPage 则保存可观察的页面状态和导航能力。把二者分开后,地址、标题、加载进度和前进后退状态不必再通过 UIKit 包装层手动转发。一个页面可以把 WebPage 作为长期状态保存,再让 WebView 消费它:

import SwiftUI
import WebKit

struct ArticleBrowser: View {
    @State private var page = WebPage()

    var body: some View {
        WebView(page)
            .task {
                guard let url = URL(string: "https://example.com/guide") else { return }
                for await event in page.load(URLRequest(url: url)) {
                    if case .finished = event { break }
                }
            }
    }
}

示例只表达测试版中的结构,不应被当作最终 SDK 的固定签名。真正的页面还要处理加载失败、任务取消和重复加载。视图重建不等于浏览会话也应该重建,所以不要在 body 中临时创建页面模型,也不要让多个 .task 无条件加载同一个请求。

把导航策略写成边界

内嵌浏览器最容易失控的地方不是布局,而是“哪些链接可以留在应用里”。帮助中心也许允许同域导航,登录、支付或文件下载却需要交给系统浏览器或专用流程。把允许的主机、协议和路径定义为纯函数,再由导航决策入口调用。这样策略可以单元测试,也不会散落在多个按钮和代理回调里。

对外部链接应明确显示即将离开应用;对自定义 URL scheme 先验证来源和参数;对 javascript:、本地文件以及未知协议默认拒绝。网页传来的标题、查询参数和脚本消息都是不可信输入,不能直接拼成命令、文件路径或深层链接。若产品不需要脚本桥接,就不要为了“以后可能用到”而开放它。

原生外壳只承载真正的原生能力

工具栏适合放返回、前进、刷新、分享和关闭操作,但它们应读取同一个 WebPage 状态,而不是维护第二份布尔值。加载进度可以成为轻量提示,失败状态则需要提供重试和打开外部浏览器的选择。网络断开时保留已经显示的内容,通常比立即用空白错误页替换更友好。

网页本身仍负责网页的语义结构。SwiftUI 外壳无法修复缺少标题层级、表单标签或键盘焦点的 HTML。验收时要同时使用 VoiceOver 检查原生按钮和网页内容,确认焦点不会困在两个层次之间;还要测试动态字体、深色模式、横竖屏、阅读器设置以及 Reduce Motion。

为旧系统保留清晰退路

如果应用仍支持 iOS 25 或更早版本,可以把浏览功能放在小型协议后面:新系统使用 SwiftUI WebKit 接口,旧系统继续使用经过验证的 WKWebView 适配器或直接打开系统浏览器。业务层只提出“显示这个可信 URL”“后退”“刷新”等意图,不接触具体网页视图类型。

这种边界还方便测试。纯导航策略用单元测试,页面模型用可控 URL 响应验证状态转换,最终再用少量 UI 测试覆盖真实点击、返回和错误恢复。不要用一次成功加载公开网站来代替确定性测试,因为网络、内容和地区差异都会让结果漂移。

结论

SwiftUI 的 WebViewWebPage 最重要的变化,是网页不再只是嵌在 representable 里的黑盒。采用它们时仍应先定义安全导航、状态所有权、无障碍和旧系统退路。测试版阶段把接入控制在一个可替换模块中,等正式 SDK 到来后再锁定 API,能够获得新数据流的好处,同时避免整座应用被预览接口牵动。