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())
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. As both are closures.
import SwiftUI
struct Foobar: Identifiable {
var id: UUID = UUID()
var title: () -> String
init (title: @escaping () -> String) {
self.title = title
}
}
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)")
}
}
Output:
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:
Obs: Posted my thoughts on StackOverflow, but end up answering my own question, but I'm afraid I don't have enough experience to keep my answer based on my analysis, so planning to delete it to avoid confusing other readers, so hopefully, I can find someone who's more experience to explain what's going on here.