Skip to content

Instantly share code, notes, and snippets.

@christopher-fuller
Last active December 9, 2016 20:53
Show Gist options
  • Save christopher-fuller/a52816d904b19d9991dfd8caab8fd0ab to your computer and use it in GitHub Desktop.
Save christopher-fuller/a52816d904b19d9991dfd8caab8fd0ab to your computer and use it in GitHub Desktop.
//
// KeyValueObservingContext.swift
// Created by Christopher Fuller for Southern California Public Radio
// Original version at https://github.com/SCPR/swift-experiments/blob/master/source/KeyValueObservable.swift
// Updated by Christopher Fuller for Swift 3 compatibility
//
// Copyright (c) 2016 Southern California Public Radio
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
class KeyValueObserver {
private weak var observingContext: KeyValueObservingContext!
fileprivate var context = 0
fileprivate let keyPath: String
fileprivate weak var object: AnyObject!
private let closure: KeyValueObservingContext.ValueChanged<Any?>
fileprivate init(observingContext: KeyValueObservingContext, keyPath: String, object: AnyObject, closure: @escaping KeyValueObservingContext.ValueChanged<Any?>) {
self.observingContext = observingContext
self.keyPath = keyPath
self.object = object
self.closure = closure
}
fileprivate func observe(object: AnyObject, oldValue: Any?, newValue: Any?) {
closure(self, object, oldValue, newValue)
}
func remove() {
observingContext.removeObserver(self)
}
}
class KeyValueObservingContext: NSObject {
typealias ValueChanged<T> = (_ observer: KeyValueObserver, _ object: AnyObject, _ oldValue: T, _ newValue: T) -> Void
fileprivate var observers = [KeyValueObserver]()
deinit {
removeObservers()
}
@discardableResult func observe<T: Any>(_ keyPath: String, object: AnyObject, closure: @escaping ValueChanged<T>) -> KeyValueObserver where T: Equatable {
let observer = KeyValueObserver(observingContext: self, keyPath: keyPath, object: object) {
observer, object, oldValue, newValue in
let oldValue = (oldValue as? T)
let newValue = (newValue as? T)
if let oldValue = oldValue, let newValue = newValue, oldValue != newValue {
closure(observer, object, oldValue, newValue)
}
}
addObserver(observer, forKeyPath: keyPath, object: object)
return observer
}
@discardableResult func observe<T: AnyObject>(_ keyPath: String, object: AnyObject, closure: @escaping ValueChanged<T?>) -> KeyValueObserver where T: Equatable {
let observer = KeyValueObserver(observingContext: self, keyPath: keyPath, object: object) {
observer, object, oldValue, newValue in
let oldValue = (oldValue as? T)
let newValue = (newValue as? T)
if oldValue != newValue {
closure(observer, object, oldValue, newValue)
}
}
addObserver(observer, forKeyPath: keyPath, object: object)
return observer
}
func removeObservers(object: AnyObject) {
observers.filter({ $0.object === object }).forEach { removeObserver($0) }
}
func removeObservers() {
observers.forEach { removeObserver($0) }
}
}
private extension KeyValueObservingContext {
func addObserver(_ observer: KeyValueObserver, forKeyPath keyPath: String, object: AnyObject) {
observers.append(observer)
object.addObserver(self, forKeyPath: keyPath, options: [ .new, .old ], context: &observer.context)
}
func removeObserver(_ observer: KeyValueObserver) {
if let index = observers.index(where: { $0 === observer }) {
let observer = observers.remove(at: index)
observer.object.removeObserver(self, forKeyPath: observer.keyPath, context: &observer.context)
}
}
}
extension KeyValueObservingContext {
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard let object = object, let change = change else { return }
if let observer = observers.filter({ &$0.context == context }).first {
let object = (object as AnyObject)
let oldValue = change[.oldKey]
let newValue = change[.newKey]
observer.observe(object: object, oldValue: oldValue, newValue: newValue)
}
}
}
@christopher-fuller
Copy link
Author

Example Usage

import AVFoundation

class Example {

    private let player: AVPlayer! // AVPlayer not required, just using here for example.

    private let context = KeyValueObservingContext()

    init() {

        player = AVPlayer(url: URL(string: "http://domain.com/example.m3u8")!)

        // Observe non-optional value type:
        context.observe(#keyPath(AVPlayer.status), object: player) {
            // Must capture self weakly to avoid retain cycle:
            [ weak self ] (observer, object, oldValue, newValue: Int) in
            // Closure is only executed when value actually changes (i.e. not every time setter is called).
            if let status = AVPlayerStatus(rawValue: newValue), status == .readyToPlay {
                self?.player.play()
            }
        }

        // Observe optional reference type:
        context.observe(#keyPath(AVPlayer.currentItem), object: player) {
            (observer, object, oldValue, newValue: AVPlayerItem?) in
            if let currentItem = newValue {
                print(currentItem)
            }
        }

        // If you want to remove an observer later, you may keep a reference to it:
        let observer = context.observe(#keyPath(AVPlayer.rate), object: player) {
            (observer, object, oldValue, newValue: Float) in
            print(newValue)
        }

        // Arbitary example of removing that observer later:
        DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
            observer.remove()
        }

        // If you want to remove all observers of a particular object later, you can do that too:
        DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
            [ weak self] in
            guard let _self = self else { return }
            _self.context.removeObservers(object: _self.player)
        }

    }

    deinit {
        // Nothing to do here since the removal of observers occurs automatically when context is deallocated.
    }

}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment