Using SwiftUI WebView Without UIViewRepresentable

For years, adding a web view to a SwiftUI screen meant writing a UIViewRepresentable wrapper around WKWebView. That worked, but it always felt like a lot of ceremony for something as common as showing a privacy policy, help center page, local HTML file, or embedded documentation.

With WebKit for SwiftUI, we finally get a native WebView type. The new API is available on iOS 26, iPadOS 26, macOS 26, Mac Catalyst 26, and visionOS 26, so you still need availability checks if your app supports older systems.

The API lives in the WebKit framework, not SwiftUI, so the first little gotcha is remembering the import:

import SwiftUI
import WebKit

The old wrapper

This is the kind of iOS wrapper many of us have written at least once:

import SwiftUI
import WebKit

struct LegacyWebView: UIViewRepresentable {
    let url: URL

    func makeUIView(context: Context) -> WKWebView {
        WKWebView()
    }

    func updateUIView(_ webView: WKWebView, context: Context) {
        webView.load(URLRequest(url: url))
    }
}

It is not awful, but the moment you need navigation delegates, loading progress, custom link behavior, or JavaScript callbacks, the wrapper starts growing a coordinator and a handful of glue code. The macOS version has the same shape, but uses NSViewRepresentable, makeNSView, and updateNSView instead.

The new simple version

For a basic page, the new version is just a SwiftUI view:

import SwiftUI
import WebKit

struct PrivacyPolicyView: View {
    private let url = URL(string: "https://example.com/privacy")

    var body: some View {
        WebView(url: url)
            .navigationTitle("Privacy Policy")
            .webViewBackForwardNavigationGestures(.disabled)
    }
}

That is enough when you only need to display a URL. WebView(url:) takes an optional URL, so you do not need to force unwrap a hard-coded string just to satisfy the initializer.

Use WebPage for state and control

If the screen needs loading progress, the current page title, reload controls, JavaScript, or navigation decisions, use WebPage. Think of WebView as the thing on screen and WebPage as the observable model controlling the web content.

Here is a small article browser:

import SwiftUI
import WebKit

struct ArticleBrowserView: View {
    @State private var page = WebPage()
    @State private var loadError: String?
    @State private var showFind = false

    private let url = URL(string: "https://www.swift.org")!

    var body: some View {
        WebView(page)
            .navigationTitle(page.title.isEmpty ? "Loading" : page.title)
            .findNavigator(isPresented: $showFind)
            .overlay(alignment: .top) {
                if page.isLoading {
                    ProgressView(value: page.estimatedProgress)
                        .progressViewStyle(.linear)
                }
            }
            .overlay {
                if let loadError {
                    ContentUnavailableView(
                        "Could not load page",
                        systemImage: "wifi.exclamationmark",
                        description: Text(loadError)
                    )
                }
            }
            .toolbar {
                Button("Reload", systemImage: "arrow.clockwise") {
                    Task {
                        await reloadPage()
                    }
                }

                Button("Find", systemImage: "magnifyingglass") {
                    showFind.toggle()
                }
            }
            .task {
                await loadPage()
            }
    }

    private func loadPage() async {
        loadError = nil

        do {
            for try await _ in page.load(URLRequest(url: url)) {
                // You can inspect navigation events here if needed.
            }
        } catch {
            loadError = error.localizedDescription
        }
    }

    private func reloadPage() async {
        loadError = nil

        do {
            for try await _ in page.reload() {
                // You can inspect navigation events here if needed.
            }
        } catch {
            loadError = error.localizedDescription
        }
    }
}

The nice part is that page.title, page.isLoading, and page.estimatedProgress are normal observable state. There is no coordinator just to get a title into the navigation bar.

Loading local HTML

WebPage can also load HTML directly. This is perfect for release notes, generated documentation, or a small piece of styled content bundled with the app:

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

    private let html = """
    <!doctype html>
    <html>
      <head>
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <style>
          body { font: -apple-system-body; padding: 24px; }
        </style>
      </head>
      <body>
        <h1>Release Notes</h1>
        <p>Thanks for updating. This version includes a few nice fixes.</p>
      </body>
    </html>
    """

    var body: some View {
        WebView(page)
            .navigationTitle("Release Notes")
            .task {
                page.load(
                    html: html,
                    baseURL: URL(string: "about:blank")!
                )
            }
    }
}

If your HTML references bundled CSS, images, or JavaScript files, use a file URL for the directory containing those subresources. If the resources are virtual or generated at runtime, Apple’s WebKit for SwiftUI docs also cover URLSchemeHandler for custom local resource loading.

When I would still use WKWebView directly

I would start with the native SwiftUI API for new iOS 26 and macOS 26 screens. I would still keep a WKWebView wrapper around when supporting older OS versions or when an app has a deep, already-tested custom WebKit integration.

For simple in-app pages, this is a huge cleanup. The code reads like SwiftUI, page state flows into the view naturally, and common behavior like find-in-page can be added with a modifier instead of a custom delegate pipeline.

Further reading: