Try swift nyc krunoslav zaher header

Modern RxSwift Architectures

This talk explains a cross platform architecture that works really well with RxSwift and other Rx implementations.


Introduction

Hi, my name is Krunoslav Zaher. I’ve been working on RxSwift for approximately three years, and also RxFeedback. I’m going to talk today about how to use Rx in more stateful environments like iOS applications.

The title of this talk is “Modern RxSwift Architecture”. Why do I use words like “modern” and “architecture” when RxSwift is just three years old? The Reactive community is a bit older. There are many common Rx patterns that you can find online, but we found that these patterns didn’t work well for us. Lately, a lot of libraries have appeared that are similar in that they use more coarse-grained state. RxFeedback is one of those libraries. There’s also Redux-Observable, ReactorKit, etc.

This talk covers RxFeedback and assumes that you know at least something about RxSwift, or some other Reactive libraries.

Cyclic Data Dependencies

Reactive libraries are straightforward to use for networking, and in some stateless environments:

fetch(resource: "http://my.io")
	.map{ ... }
	.flatMap{ ... }
	.map{ ... }
	.flatMap{ ... }
	...

Things like .map and .flatMap work pretty well and usually don’t cause issues. But if you’ve ever worked on an app, you might have run into issues with cyclic data dependencies.

Let’s say you have some kind of standard problem that has cyclic data dependencies. For example, you are working on an app that uses a paginated API. You’re performing a search, and you know that when you scroll to the bottom you’ll need to load the next page, then scroll again, then load the next page. This creates a circular data dependency.

Suppose you are using a GitHub API. You get the URL of the next result set, and it is stored somewhere in some state. Then you use that URL to make an API call. Then you get the new URL, and you somehow have to replace it with the old one. That’s another common type of cyclic data dependency.

What I would now do is create a high level description of the system first with a struct that describes this scenario:

struct Search {
	/// view model
	let query: String
	let results: [String]
	/// effects
	let nextPageURL: URL?
	let loadNextPage: Bool
}

/// state queries
extension Search {
	var isLoadingNextPage: URL? {
		return loadNextPage ? nextPageURL : nil
	}
}

You have a query that you’ve entered in some text box. You have results that represent the current page results. You have the URL of the next page to load, a boolean that indicates whether you should start loading next page. You’ll notice that this state contains descriptions of everything. It contains the view model and description of effects, basically everything.

Get more development news like this

At the bottom, you see something we call state queries. So if you’re using something like RxFeedback and you are storing your state in a database, then you’ll use normal database queries. If you’re using a struct to store your state, then you’ll use some kind of calculated properties to pull out the interesting data from the struct that controls the loading of the next page. But how do you actually perform those queries? This is how you load the next page:

let searchFeedback: FeebackLoop<Search, Event> =
	react(
		query: { $0.isLoadingNextPage },
		effects: { nextPageURL in
			return load(resource: nextPageURL)
				.map(Event.loaded)
		}
	)

Don’t worry about what a FeedbackLoop is for now – first let’s demonstrate that this code is readable, and after that we’ll discuss what it does.

How do you read something like this? There is something that creates a FeedbackLoop. There is a FeedbackLoop factory that creates a FeedbackLoop that reacts to this query. The query can return either a nil or a URL that you need to load. Once you have that URL, then you load it through some API. Once you’re done loading it, then you map it to an event that says “okay I’ve loaded this data, and I’m done”, and then you return that data to the FeedbackLoop. This is the basic structure of all feedback loops. Hopefully you can understand what’s happening here without knowing anything about RxFeedback.

Now let’s look at how this actually works. Here is what the basic structure for RxFeedback looks like:

If you don’t have state, then you don’t have a problem, right? Then you’re doing things like map and flatMap, so you obviously need to have some kind of state. It’s a good idea to put all of that state in one place. That’s the big green box there. At the bottom you see the FeedbackLoops:

typealias FeedbackLoop<State, Event> = (Observable<State>) -> Observable<Event>
typealias FeedbackLoop<State, Event> = (Driver<State>) -> Driver<Event>

Feedback loops are just lambdas that take that state, observe it, and after that, depending on that state, they perform different side effects. For example, you could use that state to make the UI, and then observe events in the UI, or control some timers, or communicate with Bluetooth layers or do some networking. It can be really any kind of effect, and the result of these effects produce events, and those events mutate the state.

I’m sure you’ve seen similar architectures before. This is kind of both an architecture and an operator. If you have a really large state, you can store that state in a database or anywhere else, and then you can use queries to observe that state. Or, if that state is manageable, you can use the system operator. This is the public interface of system operator:

func system<State, Event>(
	initialState: State,
	reduce: (State, Event) -> State,
	feedback: FeedbackLoop<State, Event>...
) -> Observable<State>

