I have a rather complicated set of views nested in views. When I trigger a button action, I pass along an optional block through my viewModel
class which calls objectWillChange.send()
on that viewModel
and I know that it's being triggered because the other parts of my view are updating. One of the child views (which is observing that viewModel
changes) doesn't update until I click on part of it (which changes viewModel.selectedIndex
and triggers redraw so I know it's listening for published changes).
Why isn't the update triggering the child view (in this case PurchaseItemGrid
) to redraw itself?
Here's where I setup the call to update...
struct RightSideView: View {
@ObservedObject var viewModel: TrenchesPurchases
var body: some View {
VStack {
...
PurchaseItemGrid(viewModel: viewModel) // <-- View not updating
Button {
viewModel.purchaseAction() {
viewModel.objectWillChange.send() // <-- Triggers redraw, reaches breakpoint here
}
} label: {
...
}
...
}
}
}
Here's where the optional is called (and I've not only visually confirmed this is happening as other parts of the view redraw, it also hits breakpoint here)...
class TrenchesPurchases: ObservableObject, CanPushCurrency {
// MARK: - Properties
@Published private var model = Purchases()
// MARK: - Properties: Computed
var selectedIndex: Int {
get { return model.selectedIndex }
set { model.selectedIndex = newValue }
}
var purchaseAction: BlockWithBlock {
{ complete in
...
complete?()
}
}
...
}
And here's the view that's not updating as expected...
struct PurchaseItemGrid: View {
@ObservedObject var viewModel: TrenchesPurchases
var body: some View {
VStack {
itemRow(indices: 0...3)
...
}
...
}
@ViewBuilder
func itemRow(indices range: ClosedRange<Int>) -> some View {
HStack {
ForEach(viewModel.purchaseItems[range], id: \.id) { item in
PurchaseItemView(item: item,
borderColor: viewModel.selectedIndex == item.id ? .green : Color(Colors.oliveGreen))
.onTapGesture { viewModel.selectedIndex = item.id }
}
}
}
}
Here's the code workingdog asked for...
struct Purchases {
// MARK: - Properties
var selectedIndex = 15
let items: [PurchaseItem] = buildCollectionOfItems()
// MARK: - Functions
// MARK: - Functions: Static
// TODO: Define Comments
static func buildCollectionOfItems() -> [PurchaseItem] {
return row0() + row1() + row2() + row3()
}
static func row0() -> [PurchaseItem] {
var items = [PurchaseItem]()
let grenade = Ammo(ammo: .grenade)
items.append(grenade)
let bullets = Ammo(ammo: .bullets)
items.append(bullets)
let infiniteBullets = Unlock(mode: .defense)
items.append(infiniteBullets)
let unlimitedInfantry = Unlock(mode: .offense)
items.append(unlimitedInfantry)
return items
}
static func row1() -> [PurchaseItem] {
var items = [PurchaseItem]()
for unit in UnitType.allCases {
let item = Unit(unit: unit)
items.append(item)
}
return items
}
static func row2() -> [PurchaseItem] {
var items = [PurchaseItem]()
let brits = NationItem(nation: .brits)
items.append(brits)
let turks = NationItem(nation: .turks)
items.append(turks)
let usa = NationItem(nation: .usa)
items.append(usa)
let insane = DifficultyItem(difficulty: .insane)
items.append(insane)
return items
}
static func row3() -> [PurchaseItem] {
var items = [PurchaseItem]()
let offenseLootBox = Random(mode: .offense)
items.append(offenseLootBox)
let defenseLootBox = Random(mode: .defense)
items.append(defenseLootBox)
let currency = Currency(isCheckin: false)
items.append(currency)
let checkIn = Currency(isCheckin: true)
items.append(checkIn)
return items
}
}
The issue I had was that the PurchaseItemGrid
was noticing the observed item being published, but the change I was trying to trigger was in the PurchaseItemView
which did not have an observed object.
I assumed that when the PurchaseItemGrid
observed the change and was redrawn, the itemRow
method would redraw a new collection of PurchaseItemView
's that would then have their image updated to match the new state.
This was further compounded because the onTapGesture
was triggering a redraw of the PurchaseItemView
, and to be honest I'm still not sure how the PurchaseItemGrid
could redraw itself while still using the same PurchaseItemView
's in it's body; but it may have to do with how @ViewBuilder works and because the views were created in an entirely separate method.
So, long story short: make sure each view you want to update has some form of observer, don't rely on the parent's redraw to create new child views.