I'm encountering difficulty updating items within a VStack
in my SwiftUI
app. Each item (ListItemView()
) displays a label showing the remaining time. I'm struggling to find an approach to achieve this for each item in a VStack
(specifically a LazyVStack
) on the screen.
Here's how my ViewModel is structured:
@MainActor
final class MyViewModel: ObservableObject {
private var cancellables: Set<AnyCancellable> = []
private (set) var models: [MyModel] = []
@Published private(set) var listItemModels: [MyListItemModel] = []
init() {
Timer.publish(every: 1, on: .main, in: .common)
.autoconnect()
.sink { [weak self] _ in
self?.updateModelsTimeLeft()
}.store(in: &cancellables)
}
func fetchData(limit: Int) async {
do {
let models = try await APIService.fetchModels()
self.models = models
listItemModels = models.map { MyListItemModel(id: $0.id,
timeInMs: $0.timeInMs)}
} catch {
print("Error fetching models: \(error)")
}
}
}
private extension MyViewModel {
func updateModelsTimeLeft() {
for index in 0..<self.listItemModels.count {
self.listItemModels[index].timeInMs = // changed based on some logic
}
}
}
}
The MyListItemModel used in the ViewModel is defined as follows:
struct MyListItemModel: Identifiable {
let id: Int
var timeInMs: Int
}
I'm attempting to display these items on the screen using:
LazyVStack(alignment: .center, spacing: 16) {
ForEach(viewModel.listItemModels, id: \.id) { model in
ListItemView(model: model)
}
}
Inside the ListItemView, I'm simply printing the value inside a Text() view.
The problem happens with Timer
publisher I guess: the UI doesn't update at all.
Any suggestions on how to properly update the UI in SwiftUI when using Timer publisher for this scenario would be greatly appreciated!
Edit:
Not sure how this can be important, but this is how I fetch data:
// This is on my main screen's view.
.onAppear {
Task {
await viewModel.fetchData(limit: 20)
}
}
To fetch data it's like this:
struct Fetcher {
// in Swift, funcs should always return a result or throw
func fetchData(limit: Int) async throws -> [Model] {
return try await APIService.fetchModels() // normally you wouldn't use another object
}
}
@State var models: [MyModel] = []
...
.task {
let fetcher = Fetcher() // good idea to make this an @Environment so can be swapped out for Previews
do {
models = await fetcher.fetchData(limit: 20)
}
catch {
// normally this error would be saved in a @State and shown in a Text
print("Error fetching models: \(error)")
}
}
.task
removes the need for a @StateObject
to manage the lifetime of the async work.
To transform models into certain Views use a computed property like this:
var listItemModels: [MyListItemModel] {
models.map { model in
MyListItemModel(id: model.id, timeInMs: model.timeInMs)
}
}
...
MyList(listItemModels: listItemModels)
I'll think about your timer, maybe another .task
with a while loop with Task.sleep
.