So I am using a WKWebView within UIViewRepresentable
so I can show a web view in my SwiftUI view.
For a while I could not figure out why my SwiftUI view would not update when the Coordinator would set @Publsihed properties that affect the SwiftUI view.
In the process I finally understood better how UIViewRepresentable
works and realized what the problem was.
This is the UIViewRepresentable
:
struct SwiftUIWebView : UIViewRepresentable {
@ObservedObject var viewModel: WebViewModel
func makeCoordinator() -> Coordinator {
Coordinator(self, viewModel: viewModel)
}
let webView = WKWebView()
func makeUIView(context: Context) -> WKWebView {
....
return self.webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {
// This made my SwiftUI view update properly when the web view would report loading progress etc..
context.coordinator.viewModel = viewModel
}
}
The SwiftUI view would pass in the viewModel
, then makeCoordinator
would be called (only the first time at init...), then the Coordinator
would be returned with that viewModel
.
However, subsequently when a new viewModel
was passed in on updates and not on coordinator init, the coordinator would just keep the old viewModel
and things would stop working.
So I added this in the updateUIView...
call, which did fix the problem:
context.coordinator.viewModel = viewModel
Question:
Is there a way to pass in the viewModel
to the Coordinator during the func makeUIView(context: Context) -> WKWebView { ... }
so that if a new viewModel is passed in to SwiftUIWebView
the coordinator would also get the change automatically instead of me having to add:
context.coordinator.viewModel = viewModel
in updateUIView...
?
EDIT: Here is the entire code. The root content view:
struct ContentView: View {
@State var showTestModal = false
@State var redrawTest = false
var body: some View {
NavigationView {
VStack {
Button(action: {
showTestModal.toggle()
}) {
Text("Show modal")
}
if redrawTest {
Text("REDRAW")
}
}
}
.fullScreenCover(isPresented: $showTestModal) {
WebContentViewTest(redraw: $redrawTest)
}
}
}
And what the Content view presents:
struct SwiftUIProgressBar: View {
@Binding var progress: Double
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .leading) {
Rectangle()
.foregroundColor(Color.gray)
.opacity(0.3)
.frame(width: geometry.size.width, height: geometry.size.height)
Rectangle()
.foregroundColor(Color.blue)
.frame(width: geometry.size.width * CGFloat((self.progress)),
height: geometry.size.height)
.animation(.linear(duration: 0.5))
}
}
}
}
struct SwiftUIWebView : UIViewRepresentable {
@ObservedObject var viewModel: WebViewModel
func makeCoordinator() -> Coordinator {
Coordinator(self, viewModel: viewModel)
}
let webView = WKWebView()
func makeUIView(context: Context) -> WKWebView {
print("SwiftUIWebView MAKE")
if let url = URL(string: viewModel.link) {
self.webView.load(URLRequest(url: url))
}
return self.webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {
//add your code here...
}
}
class Coordinator: NSObject {
private var viewModel: WebViewModel
var parent: SwiftUIWebView
private var estimatedProgressObserver: NSKeyValueObservation?
init(_ parent: SwiftUIWebView, viewModel: WebViewModel) {
print("Coordinator init")
self.parent = parent
self.viewModel = viewModel
super.init()
estimatedProgressObserver = self.parent.webView.observe(\.estimatedProgress, options: [.new]) { [weak self] webView, _ in
print(Float(webView.estimatedProgress))
guard let weakSelf = self else{return}
print("in progress observer: model is: \(Unmanaged.passUnretained(weakSelf.parent.viewModel).toOpaque())")
weakSelf.parent.viewModel.progress = webView.estimatedProgress
}
}
deinit {
estimatedProgressObserver = nil
}
}
class WebViewModel: ObservableObject {
@Published var progress: Double = 0.0
@Published var link : String
init (progress: Double, link : String) {
self.progress = progress
self.link = link
print("model init: \(Unmanaged.passUnretained(self).toOpaque())")
}
}
struct WebViewContainer: View {
@ObservedObject var model: WebViewModel
var body: some View {
ZStack {
SwiftUIWebView(viewModel: model)
VStack {
if model.progress >= 0.0 && model.progress < 1.0 {
SwiftUIProgressBar(progress: .constant(model.progress))
.frame(height: 15.0)
.foregroundColor(.accentColor)
}
Spacer()
}
}
}
}
struct WebContentViewTest : View {
@Binding var redraw:Bool
var body: some View {
let _ = print("WebContentViewTest body")
NavigationView {
ZStack(alignment: .topLeading) {
if redraw {
WebViewContainer(model: WebViewModel(progress: 0.0, link: "https://www.google.com"))
}
VStack {
Button(action: {
redraw.toggle()
}) {
Text("redraw")
}
Spacer()
}
}
.navigationBarTitle("Test Modal", displayMode: .inline)
}
}
}
If you run this you will see that while WebViewModel
can get initialized multiple times, the coordinator will only get initialized once and the viewModel in it does not get updated. Because of that, things break after the first redraw.