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 failures
  • Event: handles asynchronous tasks
  • Signal: 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.

ios

Subscribe to this substack
to stay updated with the latest content