Non-intrusive Design ✊🏻 SwiftObserver doesn’t limit or modulate your design. It just makes it easy to do the right thing.
Simplicity 🕹 SwiftObserver employs few radically simple concepts and applies them consistently without exceptions.
Flexibility 🤸🏻♀️ SwiftObserver’s types are simple but universal and composable, making them applicable in many situations.
Safety ⛑ SwiftObserver eliminates the memory leaks that such an easy to use observer-/reactive library might invite.
SwiftObserver is only 1400 lines of production code, but it’s well beyond 1000 hours of work. With precursor implementations going back to 2013, it has continuously been re-imagined, reworked and battle-tested, letting go of many fancy features while refining documentation and unit-tests.
SwiftObserver diverges from convention as it doesn’t inherit the metaphors, terms, types, or function- and operator arsenals of common reactive libraries. It’s not as fancy as Rx and Combine and not as restrictive as Redux. Instead, it offers a powerful simplicity you might actually love to work with.
No need to learn a bunch of arbitrary metaphors, terms or types.
SwiftObserver is simple: Objects observe other objects.
Or a tad more technically: Observable objects* send messages to their *observers.
That’s it. Just readable code:
dog.observe(Sky.shared) { color in
// marvel at the sky changing its color
}
Observers
Any object can be an Observer if it has a Receiver for receiving messages:
class Dog: Observer {
let receiver = Receiver()
}
The receiver keeps the observer’s observations alive. The observer just holds on to it strongly.
Notes on Observers
For a message receiving closure to be called, the Observer/Receiver must still be alive. There’s no awareness after death in memory.
An Observer can do multiple simultaneous observations of the same ObservableObject, for example by calling observe(...) multiple times.
You can check wether an observer is observing an observable via observer.isObserving(observable).
Observable Objects
Any object can be an ObservableObject if it has a Messenger<Message> for sending messages:
class Sky: ObservableObject {
let messenger = Messenger<Color>() // Message == Color
}
Notes on Observable Objects
An ObservableObject sends messages via send(_ message: Message). The object’s clients, even its observers, are also free to call that function.
An ObservableObject delivers messages in exactly the order in which send is called, which helps when observers, from their message handling closures, somehow trigger further calls of send.
Just starting to observe an ObservableObject does not trigger it to send a message. This keeps everything simple, predictable and consistent.
Ways to Create an Observable Object
Create a Messenger<Message>. It’s a mediator through which other entities communicate.
Create a Variable<Value> (a.k.a. Var<Value>). It holds a value and sends value updates.
Create a transform object. It wraps and transforms another ObservableObject.
Memory Management
With SwiftObserver, you don’t have to deal with “Cancellables”, “Tokens”, “DisposeBags” or any such weirdness for every new observation. And yet, you also don’t have to worry about any specific memory management. When an Observer or ObservableObject dies, SwiftObserver cleans up all related observations automatically.
Of course, observing- and observed objects are still free to stop particular or all their ongoing observations:
dog.stopObserving(Sky.shared) // no more messages from the sky
dog.stopObserving() // no more messages from anywhere
Sky.shared.stopBeingObserved(by: dog) // no more messages to dog
Sky.shared.stopBeingObserved() // no more messages to anywhere
Messengers
Messenger is the simplest ObservableObject and the basis of every other ObservableObject. It doesn’t send messages by itself, but anyone can send messages through it and use it for any type of message:
let textMessenger = Messenger<String>()
observer.observe(textMessenger) { textMessage in
// respond to textMessage
}
textMessenger.send("my message")
Having a Messenger is actually what defines an ObservableObject:
public protocol ObservableObject: AnyObject {
var messenger: Messenger<Message> { get }
associatedtype Message: Any
}
Messenger is itself an ObservableObject because it points to itself as the required Messenger:
extension Messenger: ObservableObject {
public var messenger: Messenger<Message> { self }
}
Every other ObservableObject class is either a subclass of Messenger or a custom ObservableObject class that provides a Messenger. Custom observable objects often employ some enum as their message type:
Var<Value> is an ObservableObject that has a property var value: Value.
Observe Variables
Whenever its value changes, Var<Value> sends a message of type Update<Value>, informing about the old and new value:
let number = Var(42)
observer.observe(number) { update in
let whatsTheBigDifference = update.new - update.old
}
number <- 123 // use convenience operator <- to set number.value
In addition, you can always manually call variable.send() (without argument) to send an update in which old and new both hold the current value (see Pull Latest Messages).
Access Variable Values
The property wrapper Observable allows to access the actual Value directly. Let’s apply it to the above example:
@Observable var number = 42
observer.observe($number) { update in
let whatsTheBigDifference = update.new - update.old
}
number = 123
The wrapper’s projected value provides the underlying Var<Value>, which you access via the $ sign like in the above example. This is analogous to how you access underlying publishers of @Published properties in Combine.
Encode and Decode Variables
A Var<Value> is automatically Codable if its Value is. So when one of your types has Var properties, you can make that type Codable by simply adopting the Codable protocol:
class Model: Codable {
private(set) var text = Var("String Variable")
}
Note that text is a var instead of a let. It cannot be constant because Swift’s implicit decoder must mutate it. However, clients of Model would be supposed to set only text.value and not text itself, so the setter is private.
Transforms
Transforms make common steps of message processing more succinct and readable. They allow to map, filter and unwrap messages in many ways. You may freely chain these transforms together and also define new ones with them.
This example transforms messages of type Update<String?> into ones of type Int:
let title = Var<String?>()
observer.observe(title).new().unwrap("Untitled").map({ $0.count }) { titleLength in
// do something with the new title length
}
Make Transforms Observable
You may transform a particular observation directly on the fly, like in the above example. Such ad hoc transforms give the observer lots of flexibility.
Or you may instantiate a new ObservableObject that has the transform chain baked into it. The above example could then look like this:
let title = Var<String?>()
let titleLength = title.new().unwrap("Untitled").map { $0.count }
observer.observe(titleLength) { titleLength in
// do something with the new title length
}
Every transform object exposes its underlying ObservableObject as origin. You may even replace origin:
let titleLength = Var("Dummy Title").new().map { $0.count }
let title = Var("Real Title")
titleLength.origin.origin = title
Such stand-alone transforms can offer the same preprocessing to multiple observers. But since these transforms are distinct ObservableObjects, you must hold them strongly somewhere. Holding transform chains as dedicated observable objects suits entities like view models that represent transformations of other data.
Use Prebuilt Transforms
Whether you apply transforms ad hoc or as stand-alone objects, they work the same way. The following list illustrates prebuilt transforms as observable objects.
Map
First, there is your regular familiar map function. It transforms messages and often also their type:
let messenger = Messenger<String>() // sends String
let stringToInt = messenger.map { Int($0) } // sends Int?
New
When an ObservableObject like a Var<Value> sends messages of type Update<Value>, we often only care about the new value, so we map the update with new():
let errorCode = Var<Int>() // sends Update<Int>
let newErrorCode = errorCode.new() // sends Int
Filter
When you want to receive only certain messages, use filter:
let messenger = Messenger<String>() // sends String
let shortMessages = messenger.filter { $0.count < 10 } // sends String if length < 10
Select
Use select to receive only one specific message. select works with all Equatable message types. select maps the message type onto Void, so a receiving closure after a selection takes no message argument:
let messenger = Messenger<String>() // sends String
let myNotifier = messenger.select("my notification") // sends Void (no messages)
observer.observe(myNotifier) { // no argument
// someone sent "my notification"
}
Unwrap
Sometimes, we make message types optional, for example when there is no meaningful initial value for a Var. But we often don’t want to deal with optionals down the line. So we can use unwrap(), suppressing nil messages entirely:
let errorCodes = Messenger<Int?>() // sends Int?
let errorAlert = errorCodes.unwrap() // sends Int if the message is not nil
Unwrap with Default
You may also unwrap optional messages by replacing nil values with a default:
let points = Messenger<Int?>() // sends Int?
let pointsToShow = points.unwrap(0) // sends Int with 0 for nil
Chain Transforms
You may chain transforms together:
let numbers = Messenger<Int>()
observer.observe(numbers).map {
"\($0)" // Int -> String
}.filter {
$0.count > 1 // suppress single digit integers
}.map {
Int.init($0) // String -> Int?
}.unwrap { // Int? -> Int
print($0) // receive and process resulting Int
}
Of course, ad hoc transforms like the above end on the actual message handling closure. Now, when the last transform in the chain also takes a closure argument for its processing, like map and filter do, we use receive to stick with the nice syntax of trailing closures:
dog.observe(Sky.shared).map {
$0 == .blue
}.receive {
print("Will we go outside? \($0 ? "Yes" : "No")!")
}
Advanced
Interoperate With Combine
CombineObserver is another library product of the SwiftObserver package. It depends on SwiftObserver and adds a simple way to transform any SwiftObserver-ObservableObject into a Combine-Publisher:
import CombineObserver
@Observable var number = 7 // SwiftObserver
let numberPublisher = $number.publisher() // Combine
let cancellable = numberPublisher.dropFirst().sink { numberUpdate in
print("\(numberUpdate.new)")
}
number = 42 // prints "42"
This interoperation goes in only one direction. Here’s some reasoning behind that: SwiftObserver is for pure Swift-/model code without external dependencies – not even on Combine. When combined with Combine (oops), SwiftObserver would be employed in the model core of an application, while Combine would be used more with I/O periphery like SwiftUI and other system-specific APIs that already rely on Combine. That means, the “Combine layer” might want to observe (react to-) the “SwiftObserver layer” – but hardly the other way around.
Pull Latest Messages
An ObservableCache is an ObservableObject that has a property latestMessage: Message which typically returns the last sent message or one that indicates that nothing has changed. ObservableCache has a function send() that takes no argument and sends latestMessage.
Four Kinds of ObservableCache
Any Var is an ObservableCache. Its latestMessage is an Update in which old and new both hold the current value.
Custom observable objects can easily conform to ObservableCache. Even if their message type isn’t based on some state, latestMessage can still return a meaningful default value - or even nil where Message is optional.
Calling cache() on an ObservableObject creates a transform that is an ObservableCache. That cache’s Message will be optional but never an optional optional, even when the origin’s Message is already optional.
Of course, cache() wouldn’t make sense as an adhoc transform of an observation, so it can only create a distinct observable object.
Any transform whose origin is an ObservableCache is itself implicitly an ObservableCacheif it never suppresses (filters) messages. These compatible transforms are: map, new and unwrap(default).
Note that the latestMessage of a transform that is an implicit ObservableCache returns the transformed latestMessage of its underlying ObservableCache origin. Calling send(transformedMessage) on that transform itself will not “update” its latestMessage.
State-Based Messages
An ObservableObject like Var, that derives its messages from its state, can generate a “latest message” on demand and therefore act as an ObservableCache:
class Model: Messenger<String>, ObservableCache { // informs about the latest state
var latestMessage: String { state } // ... either on demand
var state = "initial state" {
didSet {
if state != oldValue {
send(state) // ... or when the state changes
}
}
}
}
Identify Message Authors
Every message has an author associated with it. This feature is only noticable in code if you use it.
An observable object can send an author together with a message via object.send(message, from: author). If noone specifies an author as in object.send(message), the observable object itself becomes the author.
Mutate Variables
Variables have a special value setter that allows to identify change authors:
let number = Var(0)
number.set(42, as: controller) // controller becomes author of the update message
Receive Authors
The observer can receive the author, by adding it as an argument to the message handling closure:
observer.observe(observableObject) { message, author in
// process message from author
}
Through the author, observers can determine a message’s origin. In the plain messenger pattern, the origin would simply be the message sender.
Share Observable Objects
Identifying message authors can become essential whenever multiple observers observe the same object while their actions can cause it so send messages.
Mutable data is a common type of such shared observable objects. For example, when multiple entities observe and modify a storage abstraction or caching hierarchy, they often want to avoid reacting to their own actions. Such overreaction might lead to redundant work or inifnite response cycles. So they identify as change authors when modifying the data and ignore messages from self when observing it:
class Collaborator: Observer {
func observeText() {
observe(sharedText).notFrom(self) { update, author in // see author filters below
// someone else edited the text
}
}
func editText() {
sharedText.set("my new text", as: self) // identify as change author
}
let receiver = Receiver()
}
let sharedText = Var<String>()
Filter by Author
There are three transforms related to message authors. As with other transforms, we can apply them directly in observations or create them as standalone observable objects.
Filter Author
We filter authors just like messages:
let messenger = Messenger<String>() // sends String
let friendMessages = messenger.filterAuthor { // sends String if message is from friend
friends.contains($0)
}
From
If only one specific author is of interest, filter authors with from. It captures the selected author weakly:
let messenger = Messenger<String>() // sends String
let joesMessages = messenger.from(joe) // sends String if message is from joe
Not From
If all but one specific author are of interest, use notFrom. It also captures the excluded author weakly:
let messenger = Messenger<String>() // sends String
let humanMessages = messenger.notFrom(hal9000) // sends String, but not from an evil AI
Observe Weak Objects
When you want to put an ObservableObject into some data structure or as the origin into a transform object but hold it there as a weak reference, transform it via observableObject.weak():
let number = Var(12)
let weakNumber = number.weak()
observer.observe(weakNumber) { update in
// process update of type Update<Int>
}
var weakNumbers = [Weak<Var<Int>>]()
weakNumbers.append(weakNumber)
Of course, weak() wouldn’t make sense as an adhoc transform, so it can only create a distinct observable object.
More
Architecture
Here’s the internal architecture (composition and essential dependencies) of the “SwiftObserver” target:
More diagrams of top-level source folders are over here. The images were generated with Codeface.
SwiftObserver
SwiftObserver is a lightweight package for reactive Swift. Its design goals make it easy to learn and a joy to use:
SwiftObserver promotes meaningful metaphors, names and syntax, producing highly readable code.
SwiftObserver doesn’t limit or modulate your design. It just makes it easy to do the right thing.
SwiftObserver employs few radically simple concepts and applies them consistently without exceptions.
SwiftObserver’s types are simple but universal and composable, making them applicable in many situations.
SwiftObserver eliminates the memory leaks that such an easy to use observer-/reactive library might invite.
SwiftObserver is only 1400 lines of production code, but it’s well beyond 1000 hours of work. With precursor implementations going back to 2013, it has continuously been re-imagined, reworked and battle-tested, letting go of many fancy features while refining documentation and unit-tests.
Why the Hell Another Reactive Swift Framework?
Reactive Programming adresses the central challenge of implementing effective architectures: controlling dependency direction, in particular making specific concerns depend on abstract ones. SwiftObserver breaks reactive programming down to its essence, which is the Observer Pattern.
SwiftObserver diverges from convention as it doesn’t inherit the metaphors, terms, types, or function- and operator arsenals of common reactive libraries. It’s not as fancy as Rx and Combine and not as restrictive as Redux. Instead, it offers a powerful simplicity you might actually love to work with.
Contents
Introduction
Get Involved
Install
With the Swift Package Manager, you add the SwiftObserver package via Xcode (11+).
Or you manually adjust the Package.swift file of your project:
Then run
$ swift build
or$ swift run
.Finally, in your Swift files:
Get Started
No need to learn a bunch of arbitrary metaphors, terms or types.
SwiftObserver is simple: Objects observe other objects.
Or a tad more technically: Observable objects* send messages to their *observers.
That’s it. Just readable code:
Observers
Any object can be an
Observer
if it has aReceiver
for receiving messages:The receiver keeps the observer’s observations alive. The observer just holds on to it strongly.
Notes on Observers
Observer
/Receiver
must still be alive. There’s no awareness after death in memory.Observer
can do multiple simultaneous observations of the sameObservableObject
, for example by callingobserve(...)
multiple times.observer
is observing anobservable
viaobserver.isObserving(observable)
.Observable Objects
Any object can be an
ObservableObject
if it has aMessenger<Message>
for sending messages:Notes on Observable Objects
ObservableObject
sends messages viasend(_ message: Message)
. The object’s clients, even its observers, are also free to call that function.ObservableObject
delivers messages in exactly the order in whichsend
is called, which helps when observers, from their message handling closures, somehow trigger further calls ofsend
.ObservableObject
does not trigger it to send a message. This keeps everything simple, predictable and consistent.Ways to Create an Observable Object
Messenger<Message>
. It’s a mediator through which other entities communicate.ObservableObject
class that utilizesMessenger<Message>
.Variable<Value>
(a.k.a.Var<Value>
). It holds a value and sends value updates.ObservableObject
.Memory Management
With SwiftObserver, you don’t have to deal with “Cancellables”, “Tokens”, “DisposeBags” or any such weirdness for every new observation. And yet, you also don’t have to worry about any specific memory management. When an
Observer
orObservableObject
dies, SwiftObserver cleans up all related observations automatically.Of course, observing- and observed objects are still free to stop particular or all their ongoing observations:
Messengers
Messenger
is the simplestObservableObject
and the basis of every otherObservableObject
. It doesn’t send messages by itself, but anyone can send messages through it and use it for any type of message:Messenger
embodies the common messenger / notifier pattern and can be used for that out of the box.Understand Observable Objects
Having a
Messenger
is actually what defines anObservableObject
:Messenger
is itself anObservableObject
because it points to itself as the requiredMessenger
:Every other
ObservableObject
class is either a subclass ofMessenger
or a customObservableObject
class that provides aMessenger
. Custom observable objects often employ someenum
as their message type:Variables
Var<Value>
is anObservableObject
that has a propertyvar value: Value
.Observe Variables
Whenever its
value
changes,Var<Value>
sends a message of typeUpdate<Value>
, informing about theold
andnew
value:In addition, you can always manually call
variable.send()
(without argument) to send an update in whichold
andnew
both hold the currentvalue
(see Pull Latest Messages).Access Variable Values
The property wrapper
Observable
allows to access the actualValue
directly. Let’s apply it to the above example:The wrapper’s projected value provides the underlying
Var<Value>
, which you access via the$
sign like in the above example. This is analogous to how you access underlying publishers of@Published
properties in Combine.Encode and Decode Variables
A
Var<Value>
is automaticallyCodable
if itsValue
is. So when one of your types hasVar
properties, you can make that typeCodable
by simply adopting theCodable
protocol:Note that
text
is avar
instead of alet
. It cannot be constant because Swift’s implicit decoder must mutate it. However, clients ofModel
would be supposed to set onlytext.value
and nottext
itself, so the setter is private.Transforms
Transforms make common steps of message processing more succinct and readable. They allow to map, filter and unwrap messages in many ways. You may freely chain these transforms together and also define new ones with them.
This example transforms messages of type
Update<String?>
into ones of typeInt
:Make Transforms Observable
You may transform a particular observation directly on the fly, like in the above example. Such ad hoc transforms give the observer lots of flexibility.
Or you may instantiate a new
ObservableObject
that has the transform chain baked into it. The above example could then look like this:Every transform object exposes its underlying
ObservableObject
asorigin
. You may even replaceorigin
:Such stand-alone transforms can offer the same preprocessing to multiple observers. But since these transforms are distinct
ObservableObject
s, you must hold them strongly somewhere. Holding transform chains as dedicated observable objects suits entities like view models that represent transformations of other data.Use Prebuilt Transforms
Whether you apply transforms ad hoc or as stand-alone objects, they work the same way. The following list illustrates prebuilt transforms as observable objects.
Map
First, there is your regular familiar
map
function. It transforms messages and often also their type:New
When an
ObservableObject
like aVar<Value>
sends messages of typeUpdate<Value>
, we often only care about thenew
value, so we map the update withnew()
:Filter
When you want to receive only certain messages, use
filter
:Select
Use
select
to receive only one specific message.select
works with allEquatable
message types.select
maps the message type ontoVoid
, so a receiving closure after a selection takes no message argument:Unwrap
Sometimes, we make message types optional, for example when there is no meaningful initial value for a
Var
. But we often don’t want to deal with optionals down the line. So we can useunwrap()
, suppressingnil
messages entirely:Unwrap with Default
You may also unwrap optional messages by replacing
nil
values with a default:Chain Transforms
You may chain transforms together:
Of course, ad hoc transforms like the above end on the actual message handling closure. Now, when the last transform in the chain also takes a closure argument for its processing, like
map
andfilter
do, we usereceive
to stick with the nice syntax of trailing closures:Advanced
Interoperate With Combine
CombineObserver is another library product of the SwiftObserver package. It depends on SwiftObserver and adds a simple way to transform any SwiftObserver-
ObservableObject
into a Combine-Publisher
:Pull Latest Messages
An
ObservableCache
is anObservableObject
that has a propertylatestMessage: Message
which typically returns the last sent message or one that indicates that nothing has changed.ObservableCache
has a functionsend()
that takes no argument and sendslatestMessage
.Four Kinds of
ObservableCache
Any
Var
is anObservableCache
. ItslatestMessage
is anUpdate
in whichold
andnew
both hold the currentvalue
.Custom observable objects can easily conform to
ObservableCache
. Even if their message type isn’t based on some state,latestMessage
can still return a meaningful default value - or evennil
whereMessage
is optional.Calling
cache()
on anObservableObject
creates a transform that is anObservableCache
. That cache’sMessage
will be optional but never an optional optional, even when the origin’sMessage
is already optional.Of course,
cache()
wouldn’t make sense as an adhoc transform of an observation, so it can only create a distinct observable object.Any transform whose origin is an
ObservableCache
is itself implicitly anObservableCache
if it never suppresses (filters) messages. These compatible transforms are:map
,new
andunwrap(default)
.Note that the
latestMessage
of a transform that is an implicitObservableCache
returns the transformedlatestMessage
of its underlyingObservableCache
origin. Callingsend(transformedMessage)
on that transform itself will not “update” itslatestMessage
.State-Based Messages
An
ObservableObject
likeVar
, that derives its messages from its state, can generate a “latest message” on demand and therefore act as anObservableCache
:Identify Message Authors
Every message has an author associated with it. This feature is only noticable in code if you use it.
An observable object can send an author together with a message via
object.send(message, from: author)
. If noone specifies an author as inobject.send(message)
, the observable object itself becomes the author.Mutate Variables
Variables have a special value setter that allows to identify change authors:
Receive Authors
The observer can receive the author, by adding it as an argument to the message handling closure:
Through the author, observers can determine a message’s origin. In the plain messenger pattern, the origin would simply be the message sender.
Share Observable Objects
Identifying message authors can become essential whenever multiple observers observe the same object while their actions can cause it so send messages.
Mutable data is a common type of such shared observable objects. For example, when multiple entities observe and modify a storage abstraction or caching hierarchy, they often want to avoid reacting to their own actions. Such overreaction might lead to redundant work or inifnite response cycles. So they identify as change authors when modifying the data and ignore messages from
self
when observing it:Filter by Author
There are three transforms related to message authors. As with other transforms, we can apply them directly in observations or create them as standalone observable objects.
Filter Author
We filter authors just like messages:
From
If only one specific author is of interest, filter authors with
from
. It captures the selected author weakly:Not From
If all but one specific author are of interest, use
notFrom
. It also captures the excluded author weakly:Observe Weak Objects
When you want to put an
ObservableObject
into some data structure or as the origin into a transform object but hold it there as aweak
reference, transform it viaobservableObject.weak()
:Of course,
weak()
wouldn’t make sense as an adhoc transform, so it can only create a distinct observable object.More
Architecture
Here’s the internal architecture (composition and essential dependencies) of the “SwiftObserver” target:
More diagrams of top-level source folders are over here. The images were generated with Codeface.
Further Reading
Open Tasks