Why did we create the system operator? There isn’t any operator that enables you to model circular data dependencies in RxSwift or any other Reactive library currently, as far as I’m aware. So, we just created one!

To define this programmatically, you need an initial state from which to start. Then you need a reducer that combines the events and state. If you’ve ever done array.reduce in regular Swift, you might be familiar with the concept – it’s exactly the same. There are also the FeedbackLoops that represent how you do effects depending on the state.

At least 90% of feedback loops have exactly the same pattern. You have a query upon state, and that query returns something that is nullable. If the value returned is nil, then it’s assumed that you shouldn’t be performing any side effects. If the query returns some value, then that value is passed to the effects lambda that describes what you’re doing. If you’re doing a network API call, you get the URL, and then you get an observable that performs those events, and after you’re done with them, you produce an event. This is a really common FeedbackLoop.

The other FeedbackLoop that’s really common is when you want to bind the state to the UI, observe UI events, and mutate the state that way. It’s similar to this one. You can check it out in the repo.

The above two cases are the common cases that probably cover 98% of all of the FeedbackLoops you’ll ever need in your app. You can also always build one manually, but there is no need to do that.

Performance

Because all of the state is in one place, we often get the question “Does this perform well?”. There are some other architectures that do this, and so far we haven’t encountered any performance issues. If we have a really big state, we just put it into a database. If the state is small, then we use the system operator. Even if you run into a performance issue, there are standard Redux tricks that can be used to improve performance.

Another question that we get is how does this differentiate from Redux and Elm architectures? One of the differences is that we are using part of the state to describe which effects should be executed currently. Another difference is that Elm and Redux both assume that there is a view in which you’re observing the state. For example, in Redux you have some kind of middleware that is between view and state. But what if you don’t have a view? In some cases we don’t actually have any view. As to side effects, Elm has an additional return value from the reducer. So this is a bit different from both of those architectures.

Another difference is that Redux and Elm both have a really big state that you need to have at application level. You can also do that with RxFeedback if you like. It shouldn’t be a problem if you manage to somehow figure out how to conquer the navigation controller. You can also use a more fine-grained approach; you can have a state for each view controller. Then you can update the state per view controller, so you don’t have to constantly push your changes to your entire application state and do a lot of boilerplate code to propagate events for some view controller. It works out really well.

You can’t actually figure out the percentage of code in each of those parts from this diagram. The important thing is that for state and events that are pure Swift code, it’s approximately 5% of your code base - that is really small! We’ve proved that empirically.

That small amount of Swift code contains a very high level abstraction of your entire system, and it’s easy to read it. I spend probably 90% of my time reading someone else’s code, so this is extremely helpful. I only need to study 5% of the code, and I know what it’s doing. FeedbackLoops are basically 90% of it, and then you have a lot of boring boilerplate code where you need to map data to the UI, to use adapters for networking, etc. Those are the kind of implementation details you usually don’t care much about and you can easily find the FeedbackLoop that performed the effect that you need to investigate.

One of the benefits of using this pattern is that it normalizes architecture across the team. If you’ve used any kind of Reactive library in a team environment, you’ll find each team member will create a completely different solution. That’s bad, because when you do a code review, it’s not easy to figure out what they were doing. This makes your code base far more consistent. Since you have that small state, it’s really easy with a pull request to figure out what your coworker is trying to do. Everybody can agree about how they are working with effects, and you can even create a cookbook.

So if something is a state, then put it into a state struct. If something is a way to change state, create an event. If something is an effect, then just create another FeedbackLoop for that effect.

Another benefit to using RxSwift architecture is in the way that people handle cyclic data dependencies. It is very common for people to solve this by making subscription calls. If you are doing a lot of subscription calls, you’re probably using Rx in a non-optimal way, because you are leaving the monad, and you are hurting some of the compositional properties of the library itself. This architecture avoids those subscription calls.

We are trying to help people use Rx in a more idiomatic way. This is something that’s usually an afterthought, but it’s really important in mobile apps.

If you are working on a webpage, then the assumptions are a bit different. If you have a long-lived and long-running background process in your app, what happens when the user closes the app, or the app crashes, or you have some other problem? When the app restarts, usually you expect that all of that work is resumed automatically in the background. If you are using RxFeedback, then you have written serialized events, and you would just resume. There is no extra work. There is no extra code that needs to be added.

There is another minor thing that’s really simple to do. It was probably more important previously, when you had resource-constrained devices. When your view is not visible, the device should stop spending CPU time on it. The state of the system is stored, and when navigating back to the view controller, the app should continue where it left off. There is no reason to use expensive resources in the background. RxSwift gives you really nice properties for that. For us, this is probably the most important feature is resuming background operations after crashes.

