Try to display a slightly rich Webview in Swift UI using UIViewRepresentable

Introduction

As of iOS14, SwiftUI does not currently support View equivalent to WKWebView. Therefore, it is necessary to wrap and use WKWebView of UIKit for SwiftUI. This time, the goal is to display a WebView equipped with basic functions (toolbar, progress bar), and I would like to summarize how to use ʻUIViewRepresentable` that appears in it.

About UIViewRepresentable

To use UIKit's View in Swift UI, you need to use ʻUIViewRepresentable. ʻUIViewRepresentable is a wrapper for using UIKit View in SwiftUI. Describes each function defined in the ʻUIViewRepresentable` protocol.

func makeUIView(context: Self.Context) -> Self.UIViewType

Implementation required. Create an instance of the View to display. The View of UIKit used in SwiftUI is returned as a return value.

func updateUIView(Self.UIViewType, context: Self.Context)

Implementation required. Called when the state of the app is updated. If there is an update of View, describe it in this function.

static func dismantleUIView(_ uiView: Self.UIViewType, coordinator: Self.Coordinator)

Called when the specified UIKit View is deleted. Describe the cleanup process in this function, such as deleting the registered notification if necessary.

func makeCoordinator() -> Self.Coordinator

Implement when there is an event to be notified from the View side. By defining Coordinator, it becomes possible to perform event handling by user operation such as Delegate.

Create a WebView using WKWebView

Now let's get into the main subject of WKWebView display.

Display WKWebView

The first is to simply display the WebView in Swift UI. ʻUIViewType will be ʻassociatedtype, so change it to the View type of the UIKit you want to wrap. This time, I will use WKWebView.

WebView.swift


struct WebView: UIViewRepresentable {
    ///View to display
    private let webView = WKWebView()
    ///URL to display
    let url: URL

