RxSwift: read-only property and read-write property wrapper

Utilize Swift 5.1 property wrapper to have elegant read-only property with read-write internal semantic

Oleksandr Stepanov
4 min readMay 25, 2020

Note: I know, that in light of Apple’s Combine framework, continue to develop on RxSwift may seem a “mauvais ton”, but far not every new project may start with iOS 13 minimal support, and I believe this will be the case at least for an year. Moreover, there are tons of existing projects which must support earlier iOS versions and still demand on reactive programming frameworks.

BehaviorRelay and RxProperty

Any Swift developer who worked with RxSwift knows that Observables and Subjects lack the ability to store the last value. Before RxSwift 5.0.0 there was the Variable generic class for this purpose, now it has been substituted with the BehaviorRelay which technically is not even part of RxSwift, but RxRelay module.

A developer who just starting to use RxSwift may be confused, what’s the difference between PublishSubject, BehaviorSubject, PublishRelay, and BehaviorRelay.

  • Publish vs Behavior. The first doesn’t store the last value, while the last - does.
  • Subject vs Relay. The last doesn’t through an error and can’t be terminated, while the first - can.

As you may already know, RxSwift is used to glue components in the app: a ViewModel with a ViewController in MVVM, an Interactor with Services in RIBs, a Middleware with a Store in Redux.

Normally, a PublishSubject is used to propagate an event, while BehaviorRelay to share some value or a state. The common interface for a ViewModel looks like:

Source

There is a slight problem with this ViewModel declaration though: its state is modifiable outside. Because even it is only a get property, .accept() method, which modifies the value, is available. This totally breaks one of the fundamental rules of OOP - encapsulation.

Someone may propose to use Observable in the protocol instead of BehaviorRelay, i.e. something like that:

Source

However, Observable does not retain the last value, so one can’t just read it at any time. In some cases, this is not convenient and may require more logic and code to workaround.

This is a well-known problem, and there is an easy solution for it - RxProperty. It is basically a wrapper around BehaviorRelay which provides only read interface, no write one. It is kind of read-only BehaviorRelay. There was plenty of discussions here and there, about adding this class to the main RxSwift module, but this did not make to happen.

With this wrapper encapsulation problem get solved:

Source

However, it is a bit ugly and inconvenient to declare a private BehaviorRelay property as a complementary accessory for each RxProperty you have in the interface. And here is where Swift property wrappers come to rescue.

Property wrappers

This Swift feature was introduced in 5.1 version and as stayed in the doc:

A property wrapper adds a layer of separation between code that manages how a property is stored and the code that defines a property.

In practice, the sense of wrappers behind this not very clear sentence is to write some property-related functionality once and use it for each property where it is applicable. As a very basic example: @UserDefault property wrapper, which adds read-write functionality to UserDefaults for a property value using some key.

I’ll not go into details of property wrappers syntax and functionality, there are a lot of blog posts on this topic in Web, in particular #1, #2.

@ReadWrite property wrapper for RxProperty

Now we come to the main topic of this story. Here is the property wrapper which adds write functionality to the read-only RxProperty:

Source

It adds .accept() method and access to the internal BehaviorRelay of the wrapped RxProperty. It all makes sense if this property wrapper is declared in the same file as RxProperty class, and _behaviorRelay has fileprivate access level.

With this small addition our ViewModel declaration could be like:

Source
  • Encapsulation principle persisted, because plain RxProperty provides only read-only access.
  • No need for a complimentary BehaviorRelay declaration makes this code just perfect 👌.

NOTE: Thanks to Alexey Naumov, using a Swift property wrapper projectedValue feature, it is possible to solve this task even in more elegant way:

Source

In this version, internal BehaviorRelay may be accessed using $ notation, like: $state.accept(newValue)

The updated RxProperty solution may be found in my fork. Feel free to ⭐️ it if you like. Thank you for attention!

--

--