Swift and SwiftUI tutorials for Swift Developers

How to use WebView and WebPage in SwiftUI

For a long time, when building SwiftUI apps, developers often needed to display web content inside the app — for example, a help page, a small embedded browser, or a hybrid web-native component. The only way to do that was to integrate WKWebView via UIKit using UIViewRepresentable, or to use SFSafariViewController. This involved a fair amount of boilerplate code and complexity.

With iOS 26, Apple introduced a native SwiftUI WebView, the WebView view (and its companion model WebPage), allowing us to embed web content directly, declaratively, and fully within SwiftUI — no UIKit bridge required.
This tutorial will guide you through:

  1. How to use the basic WebView.
  2. How to use WebPage for more advanced control.
  3. How to handle navigation, JavaScript, and local HTML.
  4. Best practices, caveats, and real-world examples.

We’ll assume you already have basic SwiftUI knowledge.

1. Basic use of WebView

The simplest case: loading a remote web page in SwiftUI for iOS 26 is now as easy as:

import SwiftUI
import WebKit

struct ContentView: View {
    var body: some View {
        WebView(url: URL(string: "https://www.apple.com")!)
    }
}

As Apple documentation and several tutorials note, “In iOS 26, SwiftUI finally introduces WebView, a native solution for displaying web content.” This means we no longer have to manually wrap a WKWebView using UIViewRepresentable.

Minimal template

struct MyWebView: View {
    let url: URL

    var body: some View {
        WebView(url: url)
    }
}

Usage:

MyWebView(url: URL(string: "https://my-site.com")!)

Initial advantages

  • Cleaner code, no UIKit bridge needed.
  • Uses the same WebKit engine that powers Safari.
  • Fewer potential bugs and less boilerplate.
  • Fully declarative SwiftUI syntax.

Notes

  • Make sure your URL is valid — handle nil safely.
  • Remote URLs may require proper ATS (App Transport Security) configuration.
  • Even though it’s “native,” it still relies on WebKit under the hood — keep performance and security in mind.

2. Using WebPage for advanced control

If you need deeper control — for instance, getting the page title, tracking load progress, reloading, running JavaScript, or managing navigation — iOS 26 provides the WebPage model.

import SwiftUI
import WebKit

struct AdvancedWebView: View {
    @State private var page = WebPage()

    var body: some View {
        VStack {
            WebView(page)
                .onAppear {
                    if let url = URL(string: "https://www.apple.com") {
                        let request = URLRequest(url: url)
                        page.load(request)
                    }
                }

            Text("Title: \(page.title ?? "-")")
            Text("Current URL: \(String(describing: page.url))")
            Text("Progress: \(page.estimatedProgress.formatted(.percent.precision(.fractionLength(0))))")

            HStack {
                Button("Reload") { page.reload() }
                Button("Stop") { page.stopLoading() }
            }
        }
    }
}

Explanation

  • WebPage() creates an object representing the current page’s state.
  • WebView(page) binds the view to that state.
  • You can access titleurl, and estimatedProgress for display.
  • Call load(_:)reload(), and stopLoading() for control.
  • You can also load HTML directly:
page.load(html: "<h1>Hello from SwiftUI</h1>", baseURL: URL(string:"about:blank")!)

When to use WebPage instead of a simple URL

  • You need the page title or loading progress.
  • You want back/forward navigation buttons.
  • You want to run custom JavaScript.
  • You’re building a mini-browser or hybrid component, not just a static webpage view.

3. Practical examples

Example A: Simple mini-browser

struct MiniBrowserView: View {
    @State private var page = WebPage()

    var body: some View {
        VStack {
            HStack {
                Button(action: { Task { try? await page.callJavaScript("history.back()") } }) {
                    Image(systemName: "chevron.backward")
                }
                Button(action: { page.reload() }) {
                    Image(systemName: "arrow.clockwise")
                }
                Spacer()
                if page.isLoading {
                    ProgressView()
                }
            }.padding()

            WebView(page)
                .ignoresSafeArea(.bottom)
        }
        .onAppear {
            if let url = URL(string: "https://www.apple.com") {
                page.load(URLRequest(url: url))
            }
        }
    }
}

Details:

  • callJavaScript("history.back()") navigates back.
  • Manual reload and progress display.
  • ignoresSafeArea(.bottom) for fullscreen layout.

Example B: Load local HTML

struct LocalHtmlView: View {
    @State private var page = WebPage()
    let htmlContent = """
        <html><body><h1>Welcome</h1><p>This is local HTML inside the app.</p></body></html>
        """

    var body: some View {
        WebView(page)
            .onAppear {
                page.load(html: htmlContent, baseURL: URL(string: "about:blank")!)
            }
    }
}

Useful for offline help pages, documentation, or static templates.

Example C: Intercepting external links

If your embedded website includes links you’d like to open externally, use .onChange with the page’s URL:

struct LinkHandlerView: View {
    @State private var page = WebPage()

    var body: some View {
        WebView(page)
            .onChange(of: page.url) { newURL in
                if let u = newURL, u.host == "external.example.com" {
                    UIApplication.shared.open(u)
                    page.stopLoading()
                }
            }
            .onAppear {
                if let url = URL(string: "https://your-site.com") {
                    page.load(URLRequest(url: url))
                }
            }
    }
}

