実践SwiftUI WebKit – WebViewでできることとその実装方法

Swift

WebKitとは

WebKit for SwiftUI | Apple Developer Documentation
Integrate web content into your SwiftUI apps with new standard views you connect to webpages.

WebKitとは、iOS開発でWeb機能を提供するフレームワークです。

従来の課題

WebKitではWKWebView を使うことでWebを表示できます。

しかしこれらはSwiftUIでは直接使用できず、UIViewRepresentableでラップする必要がありました。

Swift
import SwiftUI
import WebKit

struct WebView: UIViewRepresentable {
    let url: URL
    
    func makeUIView(context: Context) -> WKWebView {
        return WKWebView()
    }
    
    func updateUIView(_ webView: WKWebView, context: Context) {
        let request = URLRequest(url: url)
        webView.load(request)
    }
}

struct ContentView: View {
    var body: some View {
        WebView(url: URL(string: "https://www.apple.com")!)
            .navigationTitle("Web View")
    }
}

WWDC25にてSwiftUI対応

しかしWWDC 2025にてWebKitがSwiftUI向けに拡張され、 WebView というものが使えるようになりました。これにより、SwiftUIでもWeb表示をより簡潔に記述できるようになりました。

SwiftUI向けWebKitの紹介 - WWDC25 - ビデオ - Apple Developer
WebKitを使って、WebコンテンツをSwiftUIアプリに簡単に統合する方法を学びましょう。Webコンテンツをロードして表示する方法や、Webページと通信する方法などを紹介します。

今回はWebViewの基本的な使い方から、応用例、さらにちょっとだけ内部設計にまで踏み込んで解説します。

動作環境

  • Xcode 26 beta 1
  • Swift6.2

基本的な使い方

WebView | Apple Developer Documentation
A view that displays some web content.

ではさっそく、WebViewの使い方について解説していきます。

まずはシンプルに指定したURLを表示してみましょう。

Swift
import SwiftUI
import WebKit

struct ContentView: View {
    var body: some View {
        WebView(url: URL(string: "https://google.com")!)
            .ignoresSafeArea(edges: .bottom)
    }
}

WKWebViewをUIViewRepresentableでラップしてた今までの方法と比べると、すごく簡潔になりました。

表示するURLを切り替えることも可能です。

Swift
import SwiftUI
import WebKit

struct ContentView: View {
    @State private var url: URL? = URL(string: "https://google.com")

    var body: some View {
        NavigationStack {
            WebView(url: url)
                .ignoresSafeArea(edges: .bottom)
                .toolbar {
                    ToolbarItem(placement: .topBarTrailing) {
                        Button {
                            url = URL(string: "https://apple.com")
                        } label: {
                            Image(systemName: "apple.logo")
                        }
                    }
                }
        }
    }
}

WebPageクラス

WebPageとは

WebPage | Apple Developer Documentation
An object that controls and manages the behavior of interactive web content.

WebPageとは、Webコンテンツを表現し、制御するためのObservableクラスです。表示しているURLやタイトル、ローディング状況などを取得することができます。

WebViewにはURLで初期化する以外に、WebPageを受け取るinitも持っています。

Swift
import SwiftUI
import WebKit

struct ContentView: View {
    @State private var page = WebPage()
    
    var body: some View {
        WebView(page) // WebPageを使って初期化
            .ignoresSafeArea(edges: .bottom)
            .onAppear {
                page.load(URLRequest(url: URL(string:"https://google.com")!))
            }
    }
}

次の例はWebPageを使って、Webコンテンツの情報を取得しています。

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

    var body: some View {
        NavigationStack {
            WebView(page)
                .navigationTitle(page.title)
                .navigationBarTitleDisplayMode(.inline)
                .ignoresSafeArea(edges: .bottom)
                .overlay {
                    if page.isLoading {
                        Color.black.opacity(0.1)
                            .ignoresSafeArea()
                        ProgressView()
                    }
                }
        }
        .onAppear {
            page.load(URLRequest(url: URL(string:"https://google.com")!))
        }
    }
}

