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:

func task1(completion: (SuccessType) -> ())
func task2(completion: (SuccessType) -> ())

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:

What makes differences?

I think what make sense are the abstract types:

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

The next blog post, I will come up with a small comparison between RAC 2.0 and RAC 3.0.