Testing

RxSwift is awesome for testability. Redux also has similar properties. You can test the reducer. You can test those calculated properties on state. FeedbackLoops are an easy way to do dependency injections. You have a separation between the high level abstract representation of your system, and dependencies that perform those effects. You can either mock those feedback loops inside your unit test (just create a lambda) or you can use the same functions and mock the resources in some other way – it works the same.

But we actually go a bit further. Here is a typical example of the code that one would write, for example, to fetch something from a server:

let results = query.flatMapLatest { query in
		return search(query)
	}
	.observeOn(MainScheduler.instance)

let results = query.flatMapLatest { query in
		return search(query)
			.observeOn(MainScheduler.instance)
	}

You’re observing some kind of text box and query. It doesn’t matter what Reactive framework you are using. It’s about the same in any of them. There are two ways that you can change the operators. You can put observeOn on the outer sequence, or you can put it on the inner sequence. One of these is actually wrong, and you can get stale data!

The first code sample is actually what I’ve seen people use most, but it’s wrong. What happens is that after the cancellation logic, you have an event that could be enqueued on the main dispatch queue, and you could actually receive that stale event.

The correct one is the second code sample, because even though this is enqueued, the cancellation logic will properly be able to cancel the event if the query changes, so you won’t get any stale events.

If you aren’t thinking about any of this, but you’re just using the code that we’ve provided, we’ve handled all of these edge cases for you. By using RxFeedback’s built-in feedback loops, cancellation works automagically on a system level, and stale events can’t be delivered to a reducer.

Reentrancy

Another big topic is reentrancy. This will bite you if you have a fairly complex app, and that’s probably the worst time to start thinking about it! Here is a simple example of how you can create reentrancy issues:

let state = BehaviorSubject(value: 1)

_ = state
	.subscribe(onNext: { newState in
	state.on(.next(newState))
})

You are observing a state, and when a new value arrives you immediately change the state. The state fires a new event. You send the next event, etc., and this will probably crash with a stack overflow really soon. This is basically what reentrancy means. If you’re using Rx in debug mode, then it will display a message “Reentrancy anomaly was detected”. You can replace the message with a crash if you like, but that might be a bad thing to do in a production environment.

So why is reentrancy problematic for all Reactive libraries? They all work with something called observable sequences. With observable sequences, there’s an assumption that there is an ordering between elements. Suppose it takes some time to process an element. If you receive a new element on the same thread, it’s not really clear what should be done with the two elements at that point. The way Rx handles this currently is to try to do the sanest thing it can. It’s not possible to cover every possible edge case. The best way would be to avoid having reentrancy problems altogether. You can change the behavior and crash the app in that case.

Proving that your app doesn’t have reentrancy cycles is far from trivial. You can’t do it for your entire app, but you can prove it on the level of the system operator. The system operator and feedback loops are designed so that reentrancy issues are handled internally for you correctly, and in a way that doesn’t negatively affect cancellation guarantees.

This code sample is the feedback loop that you saw previously. I’ve swapped that out with something that immediately returns a value:

let searchFeedback: FeebackLoop<Search, Event> =
	react(
		query: { $0.isLoadingNextPage },
		effects: { resource in
			return Observable.just(results)
		}
	)

When could this happen in a real life app? If you have something cached, you can immediately read something from cache, return it, and if there aren’t any special methods of handling these cases, this will immediately cause the reducer to run. You’ll get a new state, and you could run into issues. React feedback loops actually handle all of these kind of edge cases for you.

Other Benefits

There are lots of other benefits:

  • Easier debugging and remote debugging, just log all of the events or put a breakpoint in the reducer
  • Easy cross compiling and sharing of core logic between different platforms
  • Easier sub-system embedding
  • Straightforward integration with non Rx code
  • Get activity indicators for free

Check out the repo and experiment with the code. I’m hoping you’ll get some benefit from this library and this talk. Thank you very much!

Next Up: RxSwift from Start to Finish #1: Everyday Reactive

General link arrow white

About the content

This talk was delivered live in September 2017 at try! Swift NYC. The video was recorded, produced, and transcribed by Realm, and is published here with the permission of the conference organizers.

Krunoslav Zaher

Krunoslav Zaher worked on various projects for the past 16 years (augmented reality engines, BPM systems, mobile applications, bots …). Recently, he is studying functional programming and modeling systems in a declarative way using observable sequences. He’s the initial committer in the RxSwift repository. He’s helping out bootstrapping an ecosystem inside the RxSwiftCommunity, and sharing architecture ideas in the RxFeedback repository. Currently, he is building systems at the YC well-being startup Bellabeat.

4 design patterns for a RESTless mobile integration »

close