(I've posted this originally on StackOverflow and answered it myself, but I don't have enough experience with Swift and SwiftUI and would like this to be review by someone more experienced, to avoid misleading other users [https://stackoverflow.com/questions/61903284/why-isnt-a-closure-in-state-array-property-triggering-state-change])
A
test
view has @State
showTitle
,
title
and
items
where the
title
value text is controlled by a closure assigned to a CTA
show title
.
When the
showTitle
state changes, the value presented in the body Content of
test
view changes accordingly:
Text({ self.showTitle ? "Yes, showTitle!" : "No, showTitle!" }())
While the case where the
closure
is a value in the array
items
does not change. Why isn't the closure triggering the title state?
NestedView(title: $0.title())
The complete source-code:
struct test: View {
@State var showTitle: Bool = true
@State var title: String
@State var items: [Foobar]
var body: some View {
VStack {
Group {
Text("Case 1")
Text({ self.showTitle ? "Yes, showTitle!" : "No, showTitle!" }())
}
Group {
Text("Case 2")
ForEach (self.items, id: \.id) {
NestedView(title: $0.title())
}
}
Button("show title") {
print("show title cb")
self.showTitle.toggle()
}
}.onAppear {
let data = ["hello", "world", "test"]
for title in data {
self.items.append(Foobar(title: { self.showTitle ? title : "n/a" }))
}
}
}
}
struct NestedView: View {
var title: String
var body: some View {
Text("\(title)")
}
}
What's expected is that "Case 2" to have a similar side-effect we have in "Case 1" that should display "n/a" on
showTitle
toggle.
Output demo:
https://i.stack.imgur.com/YJVQu.gif
My conclusion:
From what I understand, the reason why the initial code does not work is related to the
showTitle
property that is passed to the Array and holds a copy of the
value
(creates a unique copy of the data).
I did think @State would make it controllable and mutable, and the
closure
would capture and store the reference (create a shared instance). In other words, to have had a
reference
, instead of a copied
value
! Feel free to correct me, if that's not the case, but that's what it looks like based on my analysis.
With that being said, I kept the initial thought process, I still want to pass a
closure
to the Array and have the state changes propagated, cause side-effects, accordingly to any references to it!
So, I've used the same pattern but instead of relying on a primitive type for
showTitle
Bool
, created a
Class
that conforms to the protocol
ObservableObject
: since
Classes
are
reference types
.
So, let's have a look and see how this worked out:
import SwiftUI
class MyOption: ObservableObject {
@Published var option: Bool = false
}
struct Foobar: Identifiable {
var id: UUID = UUID()
var title: () -> String
init (title: @escaping () -> String) {
self.title = title
}
}
struct test: View {
@EnvironmentObject var showTitle: MyOption
@State var title: String
@State var items: [Foobar]
var body: some View {
VStack {
Group {
Text("Case 1")
Text(self.showTitle.option ? "Yes, showTitle!" : "No, showTitle!")
}
Group {
Text("Case 2")
ForEach (self.items, id: \.id) {
NestedView(title: $0.title())
}
}
Button("show title") {
print("show title cb")
self.showTitle.option.toggle()
print("self.showTitle.option: ", self.showTitle.option)
}
}.onAppear {
let data = ["hello", "world", "test"]
for title in data {
self.items.append(Foobar(title: { self.showTitle.option ? title : "n/a" }))
}
}
}
}
struct NestedView: View {
var title: String
var body: some View {
Text("\(title)")
}
}
The result as expected: https://i.stack.imgur.com/dAHjh.gif
Please share your knowledge,
Thank you!