mcpField
S
swiftui-webkit

swiftui-webkit

github.com/dpearson2699开发1.2k3.3k 次安装13 周前更新
代码仓库

关于 swiftui-webkit

SwiftUI WebKit 是面向开发者的指南,介绍如何在 SwiftUI 中使用 iOS 26 等平台提供的原生 WebKit API 嵌入和管理网页内容。它涵盖页面状态、导航策略、JavaScript 交互及应用自有本地内容。

主要功能

  1. 01SwiftUI 原生 WebView 与 WebPage
  2. 02页面加载和状态观察
  3. 03导航策略控制
  4. 04调用 JavaScript 与参数传递
  5. 05通过自定义 URL Scheme 加载本地内容

适用场景

  1. 01在应用内展示网页内容
  2. 02观察页面加载与导航状态
  3. 03拦截链接并打开外部域名
  4. 04呈现离线 HTML 文档

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dpearson2699/swift-ios-skills --skill swiftui-webkit

安装后在 Claude Code 中输入/swiftui-webkit确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

evals/
evals.json
references/
loading-and-observation.md
local-content-and-custom-schemes.md
migration-and-fallbacks.md
navigation-and-javascript.md

详细介绍

SwiftUI WebKit

使用 iOS 26、iPadOS 26、macOS 26 和 visionOS 26 推出的原生 WebKit-for-SwiftUI API,在 SwiftUI 中嵌入和管理网页内容。当应用需要集成式网页视图、应用自有的 HTML 内容、基于 JavaScript 的页面交互或自定义导航策略控制时,请使用此技能。

目录

选择合适的网页容器

选择最符合需求的工具。

需求默认选择
在 SwiftUI 中嵌入应用自有的网页内容WebView + WebPage
iOS/iPadOS 中具有 Safari 行为的模态浏览SFSafariViewController
macOS 或 visionOS 中跳转到外部浏览器openURL / 默认浏览器
OAuth 或第三方登录ASWebAuthenticationSession
需要支持 iOS 26 以下版本,或使用缺失的旧版专属 WebKit 功能WKWebView 备用方案

对于面向 iOS 26 及更高版本的现代 SwiftUI 应用,如果新 API 能满足功能需求,优先使用 WebView 和 WebPage。Apple 在 WWDC25 的指导中指出,现有 SwiftUI 应用中的 UIKit/AppKit WebKit 封装值得尝试迁移,但这并不意味着必须删除所有备用方案。

OAuth 不要使用嵌入式 WebView,而应使用 ASWebAuthenticationSession 流程。

显示网页内容

如果应用只需呈现 URL,且由 SwiftUI 状态驱动导航,请使用简单的 WebView(url:) 形式。

import SwiftUI
import WebKit

struct ArticleView: View {
    let url: URL

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

如果应用需要直接加载请求、观察状态、调用 JavaScript 或自定义导航行为,请创建 WebPage。

WebPage 一次只能关联一个 WebView。对于多个可见的 WebView,请分别创建 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 的可观察类型。需要在 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 {
                    // Handle load failure.
                }
            }
    }
}

如果需要响应每次导航,请观察导航序列,而不是只检查单个属性。

Task {
    do {
        for try await event in page.navigations {
            // Handle started, redirect, committed, or finished events.
        }
    } catch {
        // Handle WebPage.NavigationError or cancellation.
    }
}

更完善的模式和加载序列示例,请参阅 references/loading-and-observation.md。

导航策略

使用 WebPage.NavigationDeciding,根据请求或响应来允许、取消或自定义导航。

常见用途:

  • 让应用自有域名保留在嵌入式 WebView 中
  • 取消对外部域名的导航,并通过 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
    }
}

应用级深层链接路由请参阅导航技能。本技能负责嵌入式网页内容中的导航。

完整模式请参阅 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 结果:没有显式返回值会产生 nil,而显式返回 JavaScript null 会返回 NSNull。

