iosarraysswiftswiftuiobservable

SwiftUI @Observable not always updating view


I'm slowly going crazy because my view doesn't always update, and I can't figure out what's causing the problem. I've added @MainActor for network calls. The picker on CreateInvoiceDetails view is empty and doesn’t show currencies on the first view appearance. This happens ~90% of the time. But if I go back and return to the screen, it shows correctly. Its not happening only with arrays its happening with pure strings.

I am creating viewmodel in HomeScreen and passing it to next view through init.

struct HomeScreen: View {
    var createInvoiceViewmodel = CreateInvoiceViewModel()
    
    var body: some View {
        VStack {
            Button {
                createInvoiceViewmodel.navPub.send(.createInvoiceGoTo(.details))
            } label: {
                Text("New Invoice")
                    .style(.labelMedium)
            }
        }
        .navigationDestination(for: CreateInvoiceFlow.self) { screen in
            switch screen {
            case .details:
                CreateInvoiceDetails(viewModel: createInvoiceViewmodel)
            }
        }
}

In CreateInvoiceDetails I tried making network call in onAppear of my view or in a task. None of the approaches work consistently.

struct CreateInvoiceDetails2: View {
    @Bindable var viewModel: CreateInvoiceViewModel
    
    var body: some View {
        VStack {
            Text(viewModel.selectedCurrency?.name ?? "-")
            Picker(selection: $viewModel.selectedCurrency) {
                ForEach(viewModel.currencies) { c in
                    Text(c.name)
                        .tag(c)
                }
            } label:{
                Text("")
            }
        }
        .task {
            await viewModel.getConstants()
        }
//        .onAppear {
//            Task {
//                await viewModel.getConstants()
//            }
//        }
    }
}

The network call succeeds, always returns the same result, and the print statement works always.

@Observable
class CreateInvoiceViewModel: BaseViewModel {
    var selectedCurrency: Currency?
    var currencies: [Currency] = []

@MainActor
    func getConstants() async {
        loadingState = .getConstants
        let response = await repository.getConstants()
        switch response {
        case .result(let everyResponse):
            guard let c = everyResponse.data?.currencies
                .map({ $0.value})
            else { return }
            currencies.removeAll()
            currencies.append(contentsOf: c)
//          currencies = c. // also not working
            print("DEBUGPRINT: currencies: ", c)
            selectedCurrency = c.first
        }
        loadingState = .no
    }
}

@Observable
class BaseViewModel {
    var loadingState: LoadingState = .no
    
    let repository = AppRepository.shared
    
    var navPub = PassthroughSubject<NavigationHelper, Never>()
}

Currency model is:

struct Currency: Decodable, Hashable, Identifiable, Equatable {
    let symbol: String
    let name: String
    let code: String
    
    var id: String {
        code
    }
}

But my view is not updating every time. Mostly the first time when I run it. Is @Observable reliable, or should I go back and use ObservedObject and @Published ?


Solution

  • An observable type should be used either as a State or Environment object in a view so change the declaration in HomeScreen to

    @State private var createInvoiceViewmodel = CreateInvoiceViewModel()
    

    You must use State & Bindable since the sub-view updates the observable object, if the sub-view is only displaying the data of the object it can be let declared.