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

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.

@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