重要限制:原生 SwiftUI WebKit API 明确支持从 Swift 调用 JavaScript,但没有提供 WKScriptMessageHandler 的明显直接替代方案。如果需要较粗略的 JS 到原生信号传递,自定义导航或回调 URL 模式可以奏效,但应将其记录为一种变通模式,而不是保证可一对一替代的方案。

参见 references/navigation-and-javascript.md。

本地内容和自定义 URL 方案

如果应用需要捆绑的 HTML、离线文档或通过自定义方案提供的应用资源,请使用 WebPage.Configuration 和 URLSchemeHandler。

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 资源
  • 通过自定义方案加载应用自有资源

不要在普通远程内容中滥用自定义方案。对于由服务器托管的页面,优先使用标准 HTTPS。

参见 references/local-content-and-custom-schemes.md。

WebView 自定义

使用 WebView 修饰符来匹配预期的浏览体验。

实用修饰符和相关 API:

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

仅在用户体验需要时应用这些修饰符。

  • 如果用户可能会访问多个页面,请启用前进/后退手势。
  • 如果内容类似文档,请添加页面内查找功能。
  • 仅当应用具有侧边栏、目录或其他明确的导航方式时,才同步滚动位置。

Apple 的 HIG 同样适用于此处:在适当情况下支持前进/后退导航,但不要将应用内的 WebView 变成通用浏览器。

常见错误

  • 在 iOS 26+ SwiftUI 应用中默认使用 WKWebView 封装,而不是从 WebView 和 WebPage 开始
  • 使用嵌入式 WebView 进行 OAuth,而不是使用 ASWebAuthenticationSession
  • 先采用普通的 WebView(url:) 方案,等到需要状态、JS 或导航控制时才改用 WebPage
  • 将 callJavaScript 视为 WKScriptMessageHandler 的直接替代方案
  • 向 callJavaScript 传入可调用的 JavaScript 包装函数,而不是只传入函数体
  • 遍历 page.navigations 时不使用 try/catch,尽管导航失败会以抛出异常的方式终止序列
  • 将同一个 WebPage 绑定到多个可见的 WebView 值
  • 当外部域名应在嵌入界面之外打开时,仍让所有链接留在应用内
  • 在 macOS 或 visionOS 上,将 SFSafariViewController 视为跨平台的外部浏览方案,而不使用默认浏览器/openURL 行为
  • 围绕 WebView 构建浏览器风格的应用外壳,而不是打造专注的嵌入式体验
  • 对本应通过 HTTPS 加载的内容使用自定义 URL 方案
  • 忘记 WebPage 隔离于主执行器

审查清单

  • 对于 iOS 26+ 上的 SwiftUI 网页内容,默认使用 WebView 和 WebPage
  • 身份验证流程使用 ASWebAuthenticationSession,而非嵌入式网页视图
  • 只要应用需要观察状态、调用 JS 或控制策略,就使用 WebPage
  • 导航策略只拦截应用实际拥有或需要重新路由的 URL
  • 适当情况下,在外部打开外部域名
  • 将 JavaScript 返回值防御性地转换为具体的 Swift 类型
  • callJavaScript 使用函数体,并通过 arguments 传递 Swift 值
  • page.navigations 循环使用 for try await,并处理抛出的导航错误
  • 每个可见的 WebView(page) 都拥有独立的 WebPage
  • 仅对应用实际拥有的资源使用自定义 URL 方案
  • 预期支持多页面浏览时,启用前进/后退手势或控件
  • SFSafariViewController 仅用于 iOS/iPadOS 上类似 Safari 的模态浏览;macOS 和 visionOS 的外部浏览流程使用平台默认浏览器行为
  • 网页体验应提供有针对性的原生功能价值,而不是表现得像一个薄薄的浏览器外壳
  • 仅在部署目标或缺少所需 API 有充分理由时,才回退到 WKWebView

参考资料

评论

支持 Markdown