Reputation: 7717
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())
I've done tests with both Foobar as Struct
and Class
.
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)")
}
}
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:
Upvotes: 0
Views: 392
Reputation: 32879
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
You were right to blame the closure capturing the value at the onAppear
time. Basically due to this, SwiftUI doesn't know to refresh the list when the showTitle
value changes, as there's no Binding
involved that SwiftUI can use to know when to re-render the list.
I can provide two alternative solutions, that don't require another class just to hold the bool value. Both solutions involve communicating to SwiftUI that you need the showTitle
binding to refresh the titles.
title
, defer the title computation to the list builder:struct Foobar: Identifiable {
var id: UUID = UUID()
var title: String
init (title: String) {
self.title = title
}
}
...
ForEach (self.items, id: \.id) {
NestedView(title: self.showTitle ? $0.title : "n/a" )
}
...
.onAppear {
let data = ["hello", "world", "test"]
self.items = data.map { Foobar(title: $0) }
}
(Binding<Bool>) -> String
one, inject the $showTitle
binding from the view:struct Foobar: Identifiable {
var id: UUID = UUID()
var title: ((Binding<Bool>) -> String)
init (title: @escaping (Binding<Bool>) -> String) {
self.title = title
}
}
...
ForEach (self.items, id: \.id) {
// here we pass the $showTitle binding, thus SwiftUI knows to re-render
// the view when the binding value is updated
NestedView(title: $0.title(self.$showTitle))
}
...
.onAppear {
let data = ["hello", "world", "test"]
self.items = data.map { Foobar(title: { $0.wrappedValue ? title : "n/a" })) }
}
Personally, I'd go with the first solution, since it better transmit the intent.
Upvotes: 1
Reputation: 7717
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:
Upvotes: 1