Skip to content

Instantly share code, notes, and snippets.

@LK-Simon
Last active July 11, 2022 14:40
Show Gist options
  • Save LK-Simon/af7f22f9ae5f9306a33fe8d0ee536dc8 to your computer and use it in GitHub Desktop.
Save LK-Simon/af7f22f9ae5f9306a33fe8d0ee536dc8 to your computer and use it in GitHub Desktop.
Generic Observable Class and Thread types, elegant and protocol-conformance-driven
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()
}
}
}
@LK-Simon
Copy link
Author

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