Changes to ObservedObject don't update a UIViewRepresentable
Asked Answered
C

4

10

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
Chappie answered 12/10, 2019 at 12:15 Comment(5)
I won't be of much help, but who knows? I was having a similar issue and never got it to work. Your stripped down code was missing things like using Combine and objectWillChange calls - and I didn't get it to work either. What I did get working was "old school" - posting Notification that my representable subscribed too. hopefully one of the gurus has a better solution, but since I asked virtually the same thing almost two months ago, I say just use what works.Cohleen
I have the same problem. Everything will work in pure SwiftUI, but the same @ObservedObject doesn't updates my UIViewControllerRepresentable.Forewing
anyone know if this has been resolved?Michelle
not to my knowledgeChappie
Retested with Xcode 12 / iOS 14 - works fine.Ehrlich
S
2

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")
            }
        }
    }
}
Shrewd answered 14/10, 2019 at 9:2 Comment(2)
Thank you for the work-around. Unfortunately, it doesn't fit my real use case for two reasons, 1) modelObject.text is a computed property, 2) I need access to modelObject within the view...Chappie
We want to use @ObservedObject for complex model classes.Forewing
L
0

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
        }
    }
}
Laski answered 5/11, 2019 at 23:33 Comment(0)
C
0

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.

Conch answered 15/4, 2020 at 6:44 Comment(0)
S
0

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)
        }
    }
}
Serif answered 13/1, 2021 at 6:21 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.