swiftui-webkit

swiftui-webkit

热门

利用 WebKit for SwiftUI 在 SwiftUI 中嵌入并控制 Web 内容,涵盖 WebView、WebPage、导航策略、JavaScript 执行、可观察的页面状态、链接拦截、本地 HTML 或数据加载以及自定义 URL Scheme。适用于构建 iOS 26+ 的文章/详情页视图、帮助中心、应用内文档或其它基于 HTML、CSS 和 JavaScript 的嵌入式 Web 体验。

957Star
48Fork
更新于 2026/7/31
SKILL.md
只读
名称
swiftui-webkit
描述

利用 WebKit for SwiftUI 在 SwiftUI 中嵌入并控制 Web 内容,涵盖 WebView、WebPage、导航策略、JavaScript 执行、可观察的页面状态、链接拦截、本地 HTML 或数据加载以及自定义 URL Scheme。适用于构建 iOS 26+ 的文章/详情页视图、帮助中心、应用内文档或其它基于 HTML、CSS 和 JavaScript 的嵌入式 Web 体验。

SwiftUI WebKit

使用为 iOS 26、iPadOS 26、macOS 26 和 visionOS 26 引入的原生 WebKit-for-SwiftUI API 在 SwiftUI 中嵌入并管理 Web 内容。当应用需要集成 Web 界面、应用自带的 HTML 内容、基于 JavaScript 的页面交互或自定义导航策略控制时,即可使用此 Skill。

目录

选择合适的 Web 容器

根据需求选择最精准轻量级的工具。

需求场景 默认首选
在 SwiftUI 中嵌入应用自有的 Web 内容 WebView + WebPage
iOS/iPadOS 上具备 Safari 行为的模态浏览 SFSafariViewController
macOS 或 visionOS 上的跳转外部浏览器行为 openURL / 默认浏览器
OAuth 或第三方登录 ASWebAuthenticationSession
向下兼容 iOS 26 以下版本,或使用缺失的旧版 WebKit 特性 WKWebView 回退方案

当针对 iOS 26+ 的现代 SwiftUI 应用且新 API 覆盖所需功能时,优先使用 WebViewWebPage。Apple 在 WWDC25 给出的指导建议是:可以尝试将 SwiftUI 应用中现有的 UIKit/AppKit WebKit 封装进行迁移,但并非要求盲目删除所有旧版回退代码。

切勿将嵌入式 Web View 用于 OAuth 流程。OAuth 应当始终保持使用 ASWebAuthenticationSession 流程。

显示 Web 内容

当应用只需要渲染某个 URL,且由 SwiftUI 状态驱动导航时,使用简洁的 WebView(url:) 即可。

import SwiftUI
import WebKit

struct ArticleView: View {
    let url: URL

    var body: some View {
        WebView(url: url)
    }
}

当应用需要直接加载 Request、监听状态、调用 JavaScript 或自定义导航行为时,请创建 WebPage

一个 WebPage 实例同一时间只能与一个 WebView 关联。若有多个可见的 Web View,需要分别为其创建独立的 WebPage 实例。

@Observable
@MainActor
final class ArticleModel {
    let page = WebPage()

    func load(_ url: URL) async throws {
        for try await _ in page.load(URLRequest(url: url)) {
        }
    }
}

struct ArticleDetailView: View {
    @State private var model = ArticleModel()
    let url: URL

    var body: some View {
        WebView(model.page)
            .task {
                try? await model.load(url)
            }
    }
}

完整示例请参阅 references/loading-and-observation.md

使用 WebPage 加载与监听

WebPage 是一个标注了 @MainActor 的可观察(observable)类型。当需要在 SwiftUI 中获取页面状态时请使用它。

常用的加载入口:

  • load(URLRequest)
  • load(URL)
  • load(html:baseURL:)
  • load(_:mimeType:characterEncoding:baseURL:)

常用的可观察属性:

  • title
  • url
  • isLoading
  • estimatedProgress
  • currentNavigationEvent
  • backForwardList
struct ReaderView: View {
    @State private var page = WebPage()

    var body: some View {
        WebView(page)
            .navigationTitle(page.title ?? "Loading")
            .overlay {
                if page.isLoading {
                    ProgressView(value: page.estimatedProgress)
                }
            }
            .task {
                do {
                    for try await _ in page.load(URLRequest(url: URL(string: "https://example.com")!)) {
                    }
                } catch {
                    // 处理加载失败
                }
            }
    }
}

如果需要对每一次导航做出响应,请监听导航序列(navigation sequence),而不是仅检查单一属性。

Task {
    do {
        for try await event in page.navigations {
            // 处理 started、redirect、committed 或 finished 事件
        }
    } catch {
        // 处理 WebPage.NavigationError 或取消操作
    }
}

更稳健的模式和加载序列示例请参阅 references/loading-and-observation.md

导航策略

使用 WebPage.NavigationDeciding 可以根据 Request 或 Response 来允许、取消或自定义导航。

典型用途:

  • 将应用自有的域名保留在嵌入式 Web View 内
  • 取消外部域名的导航,并交由 openURL 打开
  • 拦截特殊的回调 URL
  • 调优 NavigationPreferences
@MainActor
final class ArticleNavigationDecider: WebPage.NavigationDeciding {
    var urlToOpenExternally: URL?

    func decidePolicy(
        for action: WebPage.NavigationAction,
        preferences: inout WebPage.NavigationPreferences
    ) async -> WKNavigationActionPolicy {
        guard let url = action.request.url else { return .allow }

        if url.host == "example.com" {
            return .allow
        }

        urlToOpenExternally = url
        return .cancel
    }
}

应用层面的深度链接(deep-link)路由请保留在导航相关的 Skill 中。本 Skill 仅负责嵌入式 Web 内容内部发生的导航。

