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+: the native WebView and WebPage from WebKit for SwiftUI
- iOS 14+: the classic WKWebView + UIViewRepresentable approach for broader compatibility
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 SwiftUIimport WebKitstruct HTMLView: View {let fileName: Stringvar 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 SwiftUIimport WebKitstruct 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 SwiftUIimport WebKitstruct LegacyHTMLView: UIViewRepresentable {let fileName: Stringfunc 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 displayinghtmlString = 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 = falsevar 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.



Newsletter
Like to support my work?
Say hi
Related tags
Articles with related topics
Latest articles and tips



