How to Load a SwiftUI WebView with WKWebView

You oftentimes want to show web content within an app. For example, to show terms and conditions or show a tutorial site for more help

Unfortunately, there is no SwiftUI WebView component. However, SwiftUI offers a Link button that will open the provided URL in the Safari browser. If you want to keep your users inside the app, you can use WKWebView from UIKit/Appkit and integrate it with SwiftUI using UIViewRepresentable and NSViewRepresentable.

In this blog post, we’ll delve into how to create a WebView in SwiftUI.

⬇️ download the project from Github https://github.com/gahntpo/Web…

Opening a web page in Safari with SwiftUI Link view

In SwiftUI, the Link view provides an interactive element that opens a URL in Safari when clicked. Here’s how you can use it:

import SwiftUI

struct ContentView: View {
    var body: some View {
         Link("Visit SwiftyPlace",
               destination: URL(string: "https://www.swiftyplace.com")!)
    }
}

The user can go back to your app by tapping your app name in the top left edge. Your user will leave the app. This is not the best experience.

SwiftUI example with Link button to open an url in safari

What is the WebKit framework?

Apple’s WebKit framework is a versatile tool, powering not only Safari, but also extending its capabilities to developers. Central to this is WebView, provided by WebKit’s WKWebView class. This allows developers to display a variety of web content directly within their apps and interact with JavaScript from Swift or Objective-C. While not all web technologies found in a full desktop browser are supported, it opens up myriad possibilities for integrating interactive web content into your apps. Let’s explore how to leverage these tools for your SwiftUI apps.

How to write a simple SwiftUI WebView SwiftUI component?

Once you’ve set up your project, it’s time to integrate WKWebView with SwiftUI. Let’s create a new SwiftUI View that represents a web link:

import SwiftUI
import WebKit

struct WebView: UIViewRepresentable {
    let url: URL
    
    func makeUIView(context: Context) -> WKWebView  {
        let wkwebView = WKWebView()
        let request = URLRequest(url: url)
        wkwebView.load(request)
        return wkwebView
    }
    
    func updateUIView(_ uiView: WKWebView, context: Context) {
    }
}

In the above code, I declare a struct called WebView that conforms to the UIViewRepresentable protocol. This protocol lets you create a SwiftUI view based on a UIKit view.

The makeUIView(:) function is where we create an instance of WKWebView. It creates a URL request, and load the website.

The updateUIView(:) function is required by the UIViewRepresentable protocol and is used to update the UIView as needed. In this case, we don’t need to do anything when the view updates, so we leave the function empty.

Free SwiftUI Layout Book

Get Hands-on with SwiftUI Layout.

Master SwiftUI layouts with real-world examples and step-by-step guide. Building Complex and Responsive Interfaces.

Using the WebView

Now we can use the WebView in our SwiftUI views. In your ContentView, you can use the WebView like this:

import SwiftUI
struct ContentView: View {
    var body: some View {
        WebView(url: URL(string: "https://www.swiftyplace.com")!)
            .edgesIgnoringSafeArea(.all)
    }
}

In the ContentView, I created a WebView that loads “https://www.swiftyplace.com“. I use edgesIgnoringSafeArea(.all)to make the WebView fill the entire screen.

SwiftUI webview that fills the entire screen

The content in the web view is scrollable by default. The webview will expand to fill in the evaluable space. If you want to use a web view inside a scroll view, you have to add a frame to set a specific size. Otherwise, the web view will shrink to its minimum height which is zero. In the following, you can see a basic example.

import SwiftUI

struct ContentView: View {
    var body: some View {
      ScrollView {
        WebView(url: URL(string: "https://www.swiftyplace.com")!)
          .frame(height: 300)
      }
    }
}

Adding an Activity Indicator to a WebView in SwiftUI

A common requirement when loading a webpage in a WebView is to display an activity indicator while the page is loading. This provides a clear visual cue to the user that something is happening. Let’s walk through how to implement this in SwiftUI.

