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 hidden or 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() | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Important note regarding
ObservableThread
: Wherever you would invokeobjectWillChange.send()
, you should instead usenotifyChange()
.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.