@State updates view but @ObservedObject does not

Viewed 69

I have a view:

struct Form: View {
  @ObservedObject var model = FormInput()

  var body: some View {
    Form {
      TextArea("Details", text: $model.details.value)
        .validation(message: model.details.message)
    }
  }
}

Where TextArea is a custom view and .validation(message: model.details.message) is a custom view modifier. My FormInput looks as follows:

final class FormInput: ObservableObject {
  @Published var details: TextInput

  // ... other code
}

TextInput is a custom struct.

Now when I run the above code the validation never triggers because the Form never re-renders, but if I change the Form to:

 struct MyForm: View {
      @State var details = TextInput()

      var body: some View {
        Form {
          TextArea("Details", text: $details.value)
            .validation(message: details.message)
        }
      }
    }

Then everything works as expected. Why would the view render for the second version of Form but not for the first? Shouldn't the Form in the first case update when details changes since details is @Published and the Form is observing the changes?

ADDITIONAL CONTEXT

Below is additional code for the above components

final class FormInput: ObservableObject {
  @Published var details: TextInput

  init(details: String = "") {
    self.details = TextInput(value: details, isValid: false, validations: [.length(12)])
  }

  // other code
}

struct TextInput {
  var value: String {
    didSet {
      self.validate()
    }
  }
  var validations: [TextValidation] // this is just an enum of different types of validations
  var isValid: Bool
  var message: String


  mutating func validate() {
    for validation in validations {
      // run validation
    }
  }
}

struct TextArea: View {
  @Binding var text: String
  @State var height: CGFloat = 12

  init(text: Binding<String>) {
    self._text = text
  }

  var body: some View {
    TextAreaField(text: $text)
  }
}

 struct TextAreaField: UIViewRepresentable {
   @Binding var text: String
   @Binding var height: CGFloat


   func makeUIView(context: Context) -> UITextView {
     let textField = UITextView()

     textField.isEditable = true
     textField.isSelectable = true
     textField.isUserInteractionEnabled = true
     textField.isScrollEnabled = false
     // ..other initializers removed for brevity

      textField.delegate = context.coordinator
      return textField
    }

   func updateUIView(_ uiView: UITextView, context: Context) {
     calculateHeight(uiView)
   }

   func calculateHeight(_ uiView: UIView) {
        let newSize = uiView.sizeThatFits(CGSize(width: uiView.frame.size.width, height: CGFloat.greatestFiniteMagnitude))
        if self.height != newSize.height {
          DispatchQueue.main.async  {
            self.height = newSize.height
          }
        }
      }


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


   final class Coordinator: NSObject, UITextViewDelegate {
     var parent: TextAreaField


     init(_ parent: TextAreaField) {
       self.parent = parent
     }

     func textViewDidChange(_ uiView: UITextView) {
       self.parent.text = uiView.text
       self.parent.calculateHeight(uiView)
      }

    }
}

struct Validation: ViewModifier {
  let message: String

  func body(content: Content) -> some View {
    let isValid = message == ""
    print(message)

    return VStack(alignment: .leading) {
      content

      if isValid == false {
        Text(message)
          .font(.footnote)
          .italic()
          .foregroundColor(Color.red)
          .frame(height: 24)
      }

    }
    .padding(.bottom, isValid ? 24 : 0)
  }
}


extension View {
  func validation(message: String) -> some View {
    self.modifier(Validation(message: message))
  }
}
1 Answers

I suppose the issue is in absent custom components. Because below simple demo replication of provided infrastructure works well with ObservableObject, actually as expected.

Tested with Xcode 11.4 / iOS 13.4. Comparing with below demo might be helpful to find what is missed in your code.

demo

struct TextInput {
    var value: String = "" {
        didSet {
            message = value    // just duplication for demo
        }
    }
    var message: String = ""
}

// simple validator highlighting text when too long
struct ValidationModifier: ViewModifier {
    var text: String

    func body(content: Content) -> some View {
        content.foregroundColor(text.count > 5 ? Color.red : Color.primary)
    }
}

extension View {   // replicated 
    func validation(message: String) -> some View {
        self.modifier(ValidationModifier(text: message))
    }
}

struct MyForm: View {    // avoid same names with standard views
  @ObservedObject var model = FormInput()

  var body: some View {
    Form {
      TextField("Details", text: $model.details.value) // used standard
        .validation(message: model.details.message)
    }
  }
}

final class FormInput: ObservableObject {
  @Published var details: TextInput = TextInput()
}

struct TestObservedInModifier: View {
    var body: some View {
        MyForm()
    }
}
Related