取得可能な情報

WebPageを使うことで、以下のような値を取得することができます。

Swift
webPage.title              // ページタイトル
webPage.url                // 現在のURL
webPage.estimatedProgress  // 読み込み進捗(0.0〜1.0)
webPage.themeColor         // ページのテーマカラー
webPage.backForwardList    // ページ履歴のリスト

他にもありますが、詳しくは公式ドキュメントをご覧ください。

後で詳しく解説しますが、WebPageは内部でWKWebViewを保持しており、WKWebViewの値を自身に反映しています。

実践的な活用例

ページ内の文字列検索

.findNavigatorを使うことで、ページ内の文字列検索ができます。

Swift
struct ContentView: View {
    @State private var page = WebPage()
    @State private var showFindNavigator = false

    var body: some View {
        NavigationStack {
            WebView(page)
                .ignoresSafeArea(edges: .bottom)
                .findNavigator(isPresented: $showFindNavigator)
                .toolbar {
                    ToolbarItem(placement: .topBarTrailing) {
                        Button {
                            showFindNavigator.toggle()
                        } label: {
                            Image(systemName: "magnifyingglass")
                        }
                    }
                }
        }
        .onAppear {
            page.load(URLRequest(url: URL(string:"https://google.com")!))
        }
    }
}

JavaScript連携

WebPageのcallJavaScriptを使うことで、Webコンテンツに対してJavaScriptを簡単に適応できるようになります。

まずはコンテンツ内の要素を変更する方法を紹介します。

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

    var body: some View {
        VStack {
            Button("文字を赤にする") {
                Task {
                    try await page.callJavaScript("document.body.style.color = 'red';")
                }
            }

            WebView(page)
                .onAppear {
                    page.load(URLRequest(url: URL(string:"https://www.lipsum.com/")!))
                }
        }
    }
}

次にコンテンツの値を受け取る方法を紹介します。callJavaScriptは返り値で、JavaScriptの結果を返してくれます。

Swift
struct ContentView: View {
    @State private var page = WebPage()
    @State private var pageTitle: String = ""
    @State private var pageHeight: CGFloat = 0

    var body: some View {
        VStack {
            Text("ページタイトル: \(pageTitle)")
            Text("ページの高さ: \(pageHeight)")

            Button("情報を取得") {
                Task {
                    await getInfo()
                }
            }

            WebView(page)
                .onAppear {
                    page.load(URLRequest(url: URL(string:"https://apple.com")!))
                }
        }
    }

    private func getInfo() async {
        if let result = try? await page.callJavaScript("return document.title"),
           let title = result as? String {
            pageTitle = title
        }

        if let result = try? await page.callJavaScript("return document.body.scrollHeight"),
           let height = result as? CGFloat {
            pageHeight = height
        }
    }
}

戻るボタンの実装

戻るボタンはWebPageのbackForwardListを使うことで実装できます。

次の例は、2ページ目以降であればボタンを表示して、押下することで前のページに戻る方法です。

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

    var body: some View {
        NavigationStack {
            WebView(page)
                .ignoresSafeArea(edges: .bottom)
                .navigationTitle(page.title)
                .toolbar {
                    ToolbarItem(placement: .topBarLeading) {
                        if !page.backForwardList.backList.isEmpty {
                            Button {
                                page.load(page.backForwardList.backList.last!)
                            } label: {
                                Image(systemName: "chevron.left")
                                    .foregroundStyle(.blue)
                            }
                        }
                    }
                }
        }
        .onAppear {
            page.load(URLRequest(url: URL(string:"https://google.com")!))
        }
    }
}

特定のドメインのみを読み込む

アプリの要件によっては、特定のドメインだけを読み込みたいこともあります。

そんな時は、NavigationDecidingを使用します。

WebPage.NavigationDeciding | Apple Developer Documentation
Allows providing custom behavior to handle navigation changes and to coordinate these changes for the web page’s main pa...

次の例では、apple.comだけを許可し、それ以外のドメインは外部ブラウザを起動するようにしています。

