Pierrick584
Pierrick584

Reputation: 33

SwiftUI Published variable doesn't trigger UI update

I have an app with a navigation view list that doesn't update when new elements get added later on in the app. the initial screen is fine and everything get triggered at this moment no matter how I code them, but beyond that, it stays that way. At some point I had my "init" method as an .onappear, and dynamic elements wouldn't come in, but the static ones would get added multiple times when I would go back and forth in the app, this is no longer part of my code now though.

here what my content view look like, I tried to move the navigation view part to the class that has the published var, in case it help, visually it dint change anything, dint help either.

struct ContentView: View {
    
    @ObservedObject var diceViewList = DiceViewList()
    
    var body: some View {
        VStack{
            Text("Diceimator").padding()
            diceViewList.body 
            Text("Luck Selector")
        }
    }
}

and the DiceViewList class

import Foundation
import SwiftUI
class DiceViewList: ObservableObject {
    @Published var list = [DiceView]()

    init() {
        list.append(DiceView(objectID: "Generic", name: "Generic dice set"))
        list.append(DiceView(objectID: "Add", name: "Add a new dice set"))
        // This insert is a simulation of what add() does with the same exact values. it does get added properly
        let pos = 1
        let id = 1
        self.list.insert(DiceView(objectID: String(id), dice: Dice(name: String("Dice"), face: 1, amount: 1), name: "Dice"), at: pos)
    }

 var body: some View {
        NavigationView {

            List {
                ForEach(self.list) { dView in
                    NavigationLink(destination: DiceView(objectID: dView.id, dice: dView.dice, name: dView.name)) {
                        HStack {  Text(dView.name) }
                    }
                }
            }
        }
    }
    
    func add(dice: Dice) {
        let pos = list.count - 1
        let id = list.count - 1
        self.list.insert(DiceView(objectID: String(id), dice: dice, name: dice.name), at: pos)
        
    }
}

I'm working on the latest Xcode 11 in case it matter

EDIT: Edited code according to suggestions, problem didnt change at all

struct ContentView: View {
    
    @ObservedObject var vm: DiceViewList = DiceViewList()
    
    var body: some View {


            NavigationView {
                List(vm.customlist) { dice in
                    NavigationLink(destination: DiceView(dice: dice)) {
                        Text(dice.name)
                    }
                } 


        }
    }
}

and the DiceViewList class

class DiceViewList: ObservableObject {
    
    @Published var customlist: [Dice] = []
    func add(dice: Dice) {
 
        self.customlist.append(dice)

    }
     
    init() {
        customlist.append(Dice(objectID: "0", name: "Generic", face: 1, amount: 1))
        customlist.append(Dice(objectID: "999", name: "AddDice", face: 1, amount: 1))
    }
}

Upvotes: 1

Views: 2553

Answers (3)

Misguided Chunk
Misguided Chunk

Reputation: 397

@NewDev's answer is correct for the specific question, but I'd also like to provide an answer for what my problem was.

I had nested @ObservableObject classes with @Published properties:

class InnerObject: ObservableObject {
   @Published var myBool: Bool = false
}

class ContentViewModel: ObservableObject {
   @Published var innerObject: InnerObject = InnerObject()
}

struct ContentView: View {
   @StateObject var vm: ContentViewModel = ContentViewModel()

   var body: some View {
      Text(vm.innerObject.myBool ? "True" : "False")
      Button("Toggle") {
         vm.innerObject.myBool.toggle()
      }
   }
}

and whenever I—in this example—clicked the button to toggle the inner object's myBool value, the view did not refresh.

TLDR: Nested @ObservableObject situations like can be solved by passing the inner reference to a child view which should store it as a @ObservedObject property:

struct ContentView: View {
   @StateObject var vm: ContentViewModel = ContentViewModel()

   var body: some View {
      Text(vm.innerObject.myBool ? "True" : "False")
      MyButtonView(innerObject: vm.innerObject)
   }
}

struct MyButtonView: View {
   @ObservedObject var innerObject: InnerObject

   var body: some View {
      Button("Toggle") {
         innerObject.myBool.toggle()
      }
   }
}

Read more here

Upvotes: 0

New Dev
New Dev

Reputation: 49580

SwiftUI is a paradigm shift from how you would build a UIKit app.

The idea is to separate the data that "drives" the view - which is the View model, from the View presentation concerns.

In other words, if you had a ParentView that shows a list of ChildView(foo:Foo), then the ParentView's view model should be an array of Foo objects - not ChildViews:

struct Foo { var v: String }

class ParentVM: ObservableObject {
   @Published let foos = [Foo("one"), Foo("two"), Foo("three")]
}

struct ParentView: View {
   @ObservedObject var vm = ParentVM()

   var body: some View {
       List(vm.foos, id: \.self) { foo in 
          ChildView(foo: foo)
       }
   }
}

struct ChildView: View {
   var foo: Foo
   var body = Text("\(foo.v)")
}

So, in your case, separate the logic of adding Dice objects from DiceViewList (I'm taking liberties with your specific logic for brevity):

class DiceListVM: ObservableObject {
   @Published var dice: [Dice] = []

   func add(dice: Dice) {
      dice.append(dice)
   }
}

struct DiceViewList: View {

   @ObservedObject var vm: DiceListVM = DiceListVM()

   var body: some View {
       NavigationView {
          List(vm.dice) { dice in
             NavigationLink(destination: DiceView(for: dice)) {
                Text(dice.name)
             }
       }
   }
}

If you need more data than what's available in Dice, just create a DiceVM with all the other properties, like .name and .dice and objectId.

But the takeaway is: Don't store and vend out views. - only deal with the data.

Upvotes: 1

Pierrick584
Pierrick584

Reputation: 33

While testing stuff I realized the problem. I Assumed declaring @ObservedObject var vm: DiceViewList = DiceViewList() in every other class and struct needing it would make them find the same object, but it doesn't! I tried to pass the observed object as an argument to my subview that contain the "add" button, and it now work as intended.

Upvotes: 0

Related Questions