Developer-1
Developer-1

Reputation: 193

UITextView in a modal sheet is not working

To make UI-based editing of a NSAttributedString property (in a managed object) possible, a UITextView is used instead of a SwiftUI TextField View. The text view is located in a modal view being presented by a sheet function.

.sheet(isPresented: $presentSheet) { ...

(to illustrate and reproduce, the code below is a simplified version of this scenario)

The modal view is used to edit a selected model item that is shown in a list through a ForEach construct. The selected model item is passed as an @Observable object to the modal view.

When selecting an item "A", the modal view and the UITextView correctly shows this model item. If selecting a new item "B", the modal view correctly shows this "B" item. But if "B" is now being edited the change will affect the "A" object.

The reason for this behaviour is probably that the UIViewRepresentable view (representing the UITextView) is only initialised once. Further on from here, this seems to be caused by the way a sheet (modal) view is presented in SwiftUI (state variables are only initialised when the sheet first appear, but not the second time).

I am able to fix this malfunction by passing the selected item as a @Binding instead of an @Observable object, although I am not convinced that this is the right way to handle the situation, especially because everything works nicely, if a SwiftUI TextField is used instead of the UITextView (in the simplified case).

Worth mentioning, I seems to have figured out, what goes wrong in the case with the UITextView - without saying that this solves the problem.

In the code listed below (which repro the problem), the Coordinator's init function has one assignment that initialises the Coordinator with the parent. Since this is value and not a reference assignment, and since the Coordinator only get initialised once, an edit of the UITextView will likely access a wrong parent.

Again, I am not certain about my solution to the problem, is the right one, since everything works fine when using a SwiftUI TextField instead. I therefore hope to see some comments on this problem.

struct ContentView: View {

    var states = [StringState("A"), StringState("B"), StringState("C"), StringState("D"), StringState("E")]

    @State var presentSheet = false
    @State var state = StringState("A")

    var body: some View {

        VStack {

            Text("state = \(state.s)")

            ForEach(states) { s in

                Button(action: {

                    self.state = s
                    self.presentSheet.toggle()

                    })
                {
                    Text("\(s.s)")
                }
            }
        }
        .sheet(isPresented: $presentSheet) {

            EditView(state: self.state, presentSheet: self.$presentSheet)
        }
    }
}

struct EditView: View
{
    @ObservedObject var state: StringState
    @Binding var presentSheet: Bool

    var body: some View {

        VStack {

            Text("\(state.s)")

            TextView(string: $state.s) // Edit Not OK

            TextField("", text: $state.s ) // Edit OK

            Button(action: {
                self.presentSheet.toggle()
            })
            { Text("Back") }
       }
    }
}

struct TextView: UIViewRepresentable
{
    @Binding var string: String

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

    func makeUIView(context: Context) -> UITextView
    {
        let textview = UITextView(frame: CGRect.zero)

        textview.delegate = context.coordinator

        return textview
    }

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

    class Coordinator : NSObject, UITextViewDelegate
    {
        var parent: TextView

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

        func textViewDidChange(_ textView: UITextView)
        {
            self.parent.string = textView.text!
        }
    }
}

class StringState: Identifiable, ObservableObject
{
    let ID = UUID()
    var s: String

    init(_ s : String) {
        self.s = s
    }
}

Upvotes: 1

Views: 464

Answers (1)

kontiki
kontiki

Reputation: 40629

A couple of changes will fix it:

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

And also add @Published to your ObservableObject:

class StringState: Identifiable, ObservableObject
{
    let ID = UUID()
    @Published var s: String

    init(_ s : String) {
        self.s = s
    }
}

Upvotes: 1

Related Questions