WebView is Finally Coming to SwiftUI

After the 6 years that has passed since SwiftUI was first announced, we finally get a native WebView, with some additional web-related tools. Let’s take a look at it and what it means for my .

WebViewKit

I created many years ago, to make it easy to embed web content into any SwiftUI-based app on iOS, macOS, and visionOS:

import SwiftUI
import WebViewKit

struct MyView {

    var body: some View {
        WebView(
            url: URL(string: "https://danielsaidi.com")
        )
    }
}

This view is very flexible and allows you to load URLs and HTML content directly into the initializer. You could also configure the underlying WKWebView with a configuration and viewConfiguration, to for instance set up deeper integrations, navigation observation, etc.

For the cases where you just don’t want to display web content, the SafariWebView can be used to show a SFSafariViewController that contains a toolbar with navigation controls.

A New, Native WebView

In iOS, macOS, and visionOS 26, there will finally be a new, native WebView component, which gives us access to easy ways to get started, and deep ways to go deeper.

This is how you add a basic WebView to just show some web content at a certain URL:

import SwiftUI

struct MyView {

    var body: some View {
        WebView(
            url: URL(string: "https://danielsaidi.com")
        )
    }
}

If this looks similar to before, it’s because this APIs is exactly the same as in WebViewKit’s WebView.

We can make the URL a @State property, to make it easy to change which URL that is displayed:

import SwiftUI

struct MyView {

    @State var url = URL(string: "https://danielsaidi.com")

    var body: some View {
        VStack {
            WebView(url: url)
            Button("Go to Kankoda") {
                url = URL(string: "https://kankoda.com")
            }
        }
    }
}

For more granular control and web content integrations, we can use a WebPage instead of a URL:

struct MyView {

    @State var page = WebPage()

    var body: some View {
        NavigationStack {
            WebView(page)
                .navigationTitle(page.title)
        }
    }
}

The WebPage is a new, observable type that can be used to load, control, and communicate with the web page that is displayed in a WebView.

WebPage

The new WebPage type can be used on it’s own, but is great when presented within a WebView.

We can load a URL request into a WebPage without using a WebView:

// Loading a URL request
let page = WebPage()
var request = URLRequest(url: "http://danielsaidi.com/blog")
request.attribution = user
page.load(request)

We can also load HTML content directly into it:

// Loading an HTML string
let page = WebPage()
page.load(html: "<body>...</body>", baseURL: .init(string: about:blank))

We can also load web archived data directly into a web page:

// Loading data
let page = WebPage()
let baseURL = URL(string. "about:blank")
let mimeType = "application/x-webarchive"
page.load(data, mimeType: mimeType, characterEncoding: .utf8, baseURL: baseURL)

You can also inspect any navigation made to a custom scheme, like mydata://... by using a custom URLSchemeHandler and injecting it into a web page configuration:

let scheme = URLScheme("mydata")
let handler = MyDataSchemeHandler()
var config = WebPage.Configuration()
config.urlSchemeHandlers[scheme] = handler
let page = WebPage(configuration: config)

You can also observe how the web page navigates, by using the new observations API:

func loadArticle() async {
    let id = page.load(URLRequest(url: ...))
    let events = Observations { page.currentNavigationEvent }
    for await event in events where event?.navigationID == id {
        switch event?.kind {
        case let .failed(error): currentError = error
        case finished: ...
        default: break
    }
}

You can inspect many page properties, like the page title, current URL, theme color, loading status and estimated progress, its user agent, navigation stack, media type, etc.

You can also call and evaluate JavaScript, using the web page’s callJavaScript(...) function:

let jsResult = try await page.callJavaScript(
    """
    const headers = document.querySelectorAll("h2")
    return [...headers].,ap((header) => ({
        "id": header.id,
        "title": header.textContent
    }))
    """
)
let result = jsResult as? [[String : Any]]

What This Means for WebViewKit

With these powerful, native features coming to SwiftUI, my project is no longer needed. I will freeze it, convert it to a public archive, and remove it sometimes in the future.

Discussions & More

If you found this interesting, please share your thoughts on Bluesky and Mastodon. Make sure to follow to be notified when new content is published.