Can’t figure out why I can’t get ObservableObjects to work as expected

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)

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật