Using WebKit to load web content in SwiftUI
4 min read • ––– views

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:
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!