Looking for the right timer pattern (update UI based on elapsed time) #1909
-
Having navigated to a detail view, I've got fields that display the state of the item being viewed and I've got buttons to update that state. I'd also like to have a field that displays the elapsed time since the last update. To do that, I'm obviously going to need a way of getting "now" (easily injected via struct ListFeature: Reducer {
struct State: Equatable {
var items: [Item]
var selectedItem: Item.ID?
...
}
}
struct DetailFeature: Reducer {
struct State: Equatable {
var item: Item
var elapsedTime: String = "--"
}
enum Action {
case tick
case updateItemCount(Int)
}
@Dependency(\.date) var dateGen
func calculateElapsed(_ item: Item) -> String {
let now = dateGen.now
let elapsed = now.timeIntervalSince(item.lastUpdatedAt)
return "\(elapsed) seconds"
}
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case .tick: // <-- How do I generate these and send them to the store, but only if there's a currently selected item?
state.elapsedTime = calculateElapsed(state.item)
return .none
case let .updateItemCount(newCount):
state.item.count = newCount
state.item.lastUpdatedAt = dateGen.now
return .none
}
}
}
} |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 9 replies
-
Hey @sbeitzel! There are two approaches to your problem. The first one consists in performing this operation in TCA, the other one, in SwiftUI (for example if the elapsed time value doesn't participate to your business logic). For the first approach, you will need a timer. The simplest is probably to use @Dependency(\.continuousClock) var clock
var body: some Reducer {
Reduce { state, action in
switch action {
case .task:
return .run { send in
for await _ in self.clock.timer(interval: .seconds(1)) {
await send(.tick)
}
}
case …
}
}
} For the second approach, you can for example rely on some specific Text(timerInterval: viewStore.lastUpdateDate...Date.now) Alternatively, you can spawn another timer in a task where the @State var elapsedSinceLastUpdate: Duration?
var body: some View {
VStack {
if let duration = elapsedSinceLastUpdate {
Text(duration.formatted(.time(pattern: .minuteSecond)))
}
}.task(id: viewStore.lastUpdateInstant) {
let lastUpdateInstant = viewStore.lastUpdateInstant
let timer = ContinuousClock().timer(interval: .seconds(1))
self.elapsedSinceLastUpdate = nil
for await tick in timer {
self.elapasedSinceLastUpdate = lastUpdateInstant.distance(to:tick)
}
}
} I didn't check if this code builds. In particular, the |
Beta Was this translation helpful? Give feedback.
-
What's the right way to run timer on iOS 14 |
Beta Was this translation helpful? Give feedback.
Hey @sbeitzel! There are two approaches to your problem. The first one consists in performing this operation in TCA, the other one, in SwiftUI (for example if the elapsed time value doesn't participate to your business logic).
For the first approach, you will need a timer. The simplest is probably to use
@Dependency(\.continuousClock)
that has a timer utility. This thing is an async sequence, so you can iterate in a.run
effect that you start when the view appears (in.task
for example):