Combining value and reference types in Swift
Basics article available: Value and Reference TypesA very common type of decision that all Swift developers have to make on an ongoing basis is whether to model a given piece of functionality or state as either a value or a reference. While values give us very clear semantics, as each value is automatically copied when passed, references enable us to establish a single source of truth — even though that might mean sharing state, which can also become a liability if we’re not careful.
However, not all of those decisions need to result in either a reference type or a value type — sometimes combining the two can give us some really powerful capabilities, and open up some incredibly interesting code design options. That’s exactly what we’ll explore this week — let’s dive in!
Collections of weak references
Often when establishing a relationship between two kinds of objects we’d like one side of that relationship to be referenced weakly (since if both sides reference each other strongly, that’d lead to a retain cycle).
For example, let’s say that we’re building a VideoPlayer
, and that we’d like to enable another object to observe it. To make that happen, we might define a class-bound observation protocol called PlaybackObserver
, and then enable a conforming object to attach itself to our video player through an observer
property:
class VideoPlayer {
weak var observer: PlaybackObserver?
...
}
Arguably, if our VideoPlayer
will maintain a 1:1 relationship to its observer, it might be more appropriate to make it a delegate instead.
While the above approach works as long as we only support a single observer to be attached to each video player, once we add support for multiple ones, things start to get tricky — as we’d have to store our observers within some form of collection, like an Array
:
class VideoPlayer {
private var observers = [PlaybackObserver]()
func addObserver(_ observer: PlaybackObserver) {
observers.append(observer)
}
...
}
The problem is that, by making the above change, our observers are no longer stored weakly (since arrays retain their elements strongly). Thankfully, that’s quite easily fixed, by combining our reference type-based PlaybackObserver
protocol with a boxing value type — which’ll simply wrap an observer instance by storing a weak reference to it, like this:
private extension VideoPlayer {
struct Observation {
weak var observer: PlaybackObserver?
}
}
With the above in place, we can now update our VideoPlayer
to store Observation
values, rather than direct (strong) references to each observer — which’ll break any possible retain cycles, since each observer is now again referenced weakly:
class VideoPlayer {
private var observations = [Observation]()
func addObserver(_ observer: PlaybackObserver) {
let observation = Observation(observer: observer)
observations.append(observation)
}
...
}
To learn more about the above way of managing observations, and the observer pattern in general, check out the two-part article “Observers in Swift”.
While the above implementation solved the problem of storing instances of a specific type in a weak fashion, let’s now say that we want to generalize that concept to let us use the same implementation within different parts of a code base. An initial idea on how to do that might be to create a generic Weak
struct, which — similar to our above Observation
type — stores a given object weakly:
struct Weak<Object: AnyObject> {
weak var object: Object?
}
However, while the above Weak
type will work great as long as we know the exact type of the object that it should store, it’ll turn out to be quite problematic for protocol types. Even though a protocol might be class-bound (in that it’s constrained to AnyObject
, which only makes it possible for classes to conform to it), it doesn’t make the protocol itself a class type. So if we tried to use our new Weak
type within our VideoPlayer
, we’d get this kind of compiler error:
// Error: 'Weak' requires that 'PlaybackObserver' be a class type
private var observations = [Weak<PlaybackObserver>]()
At this point, it might seem like all hope is lost, but it turns out that there is a way for us to achieve what we want — which involves using a capturing closure, rather than a weak
property, to implement our object reference — like this:
struct Weak<Object> {
var object: Object? { provider() }
private let provider: () -> Object?
init(_ object: Object) {
// Any Swift value can be "promoted" to an AnyObject, however,
// that doesn't automatically turn it into a reference.
let reference = object as AnyObject
provider = { [weak reference] in
reference as? Object
}
}
}
The downside of the above approach is that we’re now able to initialize our Weak
struct with any object or value, not only with class instances. However, that might not be such a big deal, especially if we’re only using it as an implementation detail for tasks like managing observers. Because the upside is that we can now easily store collections of protocol-conforming instances weakly — enabling us to update our VideoPlayer
from before to now look like this:
class VideoPlayer {
private var observations = [Weak<PlaybackObserver>]()
func addObserver(_ observer: PlaybackObserver) {
observations.append(Weak(observer))
}
...
}
Pretty cool! Whether or not it’s worth it to create a generic Weak
wrapper, rather than just using specific ones (like the Observation
type we started out with), will of course depend heavily on how many use cases for storing collections of weak references that we have. Like always, it’s often best to start out with a very specific implementation, and then generalize later if needed.
Passing references to value types
Next, let’s flip the coin — and take a look at how reference types can be used to wrap value types. Let’s say that we’re building an app that stores various user-configurable settings using a value type that looks like this:
struct Settings {
var colorTheme: ColorTheme
var rememberLoggedInUser: Bool
...
}
While defining core data models as values is usually a good idea, as it lets us fully utilize value semantics — sometimes we might want to enable multiple objects to share a reference to a single instance of a given model.
For example, let’s say that we wanted multiple parts of our code base to be able to read and modify the same Settings
value, without having to implement any complex data flows. One way to accomplish that would be to create another boxing type, just like our Weak
struct from before, but this time to enable us to wrap a value type inside of a reference type — like this:
class Reference<Value> {
var value: Value
init(value: Value) {
self.value = value
}
}
With the above in place, we can now easily turn any value type into a reference, simply by wrapping it in a Reference
instance, and then passing that instance to whichever object or function that we want to share our value with:
let settings = loadSettings()
let sharedSettings = Reference(value: settings)
For example, here’s how a ProfileViewModel
could accept a referenced Settings
value, rather than just a copy of one:
class ProfileViewModel {
private let user: User
private let settings: Reference<Settings>
init(user: User, settings: Reference<Settings>) {
self.user = user
self.settings = settings
}
func makeEmailAddressIcon() -> Icon {
var icon = Icon.email
icon.useLightVersion = settings.value.colorTheme.isDark
return icon
}
...
}
While the above approach is a really convenient way to share value type-based data across an app, it does come with two major downsides. The first is that we have to always access the passed reference’s value
property in order to actually get access to the value that we’re interested in, and the second is that we’re now sharing mutable state between multiple parts of our code base.
While we can’t do much about the first downside (unless we want to replicate each of our value type’s APIs within our reference type, which kind of defeats the point of it), we could limit our state’s mutability — which often helps make a system a bit more predictable and easier to debug, since the number of places in which a piece of state can be modified is reduced.
One way to do that is to make our Reference
type immutable, and then create a mutable subclass of it (let’s call it MutableReference
). That way, only the creator of a reference will be able to mutate it, since it can simply be passed as an immutable Reference
afterwards:
class Reference<Value> {
fileprivate(set) var value: Value
init(value: Value) {
self.value = value
}
}
class MutableReference<Value>: Reference<Value> {
func update(with value: Value) {
self.value = value
}
}
Here’s an example of how the above can be really useful, since it lets us update our shared value reference whenever needed, while still enabling us to pass it as an immutable object — without having to do any kind of conversion:
let settings = loadSettings()
// Since this part of our code base knows that our reference is
// mutable, it can easily update it whenever needed:
let sharedSettings = MutableReference(value: settings)
observeSettingsChange(with: sharedSettings.update)
// Since our view model accepts an immutable reference, it won't
// be able to mutate our value in any way:
let viewModel = ProfileViewModel(settings: sharedSettings)
The above kind of boxing types can be incredibly useful, especially if we limit their mutation capabilities to only be visible within the parts of our code base that should be able to mutate them — as that can give us much of the flexibility and power of reference types, without the problems that usually start to occur when over-sharing mutable state.
However, when deploying boxing types, it’s always worth considering whether another abstraction (such as a model controller, a bindable value, or a reactive framework such as Combine) would be more appropriate. Especially if the way we share and pass values across our app grows more complex, a more powerful abstraction might be the way to go — even though we might choose to start out with something like the above Reference
type in order to keep things simple initially.
Using reference types as underlying storage
Finally, let’s go beyond boxing types and take a look at how value and reference types can be truly combined to unlock some really powerful capabilities. Let’s say that we’ve defined a value type that lets us express complex formatted texts, by splitting them up into separate components that can then be rendered into an NSAttributedString
:
struct FormattedText {
var components: [Component]
func render() -> NSAttributedString {
let result = NSMutableAttributedString()
components.forEach { result.append($0.render()) }
return result
}
}
While the above provides a nice, “Swifty” way of building up attributed strings, it requires an O(n)
evaluation every time that we want to display each text — which will result in duplicate work when rendering the same text in multiple places.
To address that, let’s take some inspiration from the Swift standard library, which uses pointers and references as storage for certain key value types — such as String
and Array
— to give them copy-on-write semantics. What that means is that multiple values can share the same underlying storage until one of them is actually mutated, minimizing the amount of copy operations that need to occur as we pass values around (since only the “value type shell” is actually copied).
In our case, we’ll use a similar approach, but for a slightly different purpose — to implement a RenderingCache
for our above FormattedText
type. That way we’ll only have to render each copy of the same text once, which should significantly boost performance in situations when the same text is used in multiple places (effectively turning each subsequent rendering pass into an O(1)
operation).
Our cache will be a simple class, which only has one job — to store the result of a previous rendering operation:
private extension FormattedText {
class RenderingCache {
var result: NSAttributedString?
}
}
Then, as we’ve finished rendering an NSAttributedString
, we’ll store the result in an instance of our RenderingCache
— which will be replaced whenever our components
array is mutated. That way we’re avoiding caching stale data, and since our cache is a reference type, it will keep being pointed to the same instance — even as a FormattedText
value gets passed around our app.
Here’s what the complete implementation looks like:
struct FormattedText {
var components: [Component] {
didSet { cache = RenderingCache() }
}
private var cache = RenderingCache()
init(components: [Component] = []) {
self.components = components
}
func render() -> NSAttributedString {
if let cached = cache.result {
return cached
}
let result = NSMutableAttributedString()
components.forEach { result.append($0.render()) }
cache.result = result
return result
}
}
By using a reference type as our underlying storage, while keeping the API surface entirely value-type based — we can combine the performance benefits of having a single source of truth, while still enabling the users of our API to take full advantage of value semantics, by limiting mutability and by not publicly sharing state.
Conclusion
The fact that Swift fully supports both value and reference types gives us a ton of flexibility when it comes to how we design our APIs, how we construct their underlying implementations, and how state is managed within an app or system.
While most types will probably continue being either purely a value or a class, sometimes, by combining the two we can achieve some really powerful results — enabling us to utilize both the convenience and performance characteristics of reference types, and the safety and limited mutability of value types.
What do you think? Have you ever used one of the techniques mentioned in this article, or could they become useful to you either now or in the future? Let me know — along with your questions, comments and feedback — either via Twitter or email.
Thanks for reading! 🚀