Combining Value and Reference Types in Swift for Powerful Code Design

KD Knowledge Diet
3 min readJul 10, 2024

--

Swift offers two primary types of data models: value types and reference types. Both come with their unique advantages — value types are known for their clear semantics with automatic copying on pass, while reference types offer a single source of truth, albeit with shared state complexities. However, in some scenarios, strategically combining both can unlock powerful coding capabilities and open up diverse design options. Let’s explore how blending value and reference types can be beneficial in Swift development.

Collections of Weak References

A common pattern in Swift involves establishing weak references between objects to avoid retain cycles. For instance, consider a `VideoPlayer` class that observers can watch. Initially, you might have a simple setup with a single observer:

class VideoPlayer {
weak var observer: PlaybackObserver?
}

This works for a one-to-one relationship, but what if you need multiple observers? An array of `PlaybackObserver` won’t work as arrays hold elements strongly. To tackle this, you can use a combination of reference (the observer) and value types (a struct to hold the observer weakly):

private extension VideoPlayer {
struct Observation {
weak var observer: PlaybackObserver?
}
}
class VideoPlayer {
private var observations = [Observation]()
func addObserver(_ observer: PlaybackObserver) {
observations.append(Observation(observer: observer))
}
}

Passing References to Value Types

Sometimes, sharing a reference to a single instance of a value type across multiple parts of an app is desirable. This can be done using a reference type to wrap a value type. For instance:

class Reference<Value> {
var value: Value
init(value: Value) {
self.value = value
}
}

Then, a value type like `Settings` can be shared across different components:

let settings = Reference(value: loadSettings())

Using Reference Types as Underlying Storage

You can also use reference types as storage for value types to implement copy-on-write semantics. This approach is beneficial when dealing with types that are expensive to copy. For example, you might want to cache the result of an operation in a class and then expose it through a struct:

private extension FormattedText {
class RenderingCache {
var result: NSAttributedString?
}
}
struct FormattedText {
private var cache = RenderingCache()
var components: [Component]
func render() -> NSAttributedString {
if let cached = cache.result {
return cached
}
// Perform rendering and update the cache
}
}

In this example, `FormattedText` is a struct, but it uses a class (`RenderingCache`) internally to cache rendered results. This approach allows multiple instances of `FormattedText` to share the same rendered output without immediately invalidating it on mutation.

Conclusion

Swift’s flexibility in allowing both value and reference types offers a rich landscape for developers to design their code. By combining these two models, you can harness the strengths of each: the safety and predictability of value types with the shared state and performance optimizations of reference types. This synergy can lead to efficient, maintainable, and powerful code structures that are well-suited for complex Swift applications. Whether it’s managing observers in a lightweight way, sharing mutable state conveniently, or optimizing performance with caching mechanisms, the combination of value and reference types in Swift is a toolkit worth exploring.

--

--

KD Knowledge Diet
KD Knowledge Diet

Written by KD Knowledge Diet

Software Engineer, Mobile Developer living in Seoul. I hate people using difficult words. Why not using simple words? Keep It Simple Stupid!

No responses yet