Hello, enjoying TCA architecture so far.
I have a question related with achieving similar goal with CombineLatest in TCA. Let's say when the splash screen appears, the screen must be shown to the user for at least 3 seconds, and while the splash screen is shown, the app throws an effect that contains an api call to get user information. So there can be 2 cases:
- When the getUser response comes within 3 seconds, we wait until the minimum 3 seconds to pass and move on.
- When the getUser response comes after 3 seconds, we move on as soon as we receives the response.
I know that this can be done by changing a state variable(e.g. threeSecondsPassed, gotUserResponse boolean) when 3 seconds has passed and the response is received respectively and whenever we receive a response we can check if both state variable are set to true and move on. However, I want to implement a cleaner solution like using combineLatest so that we can call an action to move on whenever we receive respective values from timer publisher, and getUser response publisher. I also know that the easiest way is to put a delay 3 seconds on receiving the response, but in this way we have to wait more than 3 seconds. Below is the pseudo code I initially thought, after checking Timer example in the case study:
//inside reducer
case .onAppear: //called when splash view is shown
let combinedPublisher = Publishers.CombineLatest(
Effect.timer(id: TimerId(), every: 3, tolerance: .zero, on: environment.mainQueue).eraseToAnyPublisher(),
environment
.userClient
.getUser()
.subscribe(on: environment.backgroundQueue)
.receive(on: environment.mainQueue)
.catchToEffect()
.eraseToAnyPublisher())
return combined
.map { (time, response) -> Result<User, Error> in
return response
}
.eraseToEffect()
.map(AppAction.userResponse) //userResponse is the action used to move on from splash view
However, this code doesn't work as we can't get the response from the combinedPublisher. What would be an ideal implementation to tackle this in TCA?
1 Like
mayoff
(Rob Mayoff)
2
What you'd like is an after operator that defers a publisher's output until after a specific date, rather than by a fixed delay. Then you could write this:
case .onAppear:
let q = environment.mainQueue
let when = q.now().advanced(by: .seconds(3))
return environment.userClient
.getUser()
.subscribe(on: environment.backgroundQueue)
.receive(on: q)
.after(when, on: q) // <-- this is the new operator
.catchToEffect()
.map(AppAction.userResponse)
Here's a simple implementation of the after operator. Note that this is not thread-safe, so it's important to use after downstream of receive(on: q). Also, to understand this implementation, you need to understand that if you ask a scheduler to run a callback at a time in the past, the scheduler will run the callback as soon as possible.
struct AfterPublisher<Upstream: Publisher, Context: Scheduler>: Publisher {
typealias Output = Upstream.Output
typealias Failure = Upstream.Failure
let upstream: Upstream
let context: Context
let when: Context.SchedulerTimeType
func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input {
let subject = PassthroughSubject<Output, Failure>()
var isLive = true
var ticket: AnyCancellable? = nil
subject
.handleEvents(receiveCancel: {
isLive = false
ticket?.cancel()
})
.subscribe(subscriber)
// The subscriber might have cancelled the subscription synchronously.
guard isLive else { return }
ticket = upstream
.sink(
receiveCompletion: { completion in
context.schedule(after: when) {
if isLive {
subject.send(completion: completion)
}
ticket = nil
}
},
receiveValue: { value in
context.schedule(after: when) {
if isLive {
subject.send(value)
}
}
})
}
}
extension Publisher {
func after<Context: Scheduler>(_ when: Context.SchedulerTimeType, on context: Context) -> AfterPublisher<Self, Context> {
return AfterPublisher(upstream: self, context: context, when: when)
}
}
@mayoff I see, this is a great way to solve this problem. Thank you!
I just found out that in my original pseudo code, simply adding ".print()" at the end of the combinedPublisher make everything work, but without .print() the code doesn't work. Does anyone know why this is happening?