Updating the WebView

You need to modify our WebView to show an activity indicator while loading. We’ll add a Coordinator class to handle the WKNavigationDelegate callbacks, specifically webView(_:didStartProvisionalNavigation:) and webView(_:didFinish:). We’ll also add a @State property isLoading to track whether the page is loading.

import SwiftUI
import WebKit

struct WebView: UIViewRepresentable {
    let url: URL
    @Binding var isLoading: Bool
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    func makeUIView(context: Context) -> WKWebView  {
        let wkwebView = WKWebView()
        wkwebView = context.coordinator
        wkwebView.load(URLRequest(url: url))
        return wkwebView
    }
    func updateUIView(_ uiView: WKWebView, context: Context) {
    }

    class Coordinator: NSObject, WKNavigationDelegate {
        var parent: WebView
        init(_ parent: WebView) {
            self.parent = parent
        }
        func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
            parent.isLoading = true
        }
        func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
            parent.isLoading = false
        }
    }
}

In the Coordinator, we set isLoading to true when the page starts loading and set it to false when it finishes loading.

Using the WebView with Loading Indicator

Finally, let’s create a new reusable component in SwiftUI ´LoadingWebView´ to use the WebView and show the ProgressView while the page is loading:

import SwiftUI

struct LoadingWebView: View {

    @State private var isLoading = true
    let url: URL?
    
    var body: some View {
        ZStack {
          if let url = url {
            WebView(url: url, isLoading: $isLoading)
                .edgesIgnoringSafeArea(.all)
            if isLoading {
                ProgressView()
            }
          }
        }
    }
}

The ZStack allows us to overlay the ProgressView on top of the web view. The ProgressView is only visible when isLoading is true.

How to show an error message when the web view cannot be opened?

If an invalid url is used, the current implementation will keep on showing the loading indicator. Instead, I want to get the error from the navigation delegate and show it to the user. First I have to add a binding for an error value:

struct WebView: UIViewRepresentable {
    let url: URL
    @Binding var isLoading: Bool
    @Binding var error: Error?
    ...
}

That I am going to update from within the web view coordinator and use the delegate callback webview(didFailProvisionalNavigation:, withError) to update the isLoading to false and set the error:

struct WebView: UIViewRepresentable {
    ...
    class Coordinator: NSObject, WKNavigationDelegate {
        ...
        func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
            parent.isLoading = true
        }
        func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
            parent.isLoading = false
        }
        func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
            print("loading error: \(error)")
            parent.isLoading = false
            parent.error = error
        }
    }
}

Now I finally have all the information to make an advanced webview in Swiftui:

struct LoadingWebView: View {
    @State private var isLoading = true
    @State private var error: Error? = nil
    let url: URL?

    var body: some View {
        ZStack {
            if let error = error {
                Text(error.localizedDescription)
                    .foregroundColor(.pink)
            } else if let url = url {
                PlatformIndependentWebView(url: url,
                                           isLoading: $isLoading,
                                           error: $error)
                     .edgesIgnoringSafeArea(.all)
                if isLoading {
                    ProgressView()
                }
            } else {
                Text("Sorry, we could not load this url.")
            }

        }
    }
}
Swiftui example with a webview that has a loading state and error state

How to open a WebView on macOS?

If you tried to use the current version of web view on macOS, you would get an error saying “cannot find UIViewRepresentable”. On macOS you have to use NSViewRepresentable instead. Because I don’t want to copy past the same implementation from the iOS app, I am going to add this protocol conformance conditionally. First, I am making web view more generic by adding NSViewRepresantable and UIViewRepresentable in extensions:

#if os(macOS)
extension WebView: NSViewRepresentable {
    func makeNSView(context: Context) -> WKWebView {
        makeWebView(context: context)
    }
    func updateNSView(_ nsView: WKWebView, context: Context) {

    }
}
#else
extension WebView: UIViewRepresentable {
     func makeUIView(context: Context) -> WKWebView {
         makeWebView(context: context)
     }

