swiftui-webkit

swiftui-webkit

熱門

在 SwiftUI 中使用專為 SwiftUI 設計的 WebKit 內嵌並控制網頁內容,包含 WebView、WebPage、導覽策略、JavaScript 執行、可觀察的頁面狀態、連結攔截、本地 HTML 或資料載入,以及自訂 URL Scheme。適用於建構 iOS 26+ 的文章/詳情頁面、說明中心、App 內文件,或是其他由 HTML、CSS 與 JavaScript 支援的嵌入式網頁體驗。

957星標
48分支
更新於 2026/7/31
SKILL.md
唯讀
名稱
swiftui-webkit
描述

在 SwiftUI 中使用專為 SwiftUI 設計的 WebKit 內嵌並控制網頁內容,包含 WebView、WebPage、導覽策略、JavaScript 執行、可觀察的頁面狀態、連結攔截、本地 HTML 或資料載入,以及自訂 URL Scheme。適用於建構 iOS 26+ 的文章/詳情頁面、說明中心、App 內文件,或是其他由 HTML、CSS 與 JavaScript 支援的嵌入式網頁體驗。

SwiftUI WebKit

使用針對 iOS 26、iPadOS 26、macOS 26 與 visionOS 26 引入的原生 WebKit-for-SwiftUI API,在 SwiftUI 中嵌入並管理網頁內容。當 App 需要整合式網頁介面、App 專屬的 HTML 內容、基於 JavaScript 的頁面互動,或自訂導覽策略控制時,請使用此 Skill。

目錄

選擇合適的網頁容器

請選擇能滿足需求的最精準工具。

需求 預設選擇
在 SwiftUI 中嵌入 App 專屬網頁內容 WebView + WebPage
具備 Safari 行為的 iOS/iPadOS 彈窗瀏覽 SFSafariViewController
macOS 或 visionOS 的外跳瀏覽行為 openURL / 預設瀏覽器
OAuth 或第三方登入 ASWebAuthenticationSession
相容 iOS 26 以下版本或使用缺少的舊版 WebKit 功能 退回使用 WKWebView

當新 API 功能可滿足需求時,針對 iOS 26+ 的現代 SwiftUI App 應優先使用 WebViewWebPage。Apple 在 WWDC25 的指引中指出,SwiftUI App 中現有的 UIKit/AppKit WebKit 包裝器適合嘗試遷移,但並非要求一律刪除所有備用方案。

切勿將內嵌網頁視圖用於 OAuth。OAuth 應始終保持使用 ASWebAuthenticationSession 流程。

顯示網頁內容

當 App 只需要渲染 URL 且導覽由 SwiftUI 狀態驅動時,請使用簡短形式 WebView(url:)

import SwiftUI
import WebKit

struct ArticleView: View {
    let url: URL

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

當 App 需要直接載入請求、觀察狀態、呼叫 JavaScript 或自訂導覽行為時,請建立 WebPage

一個 WebPage 一次只能與一個 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 的可觀察(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 {
                    // 處理載入失敗。
                }
            }
    }
}

當需要對每次導覽做出回應時,請觀察導覽序列,而非僅檢查單一屬性。

Task {
    do {
        for try await event in page.navigations {
            // 處理已開始(started)、轉址(redirect)、已提交(committed)或已完成(finished)事件。
        }
    } catch {
        // 處理 WebPage.NavigationError 或取消。
    }
}

請參閱 references/loading-and-observation.md 瞭解更穩健的模式與載入序列範例。

導覽策略

使用 WebPage.NavigationDeciding 根據請求或回應允許、取消或自訂導覽。

典型用途:

  • 將 App 專屬網域保留在嵌入式網頁視圖內
  • 取消外部網域並透過 openURL 交由外部開啟
  • 攔截特殊的 CallBack 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
    }
}

請將 App 層級的 Deep Link 路由保留在導覽 Skill 中。本 Skill 負責處理嵌入式網頁內容內發生的導覽。

