Logo for tanaschita.com

How to present a local HTML file in SwiftUI

Learn how to show a locally stored HTML file with WebView and WKWebView

04 May 2026 · 7 min read

When developing iOS apps, we sometimes need to display a locally stored HTML file, for example static help pages, terms and conditions, or release notes bundled with the app. For a long time, the only way to do this in SwiftUI was to wrap Apple's WKWebView using UIViewRepresentable. Starting with iOS 26, Apple introduced a native WebView for SwiftUI.

In this article, we'll look at both approaches:

iOS 26+: Using the native WebView

At WWDC 2025, Apple introduced a native WebView for SwiftUI. To use it, import WebKit alongside SwiftUI.

Loading a local file directly

The simplest way to show a local HTML file is to resolve its bundle URL and pass it straight to WebView:

import SwiftUI
import WebKit
struct HTMLView: View {
let fileName: String
var body: some View {
if let url = Bundle.main.url(forResource: fileName, withExtension: "html") {
WebView(url: url)
}
}
}

This works well for simple HTML files. If the file references additional local resources such as stylesheets or images, using WebPage provides more control over how the content is loaded.

Using WebPage for more control

For anything beyond a static display, reading the page title, tracking load progress, or modifying the HTML before showing it, we can use the WebPage class. WebPage is an Observable object that represents and controls the web content:

import SwiftUI
import WebKit
struct HTMLView: View {
let fileName: String
@State private var page = WebPage()
var body: some View {
WebView(page)
.onAppear {
guard let url = Bundle.main.url(
forResource: fileName,
withExtension: "html"
) else { return }
page.load(URLRequest(url: url))
}
}
}

Because WebPage conforms to Observable, we can read its properties directly in our view. For example, showing a loading indicator while the page loads:

var body: some View {
VStack {
if page.isLoading {
ProgressView()
}
WebView(page)
}
.onAppear {
guard let url = Bundle.main.url(
forResource: fileName,
withExtension: "html"
) else { return }
page.load(URLRequest(url: url))
}
}

Loading local assets

If our HTML file references local resources such as a stylesheet or images, we can load the HTML as a string and pass Bundle.main.resourceURL as the baseURL. This tells the web view where to resolve relative paths, so local assets load correctly:

.onAppear {
guard let url = Bundle.main.url(
forResource: fileName,
withExtension: "html"
) else { return }
guard let htmlString = try? String(contentsOf: url, encoding: .utf8) else { return }
page.load(html: htmlString, baseURL: Bundle.main.resourceURL)
}

If we need to process the HTML string programmatically before showing it, we can use the same approach.

Showing the view in a sheet

And here's an example on how we can show the view in a sheet:

@State private var isTermsSheetPresented = false
@State private var page = WebPage()
var body: some View {
Button("Terms & Conditions") {
isTermsSheetPresented.toggle()
}
.sheet(isPresented: $isTermsSheetPresented) {
NavigationStack {
WebView(page)
.navigationTitle(page.title)
.navigationBarTitleDisplayMode(.inline)
.onAppear {
guard let url = Bundle.main.url(forResource: "terms", withExtension: "html") else { return }
page.load(URLRequest(url: url))
}
}
}
}

iOS 14+: Using WKWebView with UIViewRepresentable

If the app targets iOS versions earlier than iOS 26, we'll need to use WKWebView wrapped in a UIViewRepresentable.

Creating the wrapper

Since WKWebView is a UIView subclass, we first create a SwiftUI-compatible wrapper:

import SwiftUI
import WebKit
struct LegacyHTMLView: UIViewRepresentable {
let fileName: String
func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}
func updateUIView(_ webView: WKWebView, context: Context) {
guard let url = Bundle.main.url(forResource: fileName, withExtension: "html") else { return }
webView.load(URLRequest(url: url))
}
}

Loading local assets

If the HTML file references local assets such as a stylesheet or images, we can use loadFileURL(_:allowingReadAccessTo:) instead of load(_:). This grants the web view read access to the parent directory so relative paths resolve correctly:

func updateUIView(_ webView: WKWebView, context: Context) {
guard let url = Bundle.main.url(forResource: fileName, withExtension: "html") else { return }
webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent())
}

Modifying the HTML before displaying it

If we need to modify the HTML content programmatically before showing it, we can use loadHTMLString(_:baseURL:). Passing the file's URL as the baseURL ensures that relative references still work:

func updateUIView(_ webView: WKWebView, context: Context) {
guard let url = Bundle.main.url(
forResource: fileName,
withExtension: "html"
) else { return }
guard var htmlString = try? String(contentsOf: url, encoding: .utf8) else { return }
// Modify the HTML string before displaying
htmlString = htmlString.replacingOccurrences(of: "{{version}}", with: "2.0")
webView.loadHTMLString(htmlString, baseURL: url)
}

Using the view in a sheet

And this is how we can use the view in a sheet:

@State private var isTermsSheetPresented = false
var body: some View {
Button("Terms & Conditions") {
isTermsSheetPresented.toggle()
}
.sheet(isPresented: $isTermsSheetPresented) {
LegacyHTMLView(fileName: "terms")
}
}

Bringing it together

Displaying local HTML in SwiftUI can be implemented in different ways depending on the deployment target.

For newer iOS versions, the native WebView provides a clean and fully SwiftUI-based solution. For broader compatibility, WKWebView wrapped in UIViewRepresentable remains a reliable approach.

We can use a conditional check to get the best of both:

var body: some View {
if #available(iOS 26, *) {
HTMLView(fileName: fileName)
} else {
LegacyHTMLView(fileName: fileName)
}
}

Both approaches allow us to display bundled HTML files and handle more advanced use cases such as modifying content or loading local resources.

Sponsorship logo
Preparing for a technical iOS job interview
Preparing for a technical iOS Job Interview with over 300 questions & answers. Covering Swift & Objective-C, SwiftUI & UIKit, Combine, HTTP Networking, iOS File System, Core Data, Concurrency with async/await, Security, Automated Testing, Dependency Management, AI & Machine Learning and more.
LEARN MORE
Sponsorship logo
Architecture & Design Patterns for iOS
This book is a practical guide to essential architectural principles and design patterns for iOS development. It covers strategies for building maintainable apps with Swift and SwiftUI, including dependency injection, navigation, common patterns, and modularization.
LEARN MORE
Sponsorship logo
Become a sponsor of tanaschita.com
By publishing an article on different iOS topics every week, tanaschita.com is constantly growing in the developer community and may provide a great audience for you as a sponsor.
CLICK TO LEARN MORE

Newsletter

Image of a reading marmot
Subscribe

Like to support my work?

Say hi

Related tags

Articles with related topics

webkit

swiftui

ios

Using WebKit with SwiftUI to manage web content

Get an overview on how to use WebView and WebPage in SwiftUI.

23 Jun 2025 · 3 min read

Latest articles and tips

© 2026 tanaschita.com

Privacy policy

Impressum