Using WebKit to load web content in SwiftUI

4 min read––– views

Using WebKit to load web content in SwiftUI

In my previous blog post, I showed how to load web content offline in SwiftUI apps. The implementation was a bit tricky because it relied on WKWebView to load web content. On WWDC25, Apple introduced WebView and WebPage to simplify web content handling in SwiftUI. Surprisingly, these new APIs are already available starting with iOS 18.4. Let's try to use them.

Loading web content

Starting with a simple example:

import SwiftUI
import WebKit
 
struct ContentView: View {
    @State private var url: URL = URL(string: "https://www.artemnovichkov.com")!
 
    var body: some View {
        WebView(url: url)
    }
}

WebView supports direct URL loading, but lacks built-in loading indicators or error handling. Add WebPage to handle loading the content:

import SwiftUI
import WebKit
 
struct ContentView: View {
    @State private var url: URL = URL(string: "https://www.artemnovichkov.com")!
    @State private var webPage = WebPage()
 
    var body: some View {
        WebView(webPage)
            .onAppear {
                let request = URLRequest(url: url)
                webPage.load(request)
            }
            .onDisappear {
                webPage.stopLoading()
            }
    }
}

Additionally, WebPage provides loading state and estimated progress support:

import SwiftUI
import WebKit
 
struct ContentView: View {
    @State private var url: URL = URL(string: "https://www.artemnovichkov.com")!
    @State private var webPage = WebPage()
 
    var body: some View {
        content
            .onAppear {
                let request = URLRequest(url: url)
                webPage.load(request)
            }
            .onDisappear {
                webPage.stopLoading()
            }
    }
 
    private var content: some View {
        ZStack {
            Color.white
            if webPage.isLoading {
                ProgressView("Loading", value: webPage.estimatedProgress)
                    .padding()
            } else {
                WebView(webPage)
            }
        }
    }
}

Here is the result:

Web page loading

To handle different states such as errors or redirects, use the currentNavigationEvent property. Let's add controls to save content — starting with a picker for selecting content type:

enum ContentType: String, CaseIterable {
    case snapshot = "Snapshot"
    case pdf = "PDF"
    case webarchive = "Web Archive"
}

And add a picker to the content:

struct ContentView: View {
    // Other states
    @State private var contentType: ContentType = .snapshot
 
    var body: some View {
        content
            .safeAreaInset(edge: .bottom) {
                HStack {
                    Picker("Content Type", selection: $contentType) {
                        ForEach(ContentType.allCases, id: \.self) { contentType in
                            Text(contentType.rawValue)
                                .tag(contentType)
                        }
                    }
                    Spacer()
                    Button("Save") {
                        // Save content
                    }
                }
                .padding()
                .disabled(disabled)
            }
            // Other modifiers
    }
 
    private var disabled: Bool {
        switch webPage.currentNavigationEvent?.kind {
        case .finished:
            false
        default:
            true
        }
    }
}

The picker remains disabled until the web content finishes loading.

Saving web content locally

As before, we have 3 types of content:

  • snapshot;
  • PDF;
  • Web archive.

For snapshots, the web page returns optional Image:

Task {
    let image = try await webPage.snapshot()
}

You can’t save the image directly, as it lacks a data representation. Instead, we need to use ImageRenderer to convert the image to UIImage and then save the data to the file:

Task {
    let image = try await webPage.snapshot()
    if let image {
        let renderer = ImageRenderer(content: image)
        renderer.scale = 2
        if let uiImage = renderer.uiImage, let data = uiImage.pngData() {
            // Save data to the file
        }
    }
}

The snapshot includes only the visible portion of the WebView, scroll indicators included. For PDFs and web archives, we get data directly:

Task {
    let pdfData = try await webPage.pdf()
    // Save data to the file
}
 
Task {
    let webArchiveData = try await webPage.webArchiveData()
    // Save data to the file
}

Simple and straightforward.

Showing saved content

To display snapshots and PDF files, I reused code from the previous blog post. For web archives, the new WebView can be used directly:

import SwiftUI
import WebKit
 
struct WebArchiveView: View {
    
    let url: URL
    @State private var webPage: WebPage = .init()
    
    var body: some View {
        WebView(webPage)
            .onAppear {
                guard let data = FileManager.default.contents(atPath: url.path()) else {
                    print("Failed to load web archive data from \(url.path())")
                    return
                }
                let baseURL = URL(string: "about:blank")!
                webPage.load(data, mimeType: "application/x-webarchive", characterEncoding: .utf8, baseURL: baseURL)
            }
    }
}

Conclusion

New APIs simplify web content handling in SwiftUI. I didn't cover all the features, like url schemes or JavaScript calling, but you can find more details in the official documentation or WWDC video. The complete sample project is available on Github. Thanks for reading!

References