0

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.

zumzum
  • 17,984
  • 26
  • 111
  • 172
  • Since SwiftUI wrappers only work in SwiftUI Views it is likely best to use something like `parent.viewModel` in the `coordinator`. Parent being a variable that holds a reference to the `SwiftUIWebView`. You can also try with `AnyCancellable` and `sink`. – lorem ipsum Nov 10 '21 at 02:00
  • parent.viewModel did not work. It's where I started and it would keep the first version of the model the coordinator was initialized with. – zumzum Nov 10 '21 at 02:47
  • I just provided the entire code showing how the coordinator gets out of sync – zumzum Nov 10 '21 at 16:58

0 Answers0