This lets you intercept redirects or external URLs safely.


4. Best practices and considerations

Performance

  • Avoid loading unnecessarily heavy or script-intensive sites.
  • Use local HTML for static content.
  • Be mindful of caching, cookies, and local storage.
  • SwiftUI’s async rendering helps, but test for smooth scrolling and input.

Security

  • Only load trusted HTTPS URLs.
  • If you execute JavaScript, avoid injection vulnerabilities.
  • Be aware of mixed or third-party content and user privacy policies.

Navigation

  • Unlike traditional WKWebView, the new WebView doesn’t include built-in back/forward buttons — you must implement them manually.
  • Some developers note that back navigation isn’t always intuitive — callJavaScript("history.back()") is a common workaround.
  • For complex navigation, expose explicit controls to users.

Compatibility and fallback

If you support earlier iOS versions (pre-iOS 26), use a conditional fallback:

if #available(iOS 26, *) {
    WebView(url: myURL)
} else {
    LegacyWebView(url: myURL)
}

Where LegacyWebView wraps a WKWebView with UIViewRepresentable.

UI/UX

  • Add a ProgressView() for loading feedback.
  • Use navigationTitle(page.title ?? "") within a NavigationStack.
  • Handle errors gracefully (“Failed to load page”).
  • Consider .ignoresSafeArea() for fullscreen views.
  • Test across iPhone and iPad layouts.

External libraries

If you need even more control, third-party wrappers like WebViewKit offer advanced configuration, but for iOS 26 the native solution is recommended.


5. Full case study: Help page browser

Here’s a complete example with title, back, reload, loading indicator, and external link handling:

import SwiftUI
import WebKit

struct HelpWebView: View {
    @State private var page = WebPage()
    @State private var showExternalLinkAlert = false
    @State private var externalURL: URL?

    var body: some View {
        NavigationStack {
            VStack(spacing: 0) {
                HStack {
                    Button(action: { Task { try? await page.callJavaScript("history.back()") } }) {
                        Image(systemName: "chevron.backward")
                    }.disabled(page.canGoBack == false)

                    Button(action: { page.reload() }) {
                        Image(systemName: "arrow.clockwise")
                    }

                    Spacer()

                    if page.isLoading {
                        ProgressView()
                    }
                }.padding()

                WebView(page)
                    .onChange(of: page.url) { newURL in
                        guard let u = newURL else { return }
                        if u.host == "external.myhelp.com" {
                            externalURL = u
                            showExternalLinkAlert = true
                        }
                    }
            }
            .navigationTitle(page.title ?? "Help")
            .navigationBarTitleDisplayMode(.inline)
            .onAppear {
                if let url = URL(string: "https://help.myapp.com") {
                    page.load(URLRequest(url: url))
                }
            }
            .alert("Open external link?", isPresented: $showExternalLinkAlert) {
                Button("Yes", role: .destructive) {
                    if let u = externalURL {
                        UIApplication.shared.open(u)
                    }
                }
                Button("No", role: .cancel) {
                    externalURL = nil
                }
            } message: {
                Text(externalURL?.absoluteString ?? "")
            }
        }
    }
}

Notes

  • We assume page.canGoBack exists or is computed via Combine.
  • NavigationStack provides a proper title bar.
  • Intercepts external URLs and confirms via alert.
  • Includes a reload button and progress indicator.

This is a reusable component for any in-app help or documentation section.


6. Comparison with the old UIViewRepresentable approach

Before iOS 26, you had to do this:

struct LegacyWebView: UIViewRepresentable {
    let url: URL

    func makeUIView(context: Context) -> WKWebView {
        WKWebView()
    }

    func updateUIView(_ uiView: WKWebView, context: Context) {
        uiView.load(URLRequest(url: url))
    }
}

Then use it in SwiftUI.
That worked, but it came with downsides: navigation delegates, extra boilerplate, less declarative syntax.

With iOS 26’s native WebView, all that is gone — the result is cleaner, faster, and easier to maintain.


7. Current limitations and notes

  • Some developers have reported layout bugs when using .ignoresSafeArea() with the new WebView on iOS 26.
  • Back/forward navigation (like goBack() and goForward()) may still require JavaScript calls.
  • For iOS < 26 support, you still need the legacy wrapper.
  • If you need deep WKWebViewConfiguration control (e.g., user scripts, custom user agent), you might still rely on UIKit integration.
  • Heavy sites can still cause performance or memory issues on older devices.

8. Conclusion

With iOS 26, embedding web content in SwiftUI apps has become effortless.
The new WebView and WebPage types make the process clean, declarative, and native.

✅ For simple cases: use WebView(url:).
🚀 For full control: use WebPage() + WebView(page).

Keep in mind performance, security, and backward compatibility — and test across devices.

With these examples and best practices, you’re now ready to integrate native SwiftUI WebViews in iOS 26 confidently.

If you have any questions about this article, please contact me and I will be happy to help you 🙂. You can contact me on my X profile or on my Instagram profile.

Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Article

How to create multiline TextField in SwiftUI

Next Article

How to add a Splash Screen to a SwiftUI iOS app

Related Posts