Reputation: 193
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
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