4 Replies
      Latest reply on May 22, 2020 8:18 AM by punkbit
      punkbit Level 1 Level 1 (0 points)

        (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!