SwiftObserver
SwiftObserver is a lightweight framework for reactive Swift. Its design goals make it easy to learn and a joy to use:
- Meaningful Code
π‘
SwiftObserver promotes meaningful metaphors, names and syntax, producing highly readable code. - 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 does the memory management for you. Oh yeah, memory leaks are impossible.
SwiftObserver is only 1800 lines of production code, but it's well beyond a 1000 hours of work, re-imagining and reworking it many times, letting go of fancy features, documenting, unit-testing, and battle-testing it in practice.
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
Get Involved
- Found a bug? Create a github issue.
- Need a feature? Create a github issue.
- Want to improve stuff? Create a pull request.
- Want to start a discussion? Visit Gitter.
- Need support and troubleshooting? Write at [email protected].
- Want to contact us? Write at [email protected].
Get Started
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:
// swift-tools-version:5.4
import PackageDescription
let package = Package(
name: "MyApp",
dependencies: [
.package(url: "https://github.com/flowtoolz/SwiftObserver.git",
.upToNextMajor(from: "6.2.0"))
],
targets: [
.target(name: "MyAppTarget",
dependencies: ["SwiftObserver"])
]
)
Then run $ swift build
or $ swift run
.
Finally, in your Swift files:
import SwiftObserver
Introduction
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 sameObservableObject
, for example by callingobserve(...)
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 viasend(_ 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 whichsend
is called, which helps when observers, from their message handling closures, somehow trigger further calls ofsend
. - 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 an object of a custom
ObservableObject
class that utilizesMessenger<Message>
. - 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
When an Observer
or ObservableObject
dies, SwiftObserver cleans up all related observations automatically, making memory leaks impossible. So there isn't really any memory management to worry about.
However, observing- and observed objects can 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
Free Observers
You may start an observation without an explicit observer:
observe(Sky.shared) { color in
// marvel at the sky changing its color
}
Sky.shared.observed { color in // ... same
// ...
}
Both examples internally use the global observer FreeObserver.shared
. You may reference FreeObserver.shared
explicitly to stop particular or all such free global observations.
You can also instantiate your own FreeObserver
to do observations even more "freely". Just keep it alive as long as the observation shall last. Such a free observer is like a "Cancellable" or "Token" in other reactive frameworks.
And you can do one-time observations:
observeOnce(Sky.shared) { color in
// notice new color. observation has stopped.
}
Sky.shared.observedOnce { color in // ... same
// ...
}
Both functions return the involved FreeObserver
as a discardable result. Typically we ignore that observer and it will die together with the observation as soon as it has received one message.
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")
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 ObservableObject
:
public protocol ObservableObject: class {
var messenger: Messenger<Message> { get }
associatedtype Message: Any
}
Messenger
is itself 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 observables often employ some enum
as their message type:
class Model: SuperModel, ObservableObject {
func foo() { send(.willUpdate) }
func bar() { send(.didUpdate) }
deinit { send(.willDie) }
let messenger = Messenger<Event>() // Message == Event
enum Event { case willUpdate, didUpdate, willDie }
}
Variables
Var<Value>
is an ObservableObject
that has a property 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
}
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 Cached Messages
).
Use Variable Values
Value
must be Equatable
, and based on its value
the whole Var<Value>
is Equatable
. Where Value
is Comparable
, Var<Value>
will also be Comparable
.
You can set value
via initializer, directly and via the <-
operator:
let text = Var<String?>() // text.value == nil
text.value = "a text"
let number = Var(23) // number.value == 23
number <- 42 // number.value == 42
Number Values
If Value
is some number type Number
that is either an Int
, Float
or Double
:
-
Every
Var<Number>
,Var<Number?>
,Var<Number>?
andVar<Number?>?
has a respective propertyvar int: Int
,var float: Float
orvar double: Double
. That property is non-optional and interpretsnil
values as zero. -
You can apply numeric operators
+
,-
,*
and/
to all pairs ofNumber
,Number?
,Var<Number>
,Var<Number?>
,Var<Number>?
andVar<Number?>?
.
let numVar = Var<Int?>() // numVar.value == nil
print(numVar.int) // 0
numVar.int += 5 // numVar.value == 5
numVar <- Var(1) + 2 // numVar.value == 3
String Values
- Every
Var<String>
,Var<String?>
,Var<String>?
andVar<String?>?
has a propertyvar string: String
. That property is non-optional and interpretsnil
values as""
. - You can apply concatenation operator
+
to all pairs ofString
,String?
,Var<String>
,Var<String?>
,Var<String>?
andVar<String?>?
. - Representing its
string
property, everyVar<String>
andVar<String?>
conforms toTextOutputStream
,BidirectionalCollection
,Collection
,Sequence
,CustomDebugStringConvertible
andCustomStringConvertible
.
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 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:
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")!")
}
Promises
Promise<Value>
helps managing asynchronous returns and makes that intention explicit.
Side Note: Promise/Future implementations are now obsolete due to Swift's native async/await.
Promise
was added to SwiftObserver before async/await existed. I intent to replace any use ofPromise
by async/await in my own apps and then removePromise
from SwiftObserver. I had added it for these reasons: Combine'sFuture
is unfortunately an impractical and incomplete solution for one-shot asynchronous calls and to depend on PromiseKit or Vapor/NIO's Async might be overkill (and too server-specific) in many contexts. Also, integrating promises in SwiftObserver offers the advantages of consistency, simplicity and readability.
Receive a Promised Value
Receive It Once
func getID() -> Promise<Int> { // getID() promises an Int
Promise { promise in // convenience initializer
getIDAsync { id in // handler retains the promise until it's fulfilled
promise.fulfill(id)
}
}
}
getID().whenFulfilled { id in // get id (if fulfilled) or observe promise
// do somethin with the ID
}
A Promise
is either fulfilled or not. When it is fulfilled it stays that way. When a function returns a Promise
, that promise might already be fulfilled and not change anymore. Also, mappings are the only transforms that make sense on promises. So, to guard against mistakes, Promise
is not directly ObservableObject
. Instead we call whenFulfilled
on it, which does an internal one-shot observation of the promise if the promise isn't yet fulfilled.
Typically, promises are shortlived objects that we don't hold on to. That works fine since an asynchronous function like getID()
that returns a promise keeps that promise alive in order to fulfill it. So we get the promised value asynchronously without even holding the promise anywhere, and the promise as well as its internal observations get cleaned up automatically when the promise is fulfilled and dies.
Receive It Again
Sometimes, we wanna do multiple things with an asynchronous result (long) after receiving it:
let idPromise = getID() // Promise<Int>
idPromise.whenFulfilled { id in
// do somethin with id
}
idPromise.whenFulfilled { id in
// do somethin else with id
}
Compose Promises
Sequential Composition
promise { // establish context and increase readability
getInt() // return a Promise<Int>
}.then { // chain another promise sequentially
getString(takeInt: $0) // take Int sent by 'promise', return a Promise<String>
}.whenFulfilled { // observation dies when promise 'then' is fulfilled
print($0) // print String sent by promise 'then'
}
promise
is for readability. It allows for nice consistent closure syntax and makes it clear that we're working with promises. It takes a closure that returns a Promise
and simply returns that Promise
.
We call then
on a first Promise
and pass it a closure that returns a second Promise
. That closure takes the value of the first promise, allowing the second promise to depend on the value of the first. then
returns a new Promise
that provides the value of the second promise.
Concurrent Composition
promise {
getInt()
}.and { // chain another promise concurrently
getString()
}.whenFulfilled {
print($0.0) // print Int sent by 'promise'
print($0.1) // print String sent by promise 'getString()'
}
We call and
on a Promise
and pass it a closure that returns another Promise
. This immediatly observes both promises and returns a new Promise
that provides the combined values of both.
Value Mapping
promise {
getInt()
}.whenFulfilled { // returns 'promise' so the chain can continue
print($0) // print Int sent by 'promise'
}.map { // chain a mapping promise sequentially
"\($0)" // map Int sent by promise 'whenFulfilled' to String
}.whenFulfilled {
print($0) // print String sent by promise 'map'
}
Transform functions that neither filter messages nor exclusively create standalone transforms return a new Promise
when called on a Promise
. These functions are map(...)
, unwrap(default)
and new()
. The advantage here is, as with any function that returns a promise, that we don't need to keep that promise alive in order to receive the promised value.
Promise a Failable Result
A function that can fail cannot promise a pure value. It can only promise a Result
, which contains a value on success and an error on failure:
func getID() -> ResultPromise<Int> { // typealias for Promise<Result<Int, Error>>
Promise { promise in
getIDAsync { result in
promise.fulfill(result) // fulfill with Result, Result.Value or Error
}
}
}
getID().whenSucceeded { id in // corresponds to whenFulfilled
// do somethin with id
} failed: { error in // whenSucceeded requires handling failure
// do somethin with error
}
When composing result promises, we often only care about either success or failure:
promise {
getID()
}.whenFailed { // whenFailed / whenSucceeded don't need to end the chain
log(error: $0)
}.onSuccess { // corresponds to then
getName(forID: $0)
}.mapSuccess { // corresponds to map
try Data(name: $0) // mapSuccess, onSuccess and whenSucceeded can throw
} // finally returns a ResultPromise<Data>
Every closure that handles the success case can throw an Error
.
Advanced
Message Authors
Every message has an author associated with it. This feature is only noticable in code if we 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
Cached 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 Caches
-
Any
Var
is anObservableCache
. ItslatestMessage
is anUpdate
in whichold
andnew
both hold the currentvalue
. -
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
. -
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.
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
}
}
}
}
Weak Observable 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
Further Reading
- Patterns: Read more about some patterns that emerged from using SwiftObserver.
- Philosophy: Read more about the philosophy and features of SwiftObserver.
- Architecture: Have a look at a dependency diagram of the types of SwiftObserver.
- License: SwiftObserver is released under the MIT license. See LICENSE for details.
Open Tasks
- Decompose, rework and extend unit test suite
- Write API documentation comments
- Update, rework and extend documentation of features, philosophy and patterns
- Document adapters for interoperation with Combine and SwiftUI
- Add syntax sugar for observing/processing on queues, if added API complexity is worth it
- Leverage property wrappers where they offer any sort of benefit
- Engage feedback and contribution