Swiftui TextEditor seems not to respond to state changes

381 Views Asked by At

I am having trouble getting the SwiftUI TextEditor to work when it is in a Child View.

This is a small example that demonstrates the issue for me:

import SwiftUI

struct ContentView: View {
  @State private var someText: String = "Hello World"
    var body: some View {
      VStack {
        HStack {
          Button("Text 1", action: {someText = "hello"})
          Button("Text 2", action: {someText = "world"})
        }
        ViewWithEditor(entry: $someText)
      }
    }
}

struct ViewWithEditor: View {
  @Binding var entry: String
  @State private var localString: String
  
  var body: some View
  {
    VStack {
      TextEditor(text: $localString)
    }
  }
  
  init(entry: Binding<String>) {
    self._entry = entry
    self._localString = State(initialValue: entry.wrappedValue)
    print("init set local String to: \(localString)")
  }
}

When I click the buttons I expected the Editor text to change, however it remains with its initial value.

The print statement show that the "localString" variable is being updated.

Is TextEditor broken or am I missing something fundamental ??

If you move the buttons into the same view as the TextEditor, directly changing local state var it works as expected.

This is being run under MacOS in case it makes a difference.

TIA Alan.

2

There are 2 best solutions below

0
altimes On

Ok. So a proxy Binding does the job for me. See updated editor view below:

struct ViewWithEditor: View {
  @Binding var entry: String
  var body: some View
  {
    let localString = Binding<String> (
      get: {
        entry
      },
      set: {
        entry = $0
      }
    )
    return VStack {
      Text(entry)
      TextEditor(text: localString)
    }
  }

A bit more ugly (proxy bindings just seem clutter), but in some ways simpler..

It allows for the result of the edit to be reviewed / rejected before being pushed into the bound var.

0
Mike R On

This is occurring because the binding entry var is not actually being used after initialization of ViewWithEditor. In order to make this work without using the proxy binding add onChange to the ViewWithEditor as below:

struct ViewWithEditor: View {
  @Binding var entry: String
  @State private var localString: String

  var body: some View
  {
    VStack {
      TextEditor(text: $localString)
    }
    .onChange(of: entry) {
        localString = $0
    }
  }

  init(entry: Binding<String>) {
    self._entry = entry
    self._localString = State(initialValue: entry.wrappedValue)
    print("init set local String to: \(localString)")
  }
}

The problem here is that now entry is not updating if localString changes. One could just use the same approach as before:

  var body: some View
  {
    VStack {
      TextEditor(text: $localString)
    }
    .onChange(of: entry) {
        localString = $0
    }
    .onChange(of: localString) {
        entry = $0
    }
  }

but why not just use $entry as the binding string for TextEditor?