9

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
Martin Brisiak
  • 3,872
  • 12
  • 37
  • 51
Rumbles
  • 806
  • 1
  • 8
  • 15
  • 1
    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. –  Oct 12 '19 at 15:17
  • I have the same problem. Everything will work in pure SwiftUI, but the same `@ObservedObject` doesn't updates my `UIViewControllerRepresentable`. – Rivera Oct 24 '19 at 20:12
  • anyone know if this has been resolved? – jake_astub Apr 05 '20 at 05:11
  • not to my knowledge – Rumbles Apr 06 '20 at 18:44
  • Retested with Xcode 12 / iOS 14 - works fine. – Asperi Aug 16 '20 at 09:49

4 Answers4

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")
            }
        }
    }
}
Michcio
  • 2,708
  • 19
  • 26
  • 1
    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... – Rumbles Oct 22 '19 at 21:26
  • We want to use `@ObservedObject` for complex model classes. – Rivera Oct 24 '19 at 20:13
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
        }
    }
}
DogCoffee
  • 19,820
  • 10
  • 87
  • 120
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.

domi852
  • 497
  • 1
  • 4
  • 13
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)
        }
    }
}