    func makeUIView(context: Context) -> WKWebView {
        //Set the return value to WKWebView and return it
        webView.load(URLRequest(url: url))
        return webView
    }

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

Create a toolbar

Next, we will create a toolbar with three elements: back`` forward reload.

Controlling actions when tapping a button

First, create a toolbar and place each button. Next, use Property Wrappers to enable WebView to detect when each button is tapped on the toolbar. If you tap the button on the toolbar in this state, the state of the application will be updated and ʻupdateUIView (_ uiView :, context :) of ʻUIViewRepresentable will be fired. From the above, on the WebView side, describe the action according to the button in ʻupdateUIView (_ uiView :, context :)`.

WebView.swift


struct WebView: UIViewRepresentable {
    //abridgement...

    ///WebView actions
    enum Action {
        case none
        case goBack
        case goForward
        case reload
    }

    ///action
    @Binding var action: Action

    func updateUIView(_ uiView: WKWebView, context: Context) {
        ///Called every time the bound value is updated
        ///When action is updated, process according to the updated value
        switch action {
        case .goBack:
            uiView.goBack()
        case .goForward:
            uiView.goForward()
        case .reload:
            uiView.reload()
        case .none:
            break
        }
        action = .none
    }
}

WebToolBarView.swift


struct WebToolBarView: View {
    ///action
    @Binding var action: WebView.Action

    var body: some View {
        VStack() {
            HStack() {
                //Update actions according to the button you tap
                Button("Back") { action = .goBack }
                Button("Forward") { action = .goForward }
                Button("Reload") { action = .reload }
            }
        }
    }
}

RichWebView.swift


struct RichWebView: View {
    /// URL
    let url: URL
    ///action
    @State private var action: WebView.Action = .none

    var body: some View {
        VStack() {
            WebView(url: url,
                    action: $action)
            WebToolBarView(action: $action)
        }
    }
}

Button deactivation

Now it is possible to process when the button is tapped in WebView. However, the buttons can be tapped in any state, so if you can not move to the previous or next page, deactivate each button.

** Define Coordinator ** Whether or not you can move to the previous or next page is determined when the WebView page load is completed. To do this, you need to implement WKNavigationDelegate, but you cannot implement it directly in WebView. So in ʻUIViewRepresentable, you need to create a ** Coordinator, a custom instance ** to tell SwiftUI the changes received from the ** UIKit View. WKNavigationDelegate is implemented in Coordinator, and event handling on the Swift UI side is performed through it.

WebView.swift


struct WebView: UIViewRepresentable {
    //abridgement... 

    ///Can i go back
    @Binding var canGoBack: Bool
    ///Do you want to proceed
    @Binding var canGoForward: Bool

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

extension WebView {
    final class Coordinator: NSObject, WKNavigationDelegate {
        ///Parent View
        let parent: WebView

        init(parent: WebView) {
            self.parent = parent
        }

        func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
            parent.canGoBack = webView.canGoBack
            parent.canGoForward = webView.canGoForward
        }
    }
}

After that, receive each value with WebToolBarView via RichWebView, and describe the inactivity and activation processing of each button, and you're done.

WebToolBarView.swift


    Button("Back") { action = .goBack }.disabled(!canGoBack)
    Button("Forward") { action = .goForward }.disabled(!canGoForward)

Create a progress bar

Finally, let's create a progress bar.

Get the value with KVO

You need to get ʻestimated Progress and ʻis Loading of WKWebView to create a progress bar. This time, we will use KVO for each. You will receive a change notification from View, so use the Coordinator you used earlier.

WebView.swift


struct WebView: UIViewRepresentable {
    //abridgement... 

     ///Loading progress
    @Binding var estimatedProgress: Double
    ///Whether loading
    @Binding var isLoading: Bool

    static func dismantleUIView(_ uiView: WKWebView, coordinator: Coordinator) {
        //Called when WKWebView is deleted
        //Disable and delete notifications when the instance is deleted
        coordinator.observations.forEach({ $0.invalidate() })
        coordinator.observations.removeAll()
    }
}

extension WebView {
    final class Coordinator: NSObject, WKNavigationDelegate {
        ///Parent View
        let parent: WebView
        /// NSKeyValueObservations
        var observations: [NSKeyValueObservation] = []

        init(parent: WebView) {
            self.parent = parent
            //Register notifications
            let progressObservation = parent.webView.observe(\.estimatedProgress, options: .new, changeHandler: { _, value in
                parent.estimatedProgress = value.newValue ?? 0
            })
            let isLoadingObservation = parent.webView.observe(\.isLoading, options: .new, changeHandler: { _, value in
                parent.isLoading = value.newValue ?? false
            })
            observations = [
                progressObservation,
                isLoadingObservation
            ]
        }
        //abridgement...
    }
}
```

#### Create a progress bar
 Now you have all the parts you need for a progress bar.
 All you have to do is create `ProgressBarView.swift`, receive the value via` RichWebView` and display it.


#### **`ProgressBarView.swift`**
```swift

struct ProgressBarView: View {
    ///Loading progress
    var estimatedProgress: Double

    var body: some View {
        VStack {
            GeometryReader { geometry in
                Rectangle()
                    .foregroundColor(Color.gray)
                    .opacity(0.3)
                    .frame(width: geometry.size.width)
                Rectangle()
                    .foregroundColor(Color.blue)
                    .frame(width: geometry.size.width * CGFloat(estimatedProgress))
            }
        }.frame(height: 3.0)
    }
}

RichWebView.swift


struct RichWebView: View {
    //abridgement...

    ///Loading progress
    @State private var estimatedProgress: Double = 0.0
    ///Whether loading
    @State private var isLoading: Bool = false

    var body: some View {
        VStack() {
            if isLoading {
                ProgressBarView(estimatedProgress: estimatedProgress)
            }
            //abridgement...
        }
    }
}

You have now implemented a set of functions. There are other points to consider such as error handling, but I think we were able to create a WebView with rough functions.

in conclusion

This time, I implemented it aiming at displaying a rich WebView, but in order to implement WebView with SwiftUI, it is necessary to create it using the basic functions of ʻUIViewRepresentable`, so it is just right for studying. think. If you are interested, please try it. I will summarize the source on github below, so if you are interested, I would appreciate it if you could see it. (The github side contains some code omitted here, such as the title display of the navigation bar and restrictions for layout.) RichWebViewSample

Also, if you have any suggestions for improvement or opinions regarding this implementation, we would appreciate it if you could comment. Thank you very much.

References

Recommended Posts

Try to display a slightly rich Webview in Swift UI using UIViewRepresentable
Try to create a browser automatic operation environment using Selenide in 5 minutes
Try to create a bulletin board in Java
How to fix a crash when deleting Realm data in Swift UI List
(Android) Try to display static text using DataBinding
Try to display prime numbers using boolean type
Try to make a music player using Basic Player
[Swift] How to display the entered characters in Widget via UserDefaults when using WidgetKit
[Swift] Save Color to User Defaults in Swift UI (memo)
How to display a browser preview in VS Code
How to convert A to a and a to A using AND and OR in Java
Try to solve a restricted FizzBuzz problem in Java
Try to build a Java development environment using Docker
I want to find a relative path in a situation using Path
How to display a graph in Ruby on Rails (LazyHighChart)
Display API definition in Swagger UI using Docker + Rails6 + apipie
How to change BackgroundColor etc. of NavigationBar in Swift UI
Try using RocksDB in Java
[Swift] Try using Collection View
Photo library in Swift UI
Try using gRPC in Ruby
[Swift] How to implement the Twitter login function using Firebase UI ①
What to do if you get a DISPLAY error in gym.render ()
How to implement UI automated test using image comparison in Selenium
[Swift] How to implement the Twitter login function using Firebase UI ②