請參閱 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 到原生的訊號傳遞,可以使用自訂導覽或 CallBack URL 模式作為替代方案,但應將其標記為變通做法,而非保證一比一替代的方案。

請參閱 references/navigation-and-javascript.md

本地內容與自訂 URL Scheme

當 App 需要在自訂 Scheme 下載入打包的 HTML、離線文件或 App 提供的資源時,請使用 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 下的 App 專屬資源載入

請勿在一般遠端內容過度使用自訂 Scheme。伺服器代管的頁面請優先使用標準 HTTPS。

請參閱 references/local-content-and-custom-schemes.md

自訂 WebView

使用 WebView 修飾符(Modifiers)以符合預期的瀏覽體驗。

實用的修飾符與相關 API:

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

僅在使用者體驗有需要時套用:

  • 當使用者有可能存取多個頁面時,啟用上一頁/下一頁手勢。
  • 當內容屬於文件類型時,加入「頁面內尋找(Find in Page)」。
  • 僅在 App 擁有側邊欄、目錄或其他明確的導覽輔助 UI 時,同步捲動位置。

Apple 的 HIG(人機介面指南)在此同樣適用:在適當時候支援上一頁/下一頁導覽,但切勿將 App 的網頁視圖變成通用型的瀏覽器。

常見錯誤

  • 在 iOS 26+ SwiftUI App 中預設使用 WKWebView 包裝器,而不是先從 WebViewWebPage 開始
  • 將內嵌網頁視圖用於 OAuth,而不是使用 ASWebAuthenticationSession
  • 建構好缺乏狀態、JS 或導覽控制的純 WebView(url:) 路徑後,才回頭改用 WebPage
  • callJavaScript 視為 WKScriptMessageHandler 的直接替代方案
  • callJavaScript 傳入可呼叫的 JavaScript 包裝函式,而非僅傳入函式主體
  • 疊代 page.navigations 時未加上 try/catch,即使導覽失敗會拋出例外並終止序列
  • 將同一個 WebPage 綁定至多個可見的 WebView 實例
  • 在外部網域應於內嵌視圖外開啟時,仍將所有連結保留在 App 內部
  • 在 macOS 或 visionOS 上將 SFSafariViewController 視為跨平台外跳瀏覽的解答,而不是使用預設瀏覽器/openURL 行為
  • 圍繞 WebView 打造瀏覽器風格的 App 外殼,而非專注的嵌入式體驗
  • 對原本應透過 HTTPS 載入的內容使用自訂 URL Scheme
  • 忘記 WebPage 是隔離在 Main Actor(@MainActor)上的

審查清單

  • [ ] WebViewWebPage 為 iOS 26+ SwiftUI 網頁內容的預設路徑
  • [ ] 認證流程使用 ASWebAuthenticationSession,而非內嵌網頁視圖
  • [ ] 每當 App 需要狀態觀察、JS 呼叫或策略控制時,均使用 WebPage
  • [ ] 導覽策略僅攔截 App 真正擁有或需要重新路由的 URL
  • [ ] 外部網域在適當時於外部開啟
  • [ ] JavaScript 回傳值會防禦性地轉型為具體的 Swift 類型
  • [ ] callJavaScript 使用函式主體並透過 arguments 傳遞 Swift 數值
  • [ ] page.navigations 迴圈使用 for try await 並處理拋出的導覽錯誤
  • [ ] 每個可見的 WebView(page) 擁有獨立的 WebPage
  • [ ] 自訂 URL Scheme 僅用於真正的 App 專屬資源
  • [ ] 預期會有多頁面瀏覽時,已啟用上一頁/下一頁手勢或控制項
  • [ ] SFSafariViewController 僅限於 iOS/iPadOS 上 Safari 風格的彈窗瀏覽;macOS 與 visionOS 的外跳流程使用平台預設瀏覽器行為
  • [ ] 網頁體驗提供專注的原生價值,而非像個薄弱的瀏覽器外殼
  • [ ] 退回使用 WKWebView 係基於部署目標或缺乏所需 API 的正當理由

參考資料