完整模式请参阅 references/navigation-and-javascript.md

JavaScript 集成

使用 callJavaScript(_:arguments:in:contentWorld:) 对页面执行 JavaScript 函数评估。

请传入 JavaScript 函数体,而不是包裹好的函数声明或调用表达式。推荐通过 arguments 传递 Swift 提供的数值,避免将不可信的字符串拼接到脚本中。

let script = """
const headings = [...document.querySelectorAll('h1, h2')];
return headings.map(node => ({
    id: node.id,
    text: node.textContent?.trim()
}));
"""

let result = try await page.callJavaScript(script)
let headings = result as? [[String: Any]] ?? []

可以通过 arguments 字典传递参数,并将返回的 Any 转换为你实际需要的 Swift 类型。

let result = try await page.callJavaScript(
    "return document.getElementById(sectionID)?.getBoundingClientRect().top ?? null;",
    arguments: ["sectionID": selectedSectionID]
)

需要审慎处理空值和 JavaScript 的 null 结果:没有显式 return 会产生 nil,而显式的 JavaScript null 会返回 NSNull

重要边界:原生的 SwiftUI WebKit API 明确支持 Swift 到 JavaScript 的调用,但并未提供针对 WKScriptMessageHandler 的直接替代方案。如果你需要粗粒度的 JS 到原生的信号通信,可以使用自定义导航或回调 URL 模式作为变通方案(workaround),但请将其记录为变通模式,而非保证一比一对齐的替代品。

详见 references/navigation-and-javascript.md

本地内容与自定义 URL Scheme

当应用需要加载打包进 App 的 HTML、离线文档或通过自定义 Scheme 提供资源时,使用 WebPage.ConfigurationURLSchemeHandler

var configuration = WebPage.Configuration()
configuration.urlSchemeHandlers[URLScheme("docs")!] = DocsSchemeHandler(bundle: .main)

let page = WebPage(configuration: configuration)
for try await _ in page.load(URL(string: "docs://article/welcome")!) {
}

适用于:

  • 应用内置的文档或文章内容
  • 离线 HTML/CSS/JS 资源
  • 在自定义 Scheme 下加载应用自有的资源

对于常规的远程内容,切勿滥用自定义 Scheme。服务器托管的页面请优先使用标准 HTTPS。

详见 references/local-content-and-custom-schemes.md

WebView 自定义

使用 WebView 修饰符(modifiers)来契合预期的浏览体验。

实用的修饰符和相关 API:

  • webViewBackForwardNavigationGestures(_:)
  • findNavigator(isPresented:)
  • webViewScrollPosition(_:)
  • webViewOnScrollGeometryChange(...)

仅在用户体验确实需要时才应用它们。

  • 当用户可能会浏览多个页面时,开启前进/后退手势。
  • 当内容类似于文档时,添加页内查找(Find in Page)。
  • 仅当应用拥有侧边栏、目录或其它显式导航控件时,才同步滚动位置。

Apple 的 HIG(人机界面指南)在此同样适用:在合适的时候支持前进/后退导航,但不要将应用内的 Web View 变成一个通用浏览器。

常见误区

  • 在 iOS 26+ SwiftUI 应用中默认使用 WKWebView 封装,而不是优先从 WebViewWebPage 开始
  • 将嵌入式 Web View 用于 OAuth,而不是使用 ASWebAuthenticationSession
  • 构建了纯 WebView(url:) 路径后,在需要状态、JS 或导航控制时才临时补救使用 WebPage
  • callJavaScript 当作 WKScriptMessageHandler 的直接替代品
  • callJavaScript 传递可调用的 JS 函数封装,而不是仅传函数体
  • 遍历 page.navigations 时未包含 try/catch,即使导航失败会抛出异常并终止序列
  • 将同一个 WebPage 绑定到多个可见的 WebView
  • 在应当跳转到外部浏览器时,仍把所有链接保留在应用内
  • 在 macOS 或 visionOS 上把 SFSafariViewController 当作跨平台跳转外部浏览器的方案,而不是使用默认浏览器/openURL 行为
  • 围绕 WebView 构建类似浏览器的 App 外壳,而不是聚焦于嵌入式体验
  • 对本应通过 HTTPS 加载的内容使用自定义 URL Scheme
  • 遗忘了 WebPage 是受主线程隔离(main-actor-isolated)的

审查清单

  • [ ] WebViewWebPage 是 iOS 26+ SwiftUI Web 内容的首选路径
  • [ ] 认证流程使用 ASWebAuthenticationSession 而非嵌入式 Web View
  • [ ] 只要应用需要状态监听、JS 调用或策略控制,就使用 WebPage
  • [ ] 导航策略仅拦截应用真正拥有或需要重定向的 URL
  • [ ] 外部域名在适当时跳转到外部打开
  • [ ] JavaScript 返回值防御性地转换为具体的 Swift 类型
  • [ ] callJavaScript 使用函数体并通过 arguments 传递 Swift 数值
  • [ ] page.navigations 循环使用 for try await 并处理抛出的导航错误
  • [ ] 每个可见的 WebView(page) 都拥有独立的 WebPage
  • [ ] 自定义 URL Scheme 仅用于真正的应用自有资源
  • [ ] 在预期有多页浏览时,开启前进/后退手势或控件
  • [ ] SFSafariViewController 仅限 iOS/iPadOS 的 Safari 风格模态浏览;macOS 和 visionOS 的外跳流程使用平台默认浏览器行为
  • [ ] Web 体验增加了聚焦的原生价值,而非表现得像一个简陋的浏览器外壳
  • [ ] 使用 WKWebView 回退方案有明确的部署目标版本或缺失 API 需求的合理依据

参考资料