Embedding reagent in iOS application architecture

  • Tutorial
Most articles on functional reactive programming are limited to demonstrating the capabilities of a particular tool in a particular task and do not provide an understanding of how to use all the power in a project.

I would like to share the experience of designing using functional reactive programming for iOS. It does not depend on the chosen tool, whether it be RAC , RxSwift , Interstellar or something else. This also applies when developing for MacOS.

At certain points, I will write using Swift + RAC4, as these are my main tools at the moment. However, I will not use the terminology and features of RAC4 in the article.

Maybe you were in vain abandoning reactive programming and it's time to start using it?

To begin with, briefly about myths among people who only heard about the reagent and heard not the best:

Myth 1 - The threshold for reactive programming is too high.


No one says that you need to use all available features from the first minutes. You just need to understand the concept and the basic basic operators (below I will write about 4 minimally necessary operators, and you will come to the rest as you solve various kinds of problems).
You do not have to spend months and years on this, just a few days / weeks are enough (depending on the available background), and if you have an experienced team, entering will be much faster.

Myth 2 - the reagent is used only in the UI layer


The reagent is convenient to use in business logic, and below I will show what we will get from this.

Myth 3 - Reactive code is very difficult to read and parse.


It all depends on the level of code written. With proper separation of the system, this improves the understanding of the code.
Moreover, it is not much more complicated than using many calbeks.
And you can almost always write unreadable code.

Reagent concept


The process of writing reactive code is similar to a children's game in a trickle. We create a path for water and only then we start water. Water in our case is computation. Network request, receiving data from the database, obtaining coordinates and many other things. The path for water in this case is the signals.

So, a signal is the main building block containing certain calculations. In turn, certain operations can be performed on signals. Applying the operation to the signal, we get a new signal, which includes the previous configurations.

Applying operations on signals and combining with other signals, we create a data stream for calculations (dataflow). All this thread starts its execution at the moment of subscribing to the signal, which seems to beto lazy computing. This makes it possible to more finely control the moment of the start of execution and subsequent actions. Our code is divided into logical parts (which increases readability), and we get the opportunity to create new "methods" literally "on the fly", which increases the reuse of code in the system. Sounds like a higher order function, doesn't it?

The minimum necessary operations for configuring dataflow for the first time are map , filter , flatMap and combineLatest .
And finally, a small feature, dataflow is data + errors , which makes it possible to describe the sequence of actions in 2 directions.

This is the minimum necessary theory.

Reagent and Modular Architecture


Take SOA as an example , but of course, this does not limit you at all.



The scheme used may differ from others or be the same, I do not pretend to the standard, as I do not pretend to be universal. In most of our tasks, we adhere to this solution, hiding data processing behind the service (and this does not have to be network requests).

Transport


So this is our first contender for reactivity. Therefore, I will dwell on this place in more detail.
First, let's look at typical solutions to this problem:

Using 2 calbeks
    typealias EmptyClosure = () -> ()
    func getReguestJSON(urlPath: String, parameters: [String : AnyObject]?, success: EmptyClosure, failed: EmptyClosure) -> NSURLSessionTask


Using 1st Callback
    typealias Response = (data: NSData?, code: Int)
    typealias Result = (response: Response, failed: NSError?)
    func getReguestJSON(urlPath: String, parameters: [String : AnyObject]?, result: Result) -> NSURLSessionTask


Both solutions have their advantages and disadvantages. Consider both solutions within the framework of the task: show the loader for the duration of the network request.
In the first case, we clearly divide the actions into success and failure of the action, however, we will duplicate the code that reports the completion of the action.
In the second case, we do not need to duplicate the code, however we mix everything into one heap.

And you also need the ability to cancel the network request, and + you need to encapsulate the operation of the Transport.
Most likely, in this case, our code will look approximately

like this:
protocol Disposable {
    func dispose()
}
    typealias Response = (data: NSData?, code: Int)
    typealias Result = (response: Response, failed: NSError?)
    func getReguestJSON(urlPath: String, parameters: [String : AnyObject]?, result: Result) -> Disposable?
...
...
...    
    typealias EmptyClosure = () -> ()
    func getReguestJSON(urlPath: String, parameters: [String : AnyObject]?, success: EmptyClosure, failed: EmptyClosure) -> Disposable?


