I have such a simple checkmark SwiftUI view. It is displayed in a List and can be toggled by tapping on it or can be toggled when refreshed from a data source (ex. Core Data)
My First implementation was
struct RoundedCheckmark: View {
@State var isChecked : Bool
let onCheck: (Bool) -> Void
func toggle() { isChecked = !isChecked; onCheck(isChecked) }
init(isChecked: Bool, onCheck: @escaping (Bool) -> Void = { _ in }) {
self._isChecked = State(initialValue: isChecked)
self.onCheck = onCheck
}
var body: some View {
Button(action: toggle) {
Image(isChecked ? "CheckedCheckmark" : "UncheckedCheckmark")
}
}
}
It seemed to work I could toggle it and it loads correctly. On toggle, I saved changed via onCheck closure/callback and it refreshed ok.
But after external refreshes like push notification refreshing underlying model in Core Data refresh of this View doesn't correctly change @State property isChecked.
In init() I am getting new value of isChecked and reinitialized @State with State(initialValue: ). But in the body, I am getting the old value of isChecked as from old @State property. so the view does have the wrong image.
Here is workaround i.e. relying only on let isChecked property and onCheck callback. But now I cannot have state inside my View, and I have to only rely on external data source change. Maybe it is more appropriate as this way I have a single source of truth without local @State storage.
struct RoundedCheckmark: View {
let isChecked: Bool
let onCheck: (Bool) -> Void
func toggle() { onCheck(isChecked) }
init(isChecked: Bool, onCheck: @escaping (Bool) -> Void = { _ in }) {
self.isChecked = isChecked
self.onCheck = onCheck
}
var body: some View {
Button(action: toggle) {
Image(isChecked ? "CheckedCheckmark" : "UncheckedCheckmark")
}
}
}
isChecked as a @State variable is your source of truth. That means when some underlying data model (like CoreData) changes, it doesn't matter. Your view is only looking at the local @State version of isChecked.
But look at your view. To me, it shouldn't own its own state. Why? Because there is no semantic meaning to this view as a checkmark view. Its parent appears to own the state (hence why there is a onCheck callback). Instead, this should use a @Binding with no callback:
struct RoundedCheckmark: View {
@Binding var isChecked: Bool
var body: some View {
Button(action: { isChecked.toggle() }) {
Image(isChecked ? "CheckedCheckmark" : "UncheckedCheckmark")
}
}
Now your parent owns the state and you can infer its semantic meaning:
struct CheckmarkOwner: View {
@State var showFavoritesOnly = false
var body: some View {
// content
RoundedCheckmark(isChecked: $showFavoritesOnly)
// and now something else will get notified when `showFavoritesOnly` gets toggled
if showFavoritesOnly {
// toggleable content
}
// more content
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With