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))
}
}
