RAC 3.0 with Login workflow
In this post, we will have a look at an example on how to use
ReactiveCocoa (v3.0) to handle
a simple Login workflow.
An example
First of all, you may wonder why we should use it. Let’s have a look at the following example.
Almost every app needs authentication, which is simply implemented by login with email and password. It is not only a network task, but also a task requiring interactions with a server. But the problem is: “every task may fail”. This leads to the fact that sometimes we spend more time handling failures than successful cases. These failures include network failures and server-interaction failures.
Traditional version
func tapLoginButton() {
...
YourAPI.login(loginParameters) { (result, error) in
if error != nil {
switch error.type {
case .NetworkError:
// Handle network failure
case .IncorrectEmailOrPassword:
// Handle failure
case .InvalidInformation:
// Handle failure
// …
} else {
// Handle success
}
}
}
}
This code has a few disadvantages:
- The handling implementation is put inside a closure. If we have 10 tasks that need to be executed immediately after logging in, they have to be placed in this closure. Ugly and hard to debug, right?
- Assume we need chaining tasks: If login task is done → execute task 1. If task 1 is done → execute task 2… In that case, each task need a closure:
func task1(completion: (SuccessType) -> ())
func task2(completion: (SuccessType) -> ())
- Errors and successful results don’t exist simultaneously. The parameters declaration (result, error) seems redundant.
How to refactor?
Replace closures by Signals or SignalProducers. We will discuss the
differences between Signal and SignalProducer later.
class API {
static func login(loginParameters: LoginParameters) ->
SignalProducer<SuccessType, ErrorType> {
let signalProducer = SignalProducer<SuccessType, ErrorType> {
sink, disposable in
// For now, dont care much about `sink` and `disposable`
// Send request
// Validate request
if networkErrorOccured() {
let error = makeUpNetworkError()
sendError(sink, error)
} else if serverErrorOccured() {
let error = makeUpServerError()
sendError(sink, error)
} else {
let successResult = parseJsonAndGetSuccessResult()
sendNext(sink, successResult)
sendCompleted(sink)
}
}
}
}
func tapLoginButton() {
let loginParameters = LoginParameters(username, password)
let loginSignalProducer = API.login(loginParameters)
// Task 1
loginSignalProducer
|> start(error: { error in
handleErrorTask1()
}, next { successfulResult in
handleSuccessTask1()
})
// Task 2
loginSignalProducer
|> observe(error: { error in
handleErrorTask2()
}, next { successfulResult in
handleSuccessTask2()
})
…
// Task 10
loginSignalProducer
|> observe(error: { error in
handleErrorTask10()
}, next { successfulResult in
handleSuccessTask10()
})
}
This implementation looks more elegant since:
- Failure and success are handled separately
- Each observation is handled separately
- We dont have to take much care of asynchronous tasks
What makes differences?
I think what make sense are the abstract types:
Result: handles failuresEvent: handles asynchronous tasksSignal: handles observation for changes along with time
A little explanation
If you already heard of FRP (Functional Reactive Programming), this may help you understand more straightforwardly:
- Result<SuccessType, ErrorType> = Try[SuccessType, ErrorType]
- Event<SomeType, ErrorType> = Future[SomeType, ErrorType]
- Signal<SomeType, ErrorType> = Observable[SomeType, ErrorType]
= a series of Events
Conclusion
- Don’t waste your time implementing Observer pattern or manually handling asynchronous tasks.
- If you’re in favor of Java, a similar framework could be found as RxJava.
The next blog post, I will come up with a small comparison between RAC 2.0 and RAC 3.0.
Like what you’re reading? Buy me a coffee and keep me going!
Subscribe to this substack
to stay updated with the latest content