     func updateUIView(_ uiView: WKWebView, context: Context) {
     }
}
#endif

The shared code that I use to set up the web view in makeUIView/makeNSView is now separate:

struct WebView {
    var url: URL
    @Binding var isLoading: Bool
    @Binding var error: Error?

    func makeCoordinator() -> WebView.Coordinator {
         Coordinator(self)
     }

    func makeWebView(context: Context) -> WKWebView {
        let webView = WKWebView()
        webView.navigationDelegate = context.coordinator
        let request = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad)
        webView.load(request)
        return webView
    }

    class Coordinator : NSObject, WKNavigationDelegate {
        var parent: PlatformIndependentWebView

        init(_ uiWebView: PlatformIndependentWebView) {
            self.parent = uiWebView
        }

        ...
    }
}
Free SwiftUI Layout Book

Get Hands-on with SwiftUI Layout.

Master SwiftUI layouts with real-world examples and step-by-step guide. Building Complex and Responsive Interfaces.

Don’t forget to change your app’s requirements to allow network requests otherwise, your web view will not be able to work.

macOS sandbox settings to allow loading a web page.
MacOS sandbox settings to allow loading a web page.

SwiftUI WebView Example: Showing Web View inside a Sheet

As an example, I want to show a web view inside a sheet. The following is a simple example:

struct SheetWebView: View {
    @State private var isSheetPresented = false
    @State private var isLoading = true
    let url = URL(string: "https://www.swiftyplace.com")

    var body: some View {
        Button(action: {
            isSheetPresented = true
        }) {
            Text("Open Web Page")
        }
        .sheet(isPresented: $isSheetPresented) {
            VStack(spacing: 0) {
                #if os(macOS)
                HStack {
                    Text(url?.absoluteString ?? "")
                    Spacer()
                    Button {
                        isSheetPresented.toggle()
                    } label: {
                        Label("Close", systemImage: "xmark.circle")
                            .labelStyle(.iconOnly)
                    }
                }
                .padding(10)
                #endif
                LoadingWebView(url: url)
                    .frame(minWidth: 300, minHeight: 300)
            }
        }
    }
}

On iOS, you can dismiss the sheet by swiping it down. But on macOS, I need to provide a button that dismisses the sheet. Additionally on macOS, when the sheet opens it only shows the progress view and is very small. When the website is loaded, it is shown in the same small sheet. Therefore I added a frame of 300 by 300. I used min values to allow the user to drag the sheet larger. You can learn more about sheet in SwiftUI in this blog post.

swiftui webviel on macOS

Making an in-app browser

If you want to create an in-app browser, you need to deal with more than just displaying a WebView. You’ll also want to add controls for navigation such as back, forward, reload, and possibly an address bar. Here is a simplified example of how you might create an in-app browser with SwiftUI:

struct BrowserView: View {

    @StateObject var browserViewModel = BrowserViewModel()
    
    var body: some View {
        VStack {
            HStack {
                Button(action: {
                    browserViewModel.goBack()
                }) {
                    Image(systemName: "chevron.backward")
                }
                .disabled(!browserViewModel.canGoBack)

                Button(action: {
                    browserViewModel.goForward()
                }) {
                    Image(systemName: "chevron.forward")
                }
                .disabled(!browserViewModel.canGoForward)

                .padding(.trailing, 5)

                TextField("URL", text: $browserViewModel.urlString, onCommit: {
                     browserViewModel.loadURLString()
                 })
                 .textFieldStyle(RoundedBorderTextFieldStyle())

                Button(action: {
                    browserViewModel.reload()
                }) {
                    Image(systemName: "arrow.clockwise")
                }
            }
            .padding(.horizontal)

            if let url =  URL(string: browserViewModel.urlString) {
                BrowserWebView(url: url,
                               viewModel: browserViewModel)
                .edgesIgnoringSafeArea(.all)
            } else {
                Text("Please, enter a url.")
            }
        }
    }
}

