Title pretty much says it all. I cannot, despite much research, including here on S.O. – some of which has been incredibly helpful, some that leaves me more baffled than before – and many (failed) attempts, seem to get ObservableObjects to work as expected so that my SwiftUI Views get updated to stay in sync when data that’s retrieved from a server not under my control changes.
The code block that follows SHOULD, based on the tutorials I’ve followed, documentation I’ve read, and answered questions I’ve found here on S.O., do what I’m attempting. But it doesn’t.
PART of it works correctly – The “go get data” part works fine, and several of the Views show me the right stuff – but I still don’t get properly updated SwiftUI Views when the data changes.
Here’s the code I believe to be relevant – No, it’s NOT the entire codebase! Gawd, how I wish it were!
import SwiftUI
// Note that for both brevity and NDA compliance, I won't be showing the full InventoryElement
// struct (NOT class - I'm not certain if that distinction is important or not - some sources
// say it doesn't matter, some say it matters a little, others say it matters A LOT, and I can't
// decide which to believe), or the internal workings of loadData(), both of which would expose
// proprietary information, and likely cost me at least a chewing-out, if not my access to the
// server that provides the data - just take it on faith that I've verified that after a call to
// loadData(), "theInventory" either holds a valid array of InventoryElement structs - one or more
// of which will have almost certainly changed between calls, or the program has been intentionally
// crashed by calling the built-in "fatalError()" routine after detecting a problem. (One that's
// almost certainly network related)
class ObservableInventory: ObservableObject {
@Published var theInventory:[InventoryElement] = loadData() // Initial data load
private var reloadTimer:Timer = Timer() // We're gonna need a timer...
init() {
// Set up the timer to fire periodically, repeating forever or until self.shutDown() is called,
reloadTimer = Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { Timer in self.fireTimer()
}
}
func fireTimer() {
self.theInventory = loadData()
}
func shutDown() {
reloadTimer.invalidate()
}
}
// MARK: - my ContentView
struct ContentView: View {
@ObservedObject var theInventory:ObservableInventory = ObservableInventory()
// First things first - get a fresh new ObservableInventory object, and in the process, start
// its reload timer so that it periodically updates the data it holds.
var body: some View {
// ... elide irrelevant stuff
TabbedRootView(theInventory:theInventory)
// ... elide more irrelevant stuff
}
}
// MARK: - my TabbedRootView
struct TabbedRootView:View {
@StateObject var theInventory:ObservableInventory
var body: some View {
// ... elide code that's irrelevant to the question
GenericRoundsPage(theInventory:theInventory)
.tabItem {
Text("Generic Rounds")
}
// ... elide more irrelevant code
}
}
// MARK: - my GenericRoundsPage
struct GenericRoundsPage: View {
@StateObject var theInventory:ObservableInventory
var body:some View {
// Up to this point in execution, various "dump values to the debug console for inspection"
// checks verify that the "TheInventory" object is indeed getting updated with good data each
// time loadData() runs. These next three lines just dump the appropriate data to the debug
// console so I can see what it is at runtime "SRDL1" is the "sku" String for a good item to
// use for checking validity, since it's one of the most volatile (and thus, more likely to
// show a change between invocations of loadData()) when it comes to its "ask" and "bid"
// properties.
let theIndex = theInventory.theInventory.firstIndex {$0.sku == "SRDL1"} // Figure out the index into the array
let _ = print("GenericRoundsPage - Ask: (theInventory.theInventory[theIndex!].ask)") // Show the info held in ask and
let _ = print("GenericRoundsPage - Bid: (theInventory.theInventory[theIndex!].bid)") // bid on the debugger console
// This is showing that, as expected, the values held in
// theInventory.theInventory[<insert the index of the item whose sku is "SRDL1">].ask
// and
// theInventory.theInventory[<insert the index of the item whose sku is "SRDL1">].bid
// are changing with almost every invocation of loadData()
// Now that we've verified that, we hand ItemDetailView the item so it can display "The Full Report".
ItemDetailView(item:theInventory.theInventory[theInventory.theInventory.firstIndex {$0.sku == "SRDL1"}!])
}
}
// MARK: - my ItemDetailView
struct ItemDetailView: View {
@State var item:InventoryElement
var body: some View {
VStack {
Spacer()
Image(item.sku) // Your basic "show the image for the item matching the sku String" view - works fine
.resizable()
.scaledToFit()
Spacer()
ImageDisclaimer() // Just some boilerplate as a Text() view - works fine
Spacer()
VStack {
Spacer()
ItemSKUView(item: item) // The item's sku String as a Text() view - works fine
Text(" ")
DescriptionView(item: item) // A Text() view of item's name or description, whichever
// is more appropriate.
AskView(item: item) // Here's where things go wonky - the values displayed by AskView
BidView(item: item) // and BidView are correct - THE FIRST TIME THROUGH. After that,
// they never get updated, no matter how item.ask or item.bid change
// due to the timer firing and loadData() running. Yes, I have verified
// (by dumping to the debug console similar to what's shown in the
// GenericRoundsPage view) that item.ask and item.bid change as expected
// with nearly every call to loadData() - but the displayed values DO NOT
// update after they're first displayed.
Spacer()
}
}
}
}
// MARK: - my ItemSKUView
struct ItemSKUView: View {
@State var item:InventoryElement
var body: some View {
HStack {
Text("SKU: ")
Text(item.sku)
}
}
}
// MARK: - myDescriptionView
struct DescriptionView: View {
@State var item:InventoryElement
var body: some View {
Text(item.description ?? item.name)
}
}
// MARK: - my AskView
struct AskView: View {
@State var item:InventoryElement
var body: some View {
var _ = print("AskView item.Ask value: (item.ask)")
// Shows me that item.ask is exactly what I expect it to be - basically, "something different
// than last time" - after each invocation of loadData()
HStack(alignment: .center) {
Spacer()
Text("Ask Price:")
Spacer()
Text(String(format: "$%.2f", item.ask)) // But it NEVER GETS UPDATED after the initial
// display - If the initial value is $31.50,
// that's what gets displayed forever - it doesn't
// matter that the next time loadData() runs,
// the value in item.ask changes to $2.39 - $31.50
// is what's shown as long as the ItemDetailView
// remains visible.
Spacer()
}
}
}
//MARK: - my BidView
struct BidView: View {
@State var item:InventoryElement
var body: some View {
var _ = print("BidView item.Bid value: (item.bid)")
// Same as in AskView - the value of item.bid changes on (pretty much) every invocation
// of loadData(), which is exactly what I expect to see.
HStack(alignment: .center) {
Spacer()
Text("Bid Price:")
Spacer()
Text(String(format: "$%.2f", item.bid)) // And just like AskView, what gets shown never changes
// after the first time the item is displayed.
Spacer()
}
}
}
Where am I going wrong? Obviously, I’m not doing something right, but for the life of me, I can’t figure out what/where the problem is.
So I’m invoking Linus’s Law here in hopes of finding out that this bug truly is shallow, and how to squish it.
Anyone?
(Before anybody goes off on how I should be using MVVM or similar, Yes, I know. That’s on the docket as a refinement to be worked on after I figure out how to actually get basic functionality)