Last active
July 11, 2022 14:40
-
-
Save LK-Simon/af7f22f9ae5f9306a33fe8d0ee536dc8 to your computer and use it in GitHub Desktop.
Generic Observable Class and Thread types, elegant and protocol-conformance-driven
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
} | |
} | |
} |
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.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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:
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 theObserver
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 :)