Rumbles
Rumbles

Reputation: 858

Changes to ObservedObject don't update a UIViewRepresentable

Simplest example below. Works fine in preview (UITextView text updates to "ouch"). But, run it in an app (ie add as rootView by the sceneDelegate), and the UITextView doesn't update.

import SwiftUI

class ModelObject : ObservableObject{
    @Published var text = "Model Text"
}

struct MyTextView : UIViewRepresentable {
    @ObservedObject var modelObject : ModelObject

    func makeUIView(context: Context) -> UITextView {
        let result = UITextView()
        result.isEditable = true
        return result
    }
    func updateUIView(_ view: UITextView, context: Context) {
        view.text = modelObject.text
    }
}

struct BugDemoView : View{
    @ObservedObject var modelObject : ModelObject
    var body : some View{
        VStack{
            MyTextView(modelObject: modelObject)
            Button(action: {
                self.modelObject.text = "ouch"
            }){
                Text("Button")
            }
        }
    }
}

#if DEBUG

var mo = ModelObject()

struct BugDemoView_Preview: PreviewProvider {
    static var previews: some View {
        BugDemoView(modelObject: mo)
    }
    
}
#endif

Upvotes: 10

Views: 3270

Answers (4)

Plan_A_is_brute_force
Plan_A_is_brute_force

Reputation: 61

I noticed via print statements that makeUIView wasn't being called again after the first URL was successfully loaded. So upon the URL changing, my WebView was remaining on the first URL.

I modified my updateUIView method - which WAS being called when the URL was updated after the first successful load - to check if there was a difference between the active url and the new url. If there was a difference I updated the page with the correct url.

Here is my sample updateUIView method:

let request: URLRequest

func updateUIView(_ uiView: WKWebView, context: Context) {
    if uiView.canGoBack, webViewStateModel.goBack {
        uiView.goBack()
        webViewStateModel.goBack = false
    } else {
        if(uiView.url?.absoluteString == request.url?.absoluteString){
            print("The urls are equal")
        } else {
            print("The urls are NOT equal")
            uiView.load(request)
        }
    }
}

Upvotes: 0

domi852
domi852

Reputation: 527

In my case I need to pass in an observable object into the uiViewRepresentable, so same case as mentioned by Rivera... When a @Published property of that observable object changes, updateUIView is called in the simulator but not on a real device... I'm using the latest Xcode 11.4 but admittedly on a device running iOS 13.3 (13.4.1 not installed yet, so I haven't checked if that bug has been eliminated or not). What solved my problem is the following: change the struct MyTextView into a final class (the final keyword is important), then add an initialiser. It is not even necessary to call .objectWillChange.send() on the observed object right before the change of the published var is triggered.

Upvotes: 0

DogCoffee
DogCoffee

Reputation: 19946

I had mixed results using the following. I'm pretty sure there is some kind bug with UIViewRepresentable

It worked for someviews, but then I pushed the exact same view again and the view model wouldn't update. Very strange...

Hopefully they release a SwiftUI TextView soon.

struct TextView: UIViewRepresentable {
    @Binding var text: String

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeUIView(context: Context) -> UITextView {
        let myTextView = UITextView()
        myTextView.delegate = context.coordinator
        myTextView.font = UIFont(name: "HelveticaNeue", size: 15)
        myTextView.isScrollEnabled = true
        myTextView.isEditable = true
        myTextView.isUserInteractionEnabled = true
        myTextView.backgroundColor = UIColor(white: 0.0, alpha: 0.05)
        return myTextView
    }

    func updateUIView(_ uiView: UITextView, context: Context) {
        uiView.text = text
    }

    class Coordinator: NSObject, UITextViewDelegate {

        var parent: TextView

        init(_ uiTextView: TextView) {
            self.parent = uiTextView
        }

        func textViewDidChange(_ textView: UITextView) {
            print("text now: \(String(describing: textView.text!))")
            self.parent.text = textView.text
        }
    }
}

Upvotes: 0

Michcio
Michcio

Reputation: 2866

It looks like some kind of bug of SwiftUI, but there are two workarounds:

  1. Pass string as @Binding to MyTextView:
struct MyTextView : UIViewRepresentable {
    @Binding var text: String

    func makeUIView(context: Context) -> UITextView {
        let result = UITextView()
        result.isEditable = true
        return result
    }
    func updateUIView(_ view: UITextView, context: Context) {
        view.text = text
    }
}

struct BugDemoView : View{
    @ObservedObject var modelObject = ModelObject()
    var body : some View{
        VStack{
            MyTextView(text: $modelObject.text)
            Button(action: {
                self.modelObject.text = "ouch"
            }){
                Text("Button")
            }
        }
    }
}
  1. Pass string to MyTextView:
struct MyTextView : UIViewRepresentable {
    var text: String

    func makeUIView(context: Context) -> UITextView {
        let result = UITextView()
        result.isEditable = true
        return result
    }
    func updateUIView(_ view: UITextView, context: Context) {
        view.text = text
    }
}

struct BugDemoView: View{
    @ObservedObject var modelObject = ModelObject()
    var body: some View{
        VStack{
            MyTextView(text: modelObject.text)
            Button(action: {
                self.modelObject.text = "ouch"
            }){
                Text("Button")
            }
        }
    }
}

Upvotes: 2

Related Questions