Collection of carefully-prepared Classes and Protocols designed to imbue your inheriting Object Types with efficient, protocol-driven Observer Pattern Behaviour.
As of version 2.0.0, this includes support for Keyed Observers (see usage examples below for details)
Installation
Xcode Projects
Select File -> Swift Packages -> Add Package Dependency and enter https://github.com/Flowduino/Observable.git
Swift Package Manager Projects
You can use Observable as a Package Dependency in your own Packages’ Package.swift file:
You can then do import Observable in any code that requires it.
Usage
Here are some quick and easy usage examples for the features provided by Observable:
ObservableClass
You can inherit from ObservableClass in your own Class Types to provide out-of-the-box Observer Pattern support.
This not only works for @ObservedObject decorated Variables in a SwiftUI View, but also between your Classes (e.g. between Services, or Repositories etc.)
First, you would define a Protocol describing the Methods implemented in your Observer Class Type that your Observable Class can invoke:
/// Protocol defining what Methods the Obverable Class can invoke on any Observer
protocol DummyObservable: AnyObject { // It's extremely important that this Protocol be constrained to AnyObject
func onFooChanged(oldValue: String, newValue: String)
func onBarChanged(oldValue: String, newValue: String)
}
Note - It is important that our Protocol define the AnyObject conformity-constraint as shown above.
Now, we can define our Observable, inheriting from ObservableClass:
/// Class that can be Observed
class Dummy: ObservableClass {
private var _foo: String = "Hello"
public var foo: String {
get {
return _foo
}
set {
// Invoke onFooChanged for all current Observers
withObservers { (observer: DummyObservable) in
observer.onFooChanged(oldValue: _foo, newValue: newValue)
}
_foo = newValue
objectWillChange.send() // This is for the standard ObservableObject behaviour (both are supported together)
}
}
private var _bar: String = "World"
public var bar: String {
get {
return _bar
}
set {
// Invoke onBarChanged for all current Observers
withObservers { (observer: DummyObservable) in
observer.onBarChanged(oldValue: _bar, newValue: newValue)
}
_bar = newValue
objectWillChange.send() // This is for the standard ObservableObject behaviour (both are supported together)
}
}
}
We can now define an Observer to register with the Observable, ensuring that we specify that it implements our DummyObservable protocol:
class DummyObserver: DummyObservable {
/* Implementations for DummyObservable */
func onFooChanged(oldValue: String, newValue: String) {
print("Foo Changed from \(oldValue) to \(newValue)")
}
func onBarChanged(oldValue: String, newValue: String) {
print("Bar Changed from \(oldValue) to \(newValue)")
}
}
We can now produce some simple code (such as in a Playground) to put it all together:
// Playground Code to use the above
var observable = Dummy() // This is the Object that we can Observe
var observer = DummyObserver() // This is an Object that will Observe the Observable
observable.addObserver(observer) // This is what registers the Observer with the Observable!
observable.foo = "Test 1"
observable.bar = "Test 2"
ObservableThreadSafeClass
ObservableThreadSafeClass works exactly the same way as ObservableClass. The internal implementation simply encapsulates the Observer collections behind a DispatchSemaphore, and provides a Revolving Door mechanism to ensure unobstructed access is available to addObserver and removeObserver, even when withObservers is in execution.
Its usage is exactly as shown above in ObservableClass, only you would substitute the inheritence of ObservableClass to instead inherit from ObservableThreadSafeClass.
ObservableThread
ObservableThread provides you with a Base Type for any Thread Types you would want to Observe.
Note - ObservableThread does implement the ObservableObject protocol, and is technically compatible with the @ObservedObject property decorator in a SwiftUI View. However, to use it in this way, anywhere you would invoke objectWillUpdate.send() you must instead use notifyChange(). Internally, ObservableThread will execute objectWillChange.send()but enforce that it must execute on the MainActor (as required by Swift)
Let’s now begin taking a look at how we can use ObservableThread in your code.
The example is intentionally simplistic, and simply generates a random number every 60 seconds within an endless loop in the Thread.
class RandomNumberObservableThread: ObservableThread {
init() {
self.start() // This will start the thread on creation. You aren't required to do it this way, I'm just choosing to!
}
public override func main() { // We must override this method
while self.isExecuting { // This creates a loop that will continue for as long as the Thread is running!
let randomNumber = Int.random(in: -9000..<9001) // We'll generate a random number between -9000 and +9000
// Now let's notify all of our Observers!
withObservers { (observer: RandomNumberObserver) in
observer.onRandomNumber(randomNumber)
}
Self.sleep(forTimeInterval: 60.00) // This will cause our Thread to sleep for 60 seconds
}
}
}
So, we now have a Thread that can be Observed, and will notify all Observers every minute when it generates a random Integer.
Let’s now implement a Class intended to Observe this Thread:
class RandomNumberObserverClass: RandomNumberObserver {
public func onRandomNumber(_ randomNumber: Int) {
print("Random Number is: \(randomNumber)")
}
We can now tie this all together in a simple Playground:
var myThread = RandomNumberObservableThread()
var myObserver = RandomNumberObserverClass()
myThread.addObserver(myObserver)
That’s it! The Playground program will now simply print out the new Random Number notice message into the console output every 60 seconds.
You can adopt this approach for any Observation-Based Thread Behaviour you require, because ObservableThread will always invoke the Observer callback methods in the execution context their own threads! This means that, for example, you can safely instantiate an Observer class on the UI Thread, while the code execution being observed resides in its own threads (one or many, per your requirements).
Keyed Observation Pattern
As of version 1.1.0, you can now register and notify Keyed Observers.
Note: Version 2.0.0 modified the Interface significantly to eliminate the need for Generic Typing of the Key. Key Types are now inferred for you.
This functionality is an extension of the standard Observer Pattern, and is implemented in the following classes from which you can extend:
KeyedObservableClass instead of ObservableClass
KeyedObservableThread instead of ObservableThread
KeyedObservableThreadSafeClass instead of ObservableThreadSafeClass
Remember, Keyed Observation is an extension of the basic Observation Pattern, so any Keyed Observable is also inherently able to register and notify non-Keyed Observers
You would use Keyed Observation whenever your Observers care about a specific context of change. A good example would be for a Model Repository, where an Observer may only care about changes to a specific Model contained in the Repository. In this scenario, you would used Keyed Observation to ensure the Observer is only being notified about changes corresponding to the given Key.
Key Types must always conform to the Hashable protocol, just as must any Key Type used for a Dictionary collection.
Let’s take a look at a basic usage example.
We shall provide a basic usage example to synchronize an Observer’s internal Dictionary for specific keys only with the values from the Observable’s internal Dictionary.
First, we would begin with an Observation Protocol:
The above Observation Protocol provides the method onValueChanged which takes the key (in this case a String value) and provides the corresponding oldValue and newValue values for that key.
Our Observer will implement TestKeyedObservable to provide an implementation for this function.
Now, let’s define a simple Keyed Observable to house the master Dictionary we will be selectively-synchronizing with one or more Observers.
class TestKeyedObservableClass: KeyedObservableClass<String> {
private var keyValues: [String:String] = ["A":"Hello", "B":"Foo", "C":"Ping"]
func setValue(key: String, value: String) {
withKeyedObservers(key: key) { (key, observer: TestKeyedObservable) in
observer.onValueChanged(key: key, oldValue: self.keyValues[key]!, newValue: value)
}
self.keyValues[key] = value
}
}
The above class inherits from KeyedObservableClass and specializes the TKey generic to be a String. In other words, the Keys for this Observable must always be String values.
It includes a simple String:String dictionary (String key with a String value)
The setValue method will simply notify all observers using withKeyedObservers any time a specific key the Obsever(s) is(are) observing is updated, passing along the oldValue and newValue values. It will then update its internal Dictionary (keyValues) so that it always contains the latest value.
Note the use of withKeyedObservers instead of withObservers. You will use this syntax in your own Keyed Observables, changing only the declared Observer Protocol (TestKeyedObservable in this example) with the Observer Protocol representing your own observation methods.
Now that we have a Keyed Observable that will notify Observers each time the value of a key changes, let’s define an Observer.
class TestKeyedObserverClass: TestKeyedObservable {
public var keyValues: [String:String] = ["A":"Hello", "B":"Foo"]
func onValueChanged(key: String, oldValue: String, newValue: String) {
keyValues[key] = newValue
}
}
So, TestKeyedObserverClass is a simple class, implementing our TestKeyedObservable Observer Protocol.
For this example, we are going to presume that there are 2 pre-defined Keys with known initial values (there do not have to be… you can have as many keys as you wish)
You will notice that we initialized both the Observable and Observer classes to have identical keyValues dictionaries. This is solely for the sake of simplifying this example by ensuring there is always an oldValue. You don’t need to do this in your own implementations.
So, now that we have the Observable and the Observer types, let’s produce a simple bit of Playground code to tie it together.
let observable = TestKeyedObservableClass() // Creates our Observable
let observer = TestKeyedObserverClass // Creates a single Observer instance
At this point, we need to consider what Key or Keys our observer is going to Observe.
For example, we can Observe just one key:
observable.addKeyedObserver(key: "A", observer)
The above means that observer would only have its onValueChanged method invoked when the value of key A is modified in observable.
Likewise, if we only care about key B, we can do:
observable.addKeyedObserver(key: "B", observer)
If we care about both known keys, we can simply register them both:
The above would register observer with observable for every key contained in observer‘s keyValues dictionary.
Ultimately, you can register the observer with the observable for any keys you want:
observable.addKeyedObserver(key: "Foo", observer)
Let’s output the initial values of all of our keys before we invoke any code that would modify their values:
for (key, value) in observer.keyValues {
print("Key: '\(key)' has a value of '\(value)'")
}
This would output:
Key: ‘A’ has a value of ‘Hello’
Key: ‘B’ has a value of ‘Foo’
So, now that we can register the Keyed Observer with the Observer for whatever key or keys we wish, let’s trigger the Observer Pattern in the observer:
observable.setValue(key: "A", "World")
The above will then update the value if A from “Hello” to “World”.
If we repeat the following code:
for (key, value) in observer.keyValues {
print("Key: '\(key)' has a value of '\(value)'")
}
This would output:
Key: ‘A’ has a value of ‘World’
Key: ‘B’ has a value of ‘Foo’
Okay, so what if we change the value for key “C”? What will happen?
observable.setValue(key: "C", "Pong")
Now, if we repeat the following code:
for (key, value) in observer.keyValues {
print("Key: '\(key)' has a value of '\(value)'")
}
This would output:
Key: ‘A’ has a value of ‘World’
Key: ‘B’ has a value of ‘Foo’
Note that the observer was not notified about the change to the value of key C. This is because observer is not observing observable for changes to key C.
This is the value of Keyed Observation Pattern. Put simply: not all Observations are meaningful to all Observers. So, as you have now seen, Keyed Observeration enables our Observers to be notified specifically of changes relevant to that Observer.
Overloaded addObserver, removeObserver, addKeyedObserver, and removeKeyedObserver methods
As of version 1.1.0, all useful combination overloads for the above-specified methods of ObservableClass, ObservableThread, ObservableThreadSafeClass, KeyedObservableClass, KeyedObservableThread, and KeyedObservableThreadSafeClass have been provided to streamline the adding and removal of Observers with/from an Observable.
removeObserver and removeKeyedObserver also provide the same overloads as shown above.
Additional Useful Hints
There are a few additional useful things you should know about this Package.
A single Observable can invoke withObservers for any number of Observer Protocols
This library intentionally performs run-time type checks against each registered Observer to ensure that it conforms to the explicitly-defined Observer Protocol being requested by your withObservers Closure method.
Which can then both be used by the same ObservableClass, ObservableThreadSafeClass, or ObservableThread descendant:
withObservers { (observer: ObserverProtocolA) in
observer.doSomethingForProtocolA()
}
withObservers { (observer: ObserverProtocolB) in
observer.doSomethingForProtocolB()
}
Any number of Observer Protocols can be marshalled by any of our Observable types, and only Observers conforming to the explicitly-specified Observer Protocol will be passed into your withObservers Closure method.
License
Observable is available under the MIT license. See the LICENSE file for more info.
Join us on Discord
If you require additional support, or would like to discuss Observable, Swift, or any other topics related to Flowduino, you can join us on Discord.
Observable
Collection of carefully-prepared Classes and Protocols designed to imbue your inheriting Object Types with efficient, protocol-driven Observer Pattern Behaviour. As of version 2.0.0, this includes support for Keyed Observers (see usage examples below for details)
Installation
Xcode Projects
Select
File
->Swift Packages
->Add Package Dependency
and enterhttps://github.com/Flowduino/Observable.git
Swift Package Manager Projects
You can use
Observable
as a Package Dependency in your own Packages’Package.swift
file:From there, refer to
Observable
as a “target dependency” in any of your package’s targets that need it.You can then do
import Observable
in any code that requires it.Usage
Here are some quick and easy usage examples for the features provided by
Observable
:ObservableClass
You can inherit from
ObservableClass
in your own Class Types to provide out-of-the-box Observer Pattern support. This not only works for@ObservedObject
decorated Variables in a SwiftUIView
, but also between your Classes (e.g. between Services, or Repositories etc.)First, you would define a Protocol describing the Methods implemented in your Observer Class Type that your Observable Class can invoke:
Note - It is important that our Protocol define the
AnyObject
conformity-constraint as shown above.Now, we can define our Observable, inheriting from
ObservableClass
:We can now define an Observer to register with the Observable, ensuring that we specify that it implements our
DummyObservable
protocol:We can now produce some simple code (such as in a Playground) to put it all together:
ObservableThreadSafeClass
ObservableThreadSafeClass
works exactly the same way asObservableClass
. The internal implementation simply encapsulates theObserver
collections behind aDispatchSemaphore
, and provides a Revolving Door mechanism to ensure unobstructed access is available toaddObserver
andremoveObserver
, even whenwithObservers
is in execution.Its usage is exactly as shown above in
ObservableClass
, only you would substitute the inheritence ofObservableClass
to instead inherit fromObservableThreadSafeClass
.ObservableThread
ObservableThread
provides you with a Base Type for any Thread Types you would want to Observe.Note -
ObservableThread
does implement theObservableObject
protocol, and is technically compatible with the@ObservedObject
property decorator in a SwiftUIView
. However, to use it in this way, anywhere you would invokeobjectWillUpdate.send()
you must instead usenotifyChange()
. Internally,ObservableThread
will executeobjectWillChange.send()
but enforce that it must execute on theMainActor
(as required by Swift)Let’s now begin taking a look at how we can use
ObservableThread
in your code. The example is intentionally simplistic, and simply generates a random number every 60 seconds within an endless loop in the Thread.Let’s begin by defining our Observation Protocol:
Any Observer for our Thread will need to conform to the RandomNumberObserver protocol above.
Now, let’s define our RandomNumberObservableThread class:
So, we now have a Thread that can be Observed, and will notify all Observers every minute when it generates a random Integer.
Let’s now implement a Class intended to Observe this Thread:
We can now tie this all together in a simple Playground:
That’s it! The Playground program will now simply print out the new Random Number notice message into the console output every 60 seconds.
You can adopt this approach for any Observation-Based Thread Behaviour you require, because
ObservableThread
will always invoke the Observer callback methods in the execution context their own threads! This means that, for example, you can safely instantiate an Observer class on the UI Thread, while the code execution being observed resides in its own threads (one or many, per your requirements).Keyed Observation Pattern
As of version 1.1.0, you can now register and notify Keyed Observers. Note: Version 2.0.0 modified the Interface significantly to eliminate the need for Generic Typing of the Key. Key Types are now inferred for you.
This functionality is an extension of the standard Observer Pattern, and is implemented in the following classes from which you can extend:
KeyedObservableClass
instead ofObservableClass
KeyedObservableThread
instead ofObservableThread
KeyedObservableThreadSafeClass
instead ofObservableThreadSafeClass
Remember, Keyed Observation is an extension of the basic Observation Pattern, so any Keyed Observable is also inherently able to register and notify non-Keyed Observers
You would use Keyed Observation whenever your Observers care about a specific context of change. A good example would be for a Model Repository, where an Observer may only care about changes to a specific Model contained in the Repository. In this scenario, you would used Keyed Observation to ensure the Observer is only being notified about changes corresponding to the given Key.
Key Types must always conform to the
Hashable
protocol, just as must any Key Type used for aDictionary
collection.Let’s take a look at a basic usage example.
We shall provide a basic usage example to synchronize an Observer’s internal Dictionary for specific keys only with the values from the Observable’s internal Dictionary.
First, we would begin with an Observation Protocol:
The above Observation Protocol provides the method
onValueChanged
which takes thekey
(in this case aString
value) and provides the correspondingoldValue
andnewValue
values for thatkey
. Our Observer will implementTestKeyedObservable
to provide an implementation for this function.Now, let’s define a simple Keyed Observable to house the master Dictionary we will be selectively-synchronizing with one or more Observers.
The above class inherits from
KeyedObservableClass
and specializes theTKey
generic to be aString
. In other words, the Keys for this Observable must always beString
values. It includes a simpleString:String
dictionary (String
key with aString
value)The
setValue
method will simply notify all observers usingwithKeyedObservers
any time a specifickey
the Obsever(s) is(are) observing is updated, passing along theoldValue
andnewValue
values. It will then update its internal Dictionary (keyValues
) so that it always contains the latest value.Note the use of
withKeyedObservers
instead ofwithObservers
. You will use this syntax in your own Keyed Observables, changing only the declared Observer Protocol (TestKeyedObservable
in this example) with the Observer Protocol representing your own observation methods.Now that we have a Keyed Observable that will notify Observers each time the value of a key changes, let’s define an Observer.
So,
TestKeyedObserverClass
is a simple class, implementing ourTestKeyedObservable
Observer Protocol. For this example, we are going to presume that there are 2 pre-defined Keys with known initial values (there do not have to be… you can have as many keys as you wish)You will notice that we initialized both the Observable and Observer classes to have identical
keyValues
dictionaries. This is solely for the sake of simplifying this example by ensuring there is always anoldValue
. You don’t need to do this in your own implementations.So, now that we have the Observable and the Observer types, let’s produce a simple bit of Playground code to tie it together.
At this point, we need to consider what Key or Keys our
observer
is going to Observe.For example, we can Observe just one key:
The above means that
observer
would only have itsonValueChanged
method invoked when the value of key A is modified inobservable
.Likewise, if we only care about key B, we can do:
If we care about both known keys, we can simply register them both:
Also, we can do something particularly clever and basically register the Observer for every Key known to its own Dictionary:
The above would register
observer
withobservable
for every key contained inobserver
‘skeyValues
dictionary.Ultimately, you can register the
observer
with theobservable
for any keys you want:Let’s output the initial values of all of our keys before we invoke any code that would modify their values:
This would output:
So, now that we can register the Keyed Observer with the Observer for whatever key or keys we wish, let’s trigger the Observer Pattern in the
observer
:The above will then update the value if A from “Hello” to “World”.
If we repeat the following code:
This would output:
Okay, so what if we change the value for key “C”? What will happen?
Now, if we repeat the following code:
This would output:
Note that the
observer
was not notified about the change to the value of key C. This is becauseobserver
is not observingobservable
for changes to key C.This is the value of Keyed Observation Pattern. Put simply: not all Observations are meaningful to all Observers. So, as you have now seen, Keyed Observeration enables our Observers to be notified specifically of changes relevant to that Observer.
Overloaded
addObserver
,removeObserver
,addKeyedObserver
, andremoveKeyedObserver
methodsAs of version 1.1.0, all useful combination overloads for the above-specified methods of
ObservableClass
,ObservableThread
,ObservableThreadSafeClass
,KeyedObservableClass
,KeyedObservableThread
, andKeyedObservableThreadSafeClass
have been provided to streamline the adding and removal of Observers with/from an Observable.Adding a single Observer to an Observable
Adding multiple Observers to an Observable
Adding a single Keyed Observer to a Keyed Observable with a single Key
Adding a single Keyed Observer to a Keyed Observable with multiple Keys
Adding multiple Keyed Observers to a Keyed Observable with a single Key
Adding multiple Keyed Observers to a Keyed Observable with multiple Keys
removeObserver
andremoveKeyedObserver
also provide the same overloads as shown above.Additional Useful Hints
There are a few additional useful things you should know about this Package.
A single Observable can invoke
withObservers
for any number of Observer ProtocolsThis library intentionally performs run-time type checks against each registered Observer to ensure that it conforms to the explicitly-defined Observer Protocol being requested by your
withObservers
Closure method.Simple example protocols:
Which can then both be used by the same
ObservableClass
,ObservableThreadSafeClass
, orObservableThread
descendant:Any number of Observer Protocols can be marshalled by any of our Observable types, and only Observers conforming to the explicitly-specified Observer Protocol will be passed into your
withObservers
Closure method.License
Observable
is available under the MIT license. See the LICENSE file for more info.Join us on Discord
If you require additional support, or would like to discuss
Observable
, Swift, or any other topics related to Flowduino, you can join us on Discord.