diff --git a/ObserverSet.swift b/ObserverSet.swift index fca5b05..cc36a70 100644 --- a/ObserverSet.swift +++ b/ObserverSet.swift @@ -26,122 +26,198 @@ import Dispatch /// A reference to an entry in the list of observers. Use this to remove an observer. -open class ObserverSetEntry { - - public typealias ObserverCallback = (Any) -> (Parameters) -> Void - - fileprivate weak var observer: AnyObject? - fileprivate let callBack: ObserverCallback - - fileprivate init(observer: AnyObject?, callBack: @escaping ObserverCallback) { - self.observer = observer - self.callBack = callBack - } - -} +public class ObserverSetEntry { -/// A set of observers that can be notified of certain actions. A more Swift-like version of NSNotificationCenter. -open class ObserverSet { - - // MARK: - Private properties - - private var entries: [ObserverSetEntry] = [] - - // MARK: - Initialisers - - /** - Creates a new instance of an observer set. - - - returns: A new instance of an observer set. - */ - public init() {} - - // MARK: - Public functions - - /** - Adds a new observer to the set. - - - parameter observer: The object that is to be notified. - - parameter callBack: The function to call on the observer when the notification is to be delivered. - - - returns: An entry in the list of observers, which can be used later to remove the observer. - */ - @discardableResult - open func add(_ observer: ObserverType, _ callBack: @escaping (ObserverType) -> (Parameters) -> Void) -> ObserverSetEntry { - let entry = ObserverSetEntry(observer: observer) { observer in callBack(observer as! ObserverType) } - synchronized { - self.entries.append(entry) - } - return entry - } - - /** - Adds a new function to the list of functions to invoke when a notification is to be delivered. - - - parameter callBack: The function to call when the notification is to be delivered. - - - returns: An entry in the list of observers, which can be used later to remove the observer. - */ - @discardableResult - open func add(_ callBack: @escaping (Parameters) -> Void) -> ObserverSetEntry { - return self.add(self) { ignored in callBack } - } - - /** - Removes an observer from the list, using the entry which was returned when adding. - - - parameter entry: An entry returned when adding a new observer. - */ - open func remove(_ entry: ObserverSetEntry) { - synchronized { - self.entries = self.entries.filter{ $0 !== entry } - } - } - + fileprivate var isExpired: Bool { false } - /** - Removes an observer from the list. + fileprivate func call(_ parameters: Parameters) { + // overridden + } +} - - parameter observer: An observer to remove from the list of observers. - */ - open func removeObserver(_ observer: AnyObject) { - synchronized { - self.entries = self.entries.filter{ $0.observer !== observer } - } - } +private class ObserverSetEntryImpl: ObserverSetEntry { + + typealias ObserverCallback = (ObserverType) -> (Parameters) -> Void + + private(set) weak var observer: ObserverType? + private let callBack: ObserverCallback + private let queue: DispatchQueue? + + override var isExpired: Bool { observer == nil } + + init(queue: DispatchQueue?, observer: ObserverType, callBack: @escaping ObserverCallback) { + self.queue = queue + self.observer = observer + self.callBack = callBack + } + + override func call(_ parameters: Parameters) { + if let observer = observer { + let callBack = self.callBack(observer) + if let queue = queue { + queue.async { + callBack(parameters) + } + } else { + callBack(parameters) + } + } + } + +} - /** - Call this method to notify all observers. - - - parameter parameters: The parameters that are required parameters specified using generics when the instance is created. - */ - open func notify(_ parameters: Parameters) { - var callBackList: [(Parameters) -> Void] = [] - synchronized { - for entry in self.entries { - if let observer = entry.observer { - callBackList.append(entry.callBack(observer)) - } - } - self.entries = self.entries.filter{ $0.observer != nil } - } - for callBack in callBackList { - callBack(parameters) - } - } - - // MARK: - Private functions - // MARK: Locking support - - private var queue = DispatchQueue(label: "com.theappbusiness.ObserverSet", attributes: []) - - private func synchronized(_ f: () -> Void) { - queue.sync(execute: f) - } +/// A set of observers that can be notified of certain actions. A more Swift-like version of NSNotificationCenter. +open class ObserverSet { + + // MARK: - Private properties + + private var entries: [ObserverSetEntry] = [] + + // MARK: - Initialisers + + /** + Creates a new instance of an observer set. + + - returns: A new instance of an observer set. + */ + public init() {} + + // MARK: - Public functions + + /** + Adds a new observer to the set. + + - parameter queue: The queue to call the function on when the notification is delivered. If nil (the default) it will be delivered on the same queue the notification was sent. + - parameter observer: The object that is to be notified. + - parameter callBack: The function to call on the observer when the notification is to be delivered. + + - returns: An entry in the list of observers, which can be used later to remove the observer. + */ + @discardableResult + open func add(queue: DispatchQueue? = nil, _ observer: ObserverType, _ callBack: @escaping (ObserverType) -> (Parameters) -> Void) -> ObserverSetEntry { + let entry = ObserverSetEntryImpl(queue: queue, observer: observer, callBack: callBack) + synchronized { + self.entries.append(entry) + } + return entry + } + + /** + Adds a new function (or closure) to call when a notification is to be delivered. + This callback will be notified until it is removed by passing the return value to the `remove(_:)` method + + The return value is not discardable as the callback should be removed manually. To attach it to the life cycle of an object, use the `add` method that takes a `lifeCycleObserver` parameter. + + - parameter queue: The queue to call the function on when the notification is delivered. If nil (the default) it will be delivered on the same queue the notification was sent. + - parameter callBack: The function to call when the notification is to be delivered. + + - returns: An entry in the list of observers, which should be used later to remove the observer. + */ + open func add(queue: DispatchQueue? = nil, _ callBack: @escaping (Parameters) -> Void) -> ObserverSetEntry { + add(queue: queue, self) { _ in callBack } + } + + /** + Adds a new function (or closure) to call when a notification is to be delivered. + The callback will be removed when the `lifeCycleObserver` object is deallocated + + - parameter queue: The queue to call the function on when the notification is delivered. If nil (the default) it will be delivered on the same queue the notification was sent. + - parameter callBack: The function to call when the notification is to be delivered. + + - returns: An entry in the list of observers, which can be used later to remove the observer. + */ + @discardableResult + open func add(queue: DispatchQueue? = nil, _ lifeCycleObserver: ObserverType, _ callBack: @escaping (Parameters) -> Void) -> ObserverSetEntry { + add(queue: queue, lifeCycleObserver) { _ in callBack } + } + + /** + Sets the keyPath on an observer to the value received when a notification is delivered. + + - parameter queue: The queue to call the function on when the notification is delivered. If nil (the default) it will be delivered on the same queue the notification was sent. + - parameter keyPath: The keyPath to set when the notification is to be delivered. + + - returns: An entry in the list of observers, which can be used later to remove the observer. + */ + @discardableResult + open func add(queue: DispatchQueue? = nil, _ observer: ObserverType, _ keyPath: ReferenceWritableKeyPath) -> ObserverSetEntry { + add(queue: queue, observer) { observer in { observer[keyPath: keyPath] = $0 } } + } + + /** + Removes an observer from the list, using the entry which was returned when adding. + + - parameter entry: An entry returned when adding a new observer. + */ + open func remove(_ entry: ObserverSetEntry) { + synchronized { + self.entries = self.entries.filter{ $0 !== entry } + } + } + + + /** + Removes an observer from the list. + + - parameter observer: An observer to remove from the list of observers. + */ + open func removeObserver(_ observer: ObserverType) { + synchronized { + self.entries.removeAll(where: { ($0 as? ObserverSetEntryImpl)?.observer === observer }) + } + } + + /** + Call this method to notify all observers. + + - parameter parameters: The parameters that are required parameters specified using generics when the instance is created. + */ + open func notify(_ parameters: Parameters) { + let callBackList = synchronized { () -> [ObserverSetEntry] in + self.entries.removeAll(where: { $0.isExpired }) + return self.entries + } + for callBack in callBackList { + callBack.call(parameters) + } + } + + // MARK: - Private functions + // MARK: Locking support + + private var queue = DispatchQueue(label: "com.theappbusiness.ObserverSet", attributes: []) + + private func synchronized(_ f: () -> T) -> T { + queue.sync(execute: f) + } } public extension ObserverSet where Parameters == Void { - public func notify() { - notify(()) - } + + /** + Call this method to notify all observers. + */ + func notify() { + notify(()) + } + /** + Adds a new observer to the set. + + Convenience method so that the callback function can be `func callback() { ... }` instead of `func callback(_: Void) { ... }` + + - parameter queue: The queue to call the function on when the notification is delivered. If nil (the default) it will be delivered on the same queue the notification was sent. + - parameter observer: The object that is to be notified. + - parameter callBack: The function to call on the observer when the notification is to be delivered. + + - returns: An entry in the list of observers, which can be used later to remove the observer. + */ + @discardableResult + func add(queue: DispatchQueue? = nil, _ observer: ObserverType, _ callBack: @escaping (ObserverType) -> () -> Void) -> ObserverSetEntry { + add(queue: queue, observer) { observer in + { _ in + callBack(observer)() + } + } + } + }