Now let's look at a solution using signals
    func getRequestJSON(urlPath: String, parameters: [String : String]) -> SignalProducer {
        return SignalProducer { observer, disposable in
            let task = ... {
                observer.sendNext(data: data, code: code) 
                observer.sendCompleted()
                //or observer.sendFailed(error)
            }
            disposable.addDisposable {
                task.cancel()
            }
        }
    }


Earlier, I deliberately missed one important point - when creating a signal, we not only write what to do when subscribing to a signal, but also what to do when the signal is canceled.
Subscribing to a signal returns an instance of the Disposable class (not written by us above, more), which allows you to cancel the signal.

Code example
    let disposable = getRequestJSON(url, parameters: parameters) //создали сигнал
        .startWithNext { data, code in
            ...
            ...
            ...
    } //с момента вызова startWithNext начался выполнятся сигнал
    disposable.dispose() //отменяем выполнение сигнала


Now the called party can easily delay the execution of the request, combine the result with other requests, write some actions on the signal events (from the example above to complete the request), as well as what to do when receiving data and what to do when an error occurs.

But before demonstrating such code, I would like to talk about such a concept as

Side effect


Even if you did not come across this concept, then 100% observed it (or you looked here by accident).
In simple language, this is when our flow of computing depends on its environment and changes it.



We try to write signals as a separate piece of code, thereby increasing its possible reuse.
However, side effects are sometimes necessary and there is nothing terrible about it. Let us see in the figure how we can use Side Effect in reactive programming:



Quite simple, isn't it? We wedge between the execution of signals and perform certain actions. In fact, we perform actions on certain signal events. But at the same time, we keep the signals still clean and ready for reuse.

For example, from a previously created task: "Show the loader at the start of the signal and remove at the end."

Parsing



Recall a typical situation - the data from the server either came in the correct or in the wrong format. Possible solutions:
1) Calbeki “data + error”
2) Apple's approach using NSError + &
3) try-catch

What can the reagent give us?
Let's create a signal in which we will parse the response from the server and give the result in certain events (next / failed).
Using the signal will make it possible to more clearly see the work of the code + combine the work with the signal of the network request. But is it worth it?

Example
class ArticleSerializer {
    func deserializeArticles(data: NSData?, code: Int) -> SignalProducer<[Article], NSError> {
        return SignalProducer<[Article], NSError> { observer, _ in
            ...
            ...
            ...
        }
    }



Services



Combine the network request, parsing and add the ability to save the parsing result in the DAO .

code example
class ArticleService {
    ...
    ...
    ...
    func downloadArticles() -> SignalProducer<[Article], NSError> {
        let url = resources.articlesPath
        let request = transport.getRequestJSON(url, parameters: nil)
            .flatMap(.Latest, transform: serializer.deserializeArticles)
            .on(next: dao.save)
        return request
    }



No nesting, everything is very simple and easy to read. Generally very consistent code, isn't it? And it will remain just as simple, even if the signals are executed on different threads. By the way, consider using combineLatest:
Parallel Query Sync
userService.downloadRelationshipd() //сигнал с сетевым запросом
	.combineLatestWith(inviteService.downloadInvitation()) //сигнал с сетевым запросом + запустить параллельно
	.observeOn(UIScheduler()) //результат сигналов вернуть на главный поток (неважно на каком будут выполнятся)
	.startWithNext { users, invitations in
		//работаем с результатом операций
	}


It is worth noting that the code written above will not start executing until it is launched by subscribing to the signal. In fact, we only indicate actions.

And now services have become even more transparent. They just connect parts of business logic (including other services) and return dataflow of these connections. And a person using the received signal can very quickly add a reaction to events or combine with other signals.

And also ...



But all this would not be so interesting if not for the many operations on the signals.
Set the delay, repeat the result, various combination systems, set the receipt of the result to a specific stream, convolutions, generators ... You just look at this list .

Why didn’t I talk about working with UI and binding, which for many is the most “juice”?
This is a topic for a separate article, and there are quite a lot of them, so I’ll just give a couple of links and end the

Best World with ReactiveCocoa
ReactiveCocoa. Concurrency. Multithreading

That's all for me. Instead of a useless conclusion, I will leave a few practical conclusions from the last project:
1) Obtaining Permission has worked well as a signal.
2) CLLocationManager behaved perfectly with signals. Especially the accumulation and editing of points.
3) It was also convenient to work with signals for such actions as: selecting a photo, sending SMS and sending email.

Also popular now: