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.
I’ll try and provide all the relevant information in the views that I’ve got working:
This is my HomeView
. It contains CharacterSummaryView
(which is working properly), a set of LevelView
s (from the Character.Levels
), and then when you press the LevelView
it displays LevelUpView
for that Level.
struct HomeView: View {
@ObservedObject var character: Character
var body: some View {
VStack {
NavigationStack {
CharacterSummaryView(character: character)
List($character.levels, id: \.levelNum) {level in
NavigationLink(destination: LevelUpView(level: level, character: character)) {
VStack {
LevelView(level: level.wrappedValue)
}
}
}
}
}
}
}
Here is LevelView
, which is where the issues lie. When I update the skillCount[index].skillsUsed
value in the LevelUpView
, it does not get correctly propagated to this view:
import SwiftUI
import OrderedCollections
struct LevelView: View {
@ObservedObject var level: Level
var body: some View {
VStack(alignment: .leading) {
Text("LEVEL \(level.levelNum)")
.font(.caption)
if let skillcount = level.appliedSkill, skillcount.count > 0 {
VStack {
ForEach(skillcount.indices, id : \.self) {index in
Text("Skill Bonus - \(skillcount[index].skillsUsed)/\(skillcount[index].skillsAllowed)")
}
}
}
}
}
}
Here is LevelUpView (ignore the double Vstacks — I removed some code that is not relevant to the problem at hand). Essentially, it’s looking through the level
being passed from HomeView
to it, and then making a section for each of the appliedSkill
s that expands on being pressed:
import SwiftUI
import OrderedCollections
import WrappingHStack
struct LevelUpView: View {
@Binding var level: Level
@ObservedObject var character: Character
@State private var activeSkillBonus: UUID?
var body: some View {
GeometryReader { geometry in
let wid = geometry.size.width * 0.50
ScrollView {
VStack (alignment: .leading) {
VStack (alignment: .leading) {
if let appliedSkill = level.appliedSkill, appliedSkill.count > 0 {
VStack (alignment: .leading) {
ForEach(appliedSkill.indices, id: \.self) { index in
SkillBoostsView(character: character, skillBoost: appliedSkill[index], level: level.levelNum, wid: wid, showSkillBonuses: $activeSkillBonus)
.padding(.bottom, 10)
}
}
}
}
}
}
}
}
}
Here is the SkillBoostsView
. It has a box/button that when pressed expands to show a SkillPillboxView
for each skill in the array of skills. Of note, the skillBoost.skillsUsed
value is updating here correctly, but is just not propagating up to the HomeView
> LevelView
:
import SwiftUI
import WrappingHStack
struct SkillBoostsView: View {
@ObservedObject var character: Character
@ObservedObject var skillBoost: LevelSkills
var level: Int
var wid: CGFloat
@Binding var showSkillBonuses: UUID?
var body: some View {
VStack (alignment: .leading)
{
HStack {
VStack {
Text("Skill Boosts - \(skillBoost.skillsUsed)/\(skillBoost.skillsAllowed)")
Text(skillBoost.skillType.rawValue)
Text(skillBoost.source.rawValue)
}
.frame(width: wid)
.foregroundStyle(Color.white)
.padding()
.background(RoundedRectangle(cornerRadius: 20)
.foregroundStyle( skillBoost.skillsUsed < skillBoost.skillsAllowed ? Color.oxblood : Color.green))
.onTapGesture{
withAnimation {
if showSkillBonuses == skillBoost.id {
showSkillBonuses = nil
} else {
showSkillBonuses = skillBoost.id
}
}
}
}
HStack {
if showSkillBonuses == skillBoost.id {
WrappingHStack {
ForEach(Array(skillBoost.skills.indices), id: \.self) { index in
SkillPillboxView(skill: skillBoost.skills[index], character: character, skillBoosts: skillBoost, level: level)
.frame(width: wid * 0.8)
}
}
.transition(.asymmetric(insertion: .push(from: .top), removal: .push(from: .bottom)))
}
}
.animation(.easeInOut, value: showSkillBonuses)
}
}
}
And here is the SkillPillboxView
, which is also working more or less as intended:
import SwiftUI
struct SkillPillboxView: View {
@ObservedObject var skill: Skill
@ObservedObject var character: Character
@ObservedObject var skillBoosts: LevelSkills
var level: Int
var body: some View {
HStack {
VStack(alignment: .leading) {
Text(skill.displayName)
.font(.headline)
HStack {
Text("\(skill.associatedAbility)")
.font(.caption)
Text("|")
Text(rankString(skillLevelCalc))
.font(.caption)
}
}
Text("+\(skillBonusCalc)")
}
.foregroundStyle(skill.hasIncreased ? Color.red : Color.primary)
.padding(10)
.background(backgroundColor(for: skillLevelCalc))
.cornerRadius(10)
.onTapGesture {
skillToggle()
}
.transition(.scale)
.animation(.easeInOut, value: skill.hasIncreased)
}
// Computed property to calculate the skill level
private var skillLevelCalc: Int {
return skillLevel(for: skill.name.rawValue, in: character, level: level)
}
private var skillBonusCalc: Int {
var bonus = 0
if skillLevelCalc > 0 {
bonus += level
bonus += 2*skillLevelCalc
}
bonus += calculateAbilityScoreAndModifier(character.abilityScores[skill.associatedAbility] ?? 0).modifier
return bonus
}
private func skillToggle() {
var maxTrain: Int = 0
if skillBoosts.skillType == .training {
maxTrain = 1
} else {
switch level {
case 0...6: maxTrain = 2
case 7...14: maxTrain = 3
case 15...20: maxTrain = 4
default: maxTrain = 1
}
}
if skill.hasIncreased {
skill.hasIncreased.toggle()
skillBoosts.skillsUsed -= 1
}
else if !skill.hasIncreased && skillBoosts.skillsUsed < skillBoosts.skillsAllowed && skillLevel(for: skill.name.rawValue, in: character, level: level) < maxTrain {
skill.hasIncreased.toggle()
skillBoosts.skillsUsed += 1
}
}
private func rankString(_ trainingLevel: Int) -> String {
switch trainingLevel {
case 0: return "Untrained"
case 1: return "Trained"
case 2: return "Expert"
case 3: return "Master"
case 4: return "Legendary"
default: return "Unknown"
}
}
}
private func backgroundColor(for level: Int) -> Color {
switch level {
case 0: return .gray
case 1: return .green
case 2: return .blue
case 3: return .purple
case 4: return .orange
default: return .black
}
}
}
What am I missing? If necessary, I can provide information about the Character
, Level
, LevelSkills
, or Skill
class that are used in each of these as well — I will head off and state in advance that they are all ObservableObject
s and that they all have the @Published
for all the values that are mentioned in these views.
Any idea why the Character.levels[x].appliedSkill[y].skillsUsed
value isn’t propagating back up from the pillbox to the top level?