Search code examples
swiftswiftuiwkwebview

How to display estimatedProgress of WKWebView in a Linear style ProgressView in swiftUI?


I tried searching for weeks on this matter and I can't find any help that is implemented in SwiftUI. Most of the answers were for UIKit.

struct ContentView: View {
    @State private var progress: Double = 0.0
    
    var body: some View {
        VStack {
            WebView()

            ProgressView(value: progress)
                .progressViewStyle(.linear)
        }
    }
}

struct WebView: UIViewRepresentable {
    func makeUIView(context: Context) -> WKWebView {
        let webView = WKWebView()
        webView.load(URLRequest(url: URL(string: "https://www.apple.com")!))
        return webView
    }
    
    func updateUIView(_ uiView: WKWebView, context: Context) {
        
    }
}

Solution

  • Because you are using UIViewRepresentable to wrap WKWebView you need to use a UIKit solution.

    We can track the estimatedProgress of the WKWebView using Key-Value-Observing (KVO). For this we need to add a Coordinator to our WebView.

    Just adding the coordinator is not enough we need a way to pass the value back from the UIViewRepresentable to the View. We could do this using a @Binding or a closure, but this will cause the following error when the view is updated:

    Warning: Modifying state during view update, this will cause undefined behavior.

    A "hacky" solution is to wrap the setting of the bound progress variable using DispatchQueue.main.async but this is not a good solution.

    A better way to get around this problem is to create a simple ViewModel that conforms to ObservableObject.

    This gives the following code:

    struct ContentView: View {
        @StateObject var viewModel = WebView.ProgressViewModel(progress: 0.0)
    
        var body: some View {
            VStack {
                WebView(url: URL(string: "https://www.apple.com")!, viewModel: viewModel)
    
                ProgressView(value: viewModel.progress)
                    .progressViewStyle(.linear)
            }
        }
    }
    

    The ContentView now holds a reference to the ProgressViewModel this is passed, along with the URL, to the WebView. The ProgressView is passed the @Published progress from the ProgressViewModel.

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

    There isn't much difference between this and your UIViewRepresentable except that we are passing the the URL and the ProgressViewModel. We have also raised the initialisation of the webView out of the makeUIView, this is so that we can have access to it in the Coordinator.

    extension WebView {
    
        func makeCoordinator() -> Coordinator {
            Coordinator(self, viewModel: viewModel)
        }
    
        class Coordinator: NSObject {
            private var parent: WebView
            private var viewModel: ProgressViewModel
            private var observer: NSKeyValueObservation?
    
            init(_ parent: WebView, viewModel: ProgressViewModel) {
                self.parent = parent
                self.viewModel = viewModel
                super.init()
    
                observer = self.parent.webView.observe(\.estimatedProgress) { [weak self] webView, _ in
                    guard let self = self else { return }
                    self.parent.viewModel.progress = webView.estimatedProgress
                }
            }
    
            deinit {
                observer = nil
            }
        }
    }
    

    This extension on WebView creates the coordinator, which inherits from NSObject. It holds references to the parent, the WebView, and the viewModel. We set up a KVO observer in the initializer observing the estimatedProgress on the webView, this allows us to update the progress value on our viewModel.

    extension WebView {
        class ProgressViewModel: ObservableObject {
            @Published var progress: Double = 0.0
    
            init (progress: Double) {
                self.progress = progress
            }
        }
    }
    

    Finally this is the ProgressViewModel that ties the UIViewRepresentable and the View together.

    You can see a video of it here