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) {
}
}
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