Why isn't 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())

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:

demo-state-change-reference-types

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.

Terms of Service

Privacy Policy

Cookie Policy