Swift beginner here. I am trying to create a reusable component that simply renders either a filled or empty systemImage circle based on a value stored in state. I’ve put together a reproduction example below. Right now it only appears to be updating the value in state (in formData
), but the UI doesn’t seem to be updating (the circle is not filling up). I have a feeling the Image
and Text
components are set up to react to state changes, but I’m not sure how to fix from there. Thank you in advance!
import SwiftUI
struct Test: View {
@ObservedObject var formData = FormData()
var body: some View {
VStack {
Text("Performance: (String(formData.goals.performance))")
Toggle(isOn: Binding(
get: { formData.goals.performance },
set: { formData.goals.performance = $0 }
)) {
Text("Performance")
}
ChildWrapper(isPerformanceEnabled: $formData.goals.performance)
}
}
}
struct ChildWrapper: View {
@Binding var isPerformanceEnabled: Bool
var body: some View {
VStack {
Image(systemName: isPerformanceEnabled ? "circle.fill" : "circle")
.font(.system(size: 50))
.foregroundColor(isPerformanceEnabled ? .green : .red)
.onTapGesture {
isPerformanceEnabled.toggle()
}
Text(isPerformanceEnabled ? "Enabled" : "Disabled")
}
}
}
struct Test_Previews: PreviewProvider {
static var previews: some View {
Test()
}
}
2
Answers
If you use this:
maybe work but is not the complete solution for you. I think you do check the correct binding $ of the ObservedObject.
The problem is in your model (
FormData
).Let’s examine the code:
1. This bit is incorrect. You need
@StateObject
. Here is why:@ObservedObject
implies that ownership is not local. For example you could have an object higher up in the hierarchy or a singleton passed to this view.The documentation states:
2. There is no reason to pass a custom Binding to your toggle. Just pass:
Toggle(isOn: $formData.goals.performance)
3. A typical implementation of your model could look like this:
note here that
Goals
is a struct, which means that whenever any of its properties change,goals
would be re-assigned andFormData
would emit anobjectWillChange()
triggering your view to re-evaluate thebody
.Bonus Bit:
Here is an interesting fact that many people get wrong…
ChildWrapper
got a binding in order to be able to changeisPerformanceEnabled
, which is correct.But what is exactly causing the re-evaluation?
The answer is, not the change to the binding, but the change to
FormData
(through the binding) that re-evaluated the parent view!In fact, if the binding changed something that not an ancestor view would be watching, no re-evaluation would happen.
That’s all. I hope that this makes sense.