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.

WebViewKit

I created WebViewKit many years ago, to make it easy to embed web content into any SwiftUI-based app, using a custom WebView that works all the way back in iOS 14, macOS 11 & visionOS 1:

import SwiftUI
import WebViewKit

struct MyView {

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

This WebView lets you to load URLs and HTML content, and configure the underlying WKWebView with a configuration and viewConfiguration, to set up deeper integrations, navigation observation, etc.

For 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 native WebView that is easy to get started with, and that can be configured for more complex use-cases.

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")
        )
    }
}

We convert the URL to a @State property, to make it easy to change and inspect the current URL:

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 native tools coming to SwiftUI, my WebViewKit project is no longer needed, except for polyfill porposes. I will keep it alive, but it will most probably not change much 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.