-
-
Save LK-Simon/af7f22f9ae5f9306a33fe8d0ee536dc8 to your computer and use it in GitHub Desktop.
import Foundation | |
public protocol Observable { | |
/// Registers an Observer against this Observable Type | |
func addObserver<TObservationProtocol: AnyObject>(_ observer: TObservationProtocol) | |
/// Removes an Observer from this Observable Type | |
func removeObserver<TObservationProtocol: AnyObject>(_ observer: TObservationProtocol) | |
} | |
public protocol Observer { | |
} | |
/// Provides custom Observer subscription and notification behaviour | |
/// Note that this type is also ObservableObject, which means we can invoke `objectWillChange.send() | |
/// To notify your observers, wherever a notification is required, use `withObservers() { <your code here> }` | |
open class ObservableClass: Observable, ObservableObject { | |
struct ObserverContainer{ | |
weak var observer: AnyObject? | |
} | |
private var observers = [ObjectIdentifier : ObserverContainer]() | |
public func addObserver<TObservationProtocol: AnyObject>(_ observer: TObservationProtocol) { | |
observers[ObjectIdentifier(observer)] = ObserverContainer(observer: observer) | |
} | |
public func removeObserver<TObservationProtocol: AnyObject>(_ observer: TObservationProtocol) { | |
observers.removeValue(forKey: ObjectIdentifier(observer)) | |
} | |
internal func withObservers<TObservationProtocol>(_ code: (_ observer: TObservationProtocol) -> ()) { | |
for (id, observation) in observers { | |
guard let observer = observation.observer else { // Check if the Observer still exists | |
observers.removeValue(forKey: id) // If it doesn't, remove the Observer from the collection... | |
continue // ...then continue to the next one | |
} | |
if let typedObserver = observer as? TObservationProtocol { | |
code(typedObserver) | |
} | |
} | |
} | |
} | |
/// Provides custom Observer subscription and notification behaviour for Threads | |
/// The Observers are behind a Semaphore Lock | |
/// Don't modify Observers via any code invoked from `withObservers`or you'll end up in a Deadlock | |
open class ObservableThread: Thread { | |
public struct ObserverContainer { | |
weak var observer: AnyObject? | |
var dispatchQueue: DispatchQueue? | |
} | |
private var observerLock: DispatchSemaphore = DispatchSemaphore(value: 1) | |
private var observers = [ObjectIdentifier : ObserverContainer]() | |
public func addObserver<TObservationProtocol: AnyObject>(_ observer: TObservationProtocol) { | |
observerLock.wait() | |
observers[ObjectIdentifier(observer)] = ObserverContainer(observer: observer, dispatchQueue: OperationQueue.current?.underlyingQueue) | |
observerLock.signal() | |
} | |
public func removeObserver<TObservationProtocol: AnyObject>(_ observer: TObservationProtocol) { | |
observerLock.wait() | |
observers.removeValue(forKey: ObjectIdentifier(observer)) | |
observerLock.signal() | |
} | |
internal func withObservers<TObservationProtocol>(_ code: @escaping (_ observer: TObservationProtocol) -> ()) { | |
self.observerLock.wait() | |
for (id, observation) in observers { | |
guard let observer = observation.observer else { // Check if the Observer still exists | |
observers.removeValue(forKey: id) // If it doesn't, remove the Observer from the collection... | |
continue // ...then continue to the next one | |
} | |
if let typedObserver = observer as? TObservationProtocol { | |
let dispatchQueue = observation.dispatchQueue ?? DispatchQueue.main | |
dispatchQueue.async { | |
code(typedObserver) | |
} | |
} | |
} | |
self.observerLock.signal() | |
} | |
internal func notifyChange() { | |
Task { | |
await notifyChange() | |
} | |
} | |
internal func notifyChange() async { | |
await MainActor.run { | |
objectWillChange.send() | |
} | |
} | |
} |
Here's an example (as requested) for ObservableThread
.
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:
protocol RandomNumberObserver: AnyObject {
func onRandomNumber(_ randomNumber: Int)
}
Any Observer for our Thread will need to conform to the RandomNumberObserver
protocol above.
Now, let's define our RandomNumberObservableThread
class:
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).
Enjoy :)
Both ObservableClass
and ObservableThread
support multiple concurrent Observation Protocols.
You can, for example, in the same object, invoke two (or more) separate withObservers
calls for different supported Observation Protocols!
withObservers { (observer: ObservationProtocolA) in
observer.someFunctionInProtocolA(someValue)
}
and
withObservers { (observer: ObservationProtocolB) in
observer.someFunctionInProtocolB(someOtherValue)
}
The base classes automatically perform Protocol Conformance Checks for you, so every observer
passed into your withObservers
closure will conform to the Protocol you explicitly specify in your closure.
Important note regarding ObservableThread
: Wherever you would invoke objectWillChange.send()
, you should instead use notifyChange()
.
This is simply a wrapper method to ensure that objectWillChange.send()
is executed in the context of the Main Actor. This is required by Swift for any UI-observed Thread.
The above provides subscribable Observer behaviour to any Class inheriting from
ObservableClass
or any Thread Type inheriting fromObservableThread
The
ObservableThread
uses aDispatchSemaphore
lock to ensure that any pending new Observer, or pending departing Observer waits until anywithObservers
call completes its iteration of its current Observers. Basically, they make it threadsafe.Also, any expired Observers (instances which no longer exist) will be automatically removed from the Observer Collection... you don't need to manually invoke
removeObserver
to accommodate object destruction.Simple usage example for
ObservableClass