Swift
struct ContentView: View {
    @Environment(\.openURL) private var openURL

    @State private var page = WebPage()

    var body: some View {
        NavigationStack {
            WebView(page)
                .ignoresSafeArea(edges: .bottom)
        }
        .onAppear {
            let navigationDecider = NavigationDeciding()
            navigationDecider.urlToOpen = { url in
                if let url {
                    openURL(url)
                }
            }
            page = WebPage(navigationDecider: navigationDecider)
            page.load(URLRequest(url: URL(string:"https://apple.com")!))
        }
    }
}

final class NavigationDeciding: WebPage.NavigationDeciding {

    var urlToOpen: ((URL?) -> Void)?

    func decidePolicy(for action: WebPage.NavigationAction, preferences: inout WebPage.NavigationPreferences) async -> WKNavigationActionPolicy {
        let url = action.request.url
        if url?.host() == "apple.com" || url?.host() == "www.apple.com" {
            return .allow
        }
        urlToOpen?(url)
        return .cancel
    }
}

WebViewとWebPageの関係性

WebKitはOSSなので、誰でもソースコードを閲覧できます。

GitHub - WebKit/WebKit: Home of the WebKit project, the browser engine used by Safari, Mail, App Store and many other applications on macOS, iOS and Linux.
Home of the WebKit project, the browser engine used by Safari, Mail, App Store and many other applications on macOS, iOS...

ここからは、少しだけ内部設計に踏み込んでWebViewとWebPageの関係性について解説します。

WebPageは内部にWKWebViewを保持しています。WKWebViewの値(title, urlなど)を監視することで、自身の値を更新しています。

WebPage.swift
// このWebPageWebViewはWKWebViewを継承してる
public lazy var backingWebView: WebPageWebView = {
    let webView = WebPageWebView(frame: .zero, configuration: WKWebViewConfiguration(configuration))
    webView.navigationDelegate = backingNavigationDelegate
    webView.uiDelegate = backingUIDelegate
    #if os(macOS)
    webView._usePlatformFindUI = false
    #endif
    return webView
}()
WebPageWebView.swift
public final class WebPageWebView: WKWebView {
    // ...
}

そしてWebViewは内部でWebPageを保持しています。

WebViewにURLを渡して初期化する時も、内部ではWebPageを生成しています。

WebView.swift
public init(url: URL?) {
    self.storage = .state(State(initialValue: WebPage()), url)
    //                          ↑ ここで WebPage() を作成
}

WebViewはView構造体なので、bodyを見てみましょう。

WebView.swift
public var body: some View {
    representable
}

@ViewBuilder
private var representable: some View {
    WebViewRepresentable(page: storage.webPage, safeAreaInsets: proxy.safeAreaInsets)
}

そしてこのWebViewRepresentableの中で、WebPageのWKWebViewを参照しています。

WebViewRepresentable.swift
func makePlatformView(context: Context) -> CocoaWebViewAdapter {
    precondition(!page.isBoundToWebView, "This web page is already bound to another web view.")
    
    let parent = CocoaWebViewAdapter()
    parent.webView = page.backingWebView  // ← 実際のWKWebViewを設定
    page.isBoundToWebView = true
    
    return parent
}

つまりWebPageが中核となって、UIKit(WKWebView)とSwiftUI(WebView)の橋渡し的な役割を果たしています。

まとめると、こんな感じです。

WebView → WebPageのbackingWebViewを見て、Webを表示
WebPage → データの管理、UIKitとSwiftUIの橋渡し

まとめ

今回はWWDC25で発表された、SwiftUI向けのWebKitについて解説しました。

新たに追加されたWebViewを使えば、SwiftUIでも簡単にWebコンテンツを表示できることがわかったと思います。

ただこれらの機能が使えるのは、iOS26以降なのでそこは注意です。

ぜひご自身の手で色々と試してみてください。

ここまでのご閲覧ありがとうございました!

参考にした記事

コメント

タイトルとURLをコピーしました