The textfield shows the current URL string and updates correctly when the user navigates to another web page.

The SwiftUI wrapper around WKWebView is very simple:

struct BrowserWebView: UIViewRepresentable {
    let url: URL
    @ObservedObject var viewModel: BrowserViewModel
    func makeUIView(context: Context) -> WKWebView {
        let webView = WKWebView()
        viewModel.webView = webView
        webView.load(URLRequest(url: url))
        return webView
    }
    func updateUIView(_ uiView: WKWebView, context: Context) {
    }
}

The handling of forward and backward actions is done by the view model:

import Foundation
import WebKit

class BrowserViewModel: NSObject, ObservableObject {
    weak var webView: WKWebView? {
        didSet {
            webView?.navigationDelegate = self
        }
    }

    @Published var urlString = "https://www.apple.com"
    @Published var canGoBack = false
    @Published var canGoForward = false

    func loadURLString() {
        if let url = URL(string: urlString) {
            webView?.load(URLRequest(url: url))
        }
    }

    func goBack() {
        webView?.goBack()
    }

    func goForward() {
        webView?.goForward()
    }

    func reload() {
        webView?.reload()
    }
}
Swiftui example to use WKWebview to build an app for web browsing.
You can use WKWebview to build an app for web browsing.

Conclusion

SwiftUI doesn’t directly support WebView, but by leveraging the WebKit framework, we can create a WebView within the SwiftUI app. This post explored the step-by-step process of creating a WebView in SwiftUI, handling errors, displaying a loading indicator, and implementing web browsers. We also discussed how to make a cross-platform WebView for iOS and macOS. This comprehensive guide should be helpful for developers looking to embed web content directly into their SwiftUI apps, providing a seamless user experience.

FAQ

Does SwiftUI have WebView?

SwiftUI native views do not have a WebView. However, you can use the WKWebView from WebKit to display web content in a SwiftUI app by using the UIViewRepresentable protocol to create a SwiftUI view that wraps the WKWebView.

Is WKWebView same as Safari?

WKWebView is not exactly the same as Safari. WKWebView is a component provided by WebKit which underlies Safari. It allows developers to embed web content in their applications. Safari is a full-featured web browser that uses WebKit to render web pages, but it also includes additional features like bookmarks, history, and sharing features that are not part of WKWebView.

What is the usage of WKWebView?

WKWebView is used to display web content within a native app. It can load and display a wide variety of web content, including HTML, CSS, SVG, images, and JavaScript. It also allows developers to interact with JavaScript code, and manage navigation and loading resources, enabling the creation of custom, in-app browsers or the display of certain web pages without leaving the app.

How do I open web view in iOS?

To open a WebView in iOS, you typically use the WKWebView class from the WebKit framework. After making an instance of WKWebView, you can load web content by creating a URLRequest and calling the load() method. If you’re using SwiftUI, you’ll need to wrap the WKWebView in a SwiftUI view using the UIViewRepresentable protocol.

5 thoughts on “How to Load a SwiftUI WebView with WKWebView”

  1. I do not even understand how I finished up here, however I thought this
    submit was once great. I do not realize who you might be
    but definitely you are going to a well-known blogger for those who are not
    already. Cheers!

    Reply
  2. I am now not sure where you are getting your information,
    but good topic. I must spend some time learning much more or working out more.
    Thanks for fantastic information I used to be on the lookout for this info for my mission.

    Reply

Leave a Comment

Subscribe to My Newsletter

Want the latest iOS development trends and insights delivered to your inbox? Subscribe to our newsletter now!

Newsletter Form

New Course Announcement!!!

50% OFF Launch Sale

MacOS Development with SwiftUI