I’m working on the creation of an iPhone application and am having a few issues with the nature of @Binding, @ObservedObject, and the @Published property. I was pretty sure I have this all right but apparently not since it’s still not working.
Here is a "minimal" example of what’s happening — if I reduce the layers of nesting views, then it starts working correctly.
import SwiftUI
//character
class TopLevelParent: ObservableObject {
var id: UUID = UUID()
@Published var prop: [Child1] = [Child1(), Child1(), Child1()]
}
class TestState: ObservableObject {
@Published var tlp: TopLevelParent = TopLevelParent()
}
//level
class Child1: ObservableObject, Identifiable {
var id: UUID = UUID()
@Published var prop: [Child2] = [Child2(), Child2()]
}
//levelskill
class Child2: ObservableObject, Identifiable {
var id: UUID = UUID()
@Published var clicked: Int = 0
@Published var maxClick: Int = 3
@Published var prop: [YoungestChild] = [YoungestChild(), YoungestChild(), YoungestChild(), YoungestChild(), YoungestChild()]
}
//skill
class YoungestChild: ObservableObject, Identifiable {
var id: UUID = UUID()
@Published var name: String = "name"
@Published var hasIncreased: Bool = false
}
struct TestView: View {
@EnvironmentObject var state: TestState
var body: some View {
VStack {
NavigationStack {
List {
ForEach(state.tlp.prop, id:.id) { prop in
NavigationLink(destination: TestView2(testclass2: prop)) {
VStack {
TestView5(testclass2: prop)
}
}
}
}
}
}
}
}
struct TestView2: View {
@ObservedObject var testclass2: Child1
@State private var activeSkillBonus: UUID?
var body: some View {
VStack (alignment: .leading) {
let testclass3 = testclass2.prop
VStack (alignment: .leading) {
ForEach(testclass3) { index in
TestView3(testclass3: index, activeSkillBonuses: $activeSkillBonus)
}
}
}
}
}
struct TestView3: View {
@ObservedObject var testclass3: Child2
@Binding var activeSkillBonuses: UUID?
var body: some View {
VStack (alignment: .leading)
{
VStack {
Text("Skill Boosts - (testclass3.clicked)/(testclass3.maxClick)")
Text("(testclass3.id)")
}
.foregroundStyle(Color.white)
.background(RoundedRectangle(cornerRadius: 20)
.foregroundStyle(Color.oxblood))
.onTapGesture{
withAnimation {
if activeSkillBonuses == testclass3.id {
activeSkillBonuses = nil
} else {
activeSkillBonuses = testclass3.id
}
}
}
if activeSkillBonuses == testclass3.id {
VStack {
ForEach(testclass3.prop) { index in
TestView4(testclass4: index, testclass3: testclass3)
}
}
}
}
}
}
struct TestView4: View {
@ObservedObject var testclass4: YoungestChild
@ObservedObject var testclass3: Child2
var body: some View {
HStack {
VStack(alignment: .leading) {
Text(testclass4.name)
}
}
.foregroundStyle(testclass4.hasIncreased ? Color.red : Color.primary)
.background(testclass4.hasIncreased ? Color.green : Color.brown)
.onTapGesture {
testclass3.objectWillChange.send()
if testclass4.hasIncreased {
testclass3.clicked -= 1
} else
{
testclass3.clicked += 1
}
testclass4.hasIncreased.toggle()
}
}
}
struct TestView5: View {
@ObservedObject var testclass2: Child1
var body: some View {
VStack {
ForEach(testclass2.prop) {testclass3 in
Text("Skill Bonus - (testclass3.clicked)/(testclass3.maxClick)")
}
}
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
.environmentObject(TestState())
}
}
Any idea why the TopLevelParent.Child1[x].Child2[y].YoungestChild
value isn’t propagating from TestView4 to TestView5?
Essentially, TestView1 is the "Top level" view, and has a set of TestView5’s, one for each set of TopLevelParent.prop, set in a NagivationStack. When one of the TestView5’s in the NavigationStack is pressed, it opens a view that has TestView2, which shows a TestView3 for each Child1.Prop, and in each TestView3, it has a TestView4 for each Child2.Prop (or Child2.YoungestChild) that can be pressed, which updates a value for Child2, which value is being displayed in TestView3, as well as in TestView5. TestView3 updates with the value correctly, but TestView5 remains at the starting value (0/3 pressed).
2
Answers
If I update Child1 and Child2 with these changes, the code starts to work, and the value gets propagated to TestView5:
ChatGPT explains this:
Which makes sense, but I'm not sure if that's the real reason because we all know that ChatGPT is not entirely reliable all the time. In any case, I have it working for now (but am absolutely open to improvements on the code)
Use one source of truth:
Make sure there’s one source of truth for your data. This often means having your data owned by a top level view or a shared
EnvironmentObject
.Here’s an example how you can demonstrate this:
CharacterViewModel
that holds theCharacter
and applies different actions on it or thelevels
in it:.environmentObject()
:Character
directly inLevelUpView
and not inSkillBoostsView
either, then you wouldn’t even declare it there. Instead, you’d use it directly inSkillPillboxView
:This separation also ensures your code is clean and easy to debug by removing a lot of boilerplate code.