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() | |
} | |
} | |
} |
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
Both
ObservableClass
andObservableThread
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!and
The base classes automatically perform Protocol Conformance Checks for you, so every
observer
passed into yourwithObservers
closure will conform to the Protocol you explicitly specify in your closure.