Skip to content

Instantly share code, notes, and snippets.

@JunyuKuang
Last active March 24, 2020 09:54
Show Gist options
  • Save JunyuKuang/72314f4154b00e05922d86357d3511c6 to your computer and use it in GitHub Desktop.
Save JunyuKuang/72314f4154b00e05922d86357d3511c6 to your computer and use it in GitHub Desktop.
An NSKeyValueObservation replacement.
//
// JonnyKeyValueObserver.swift
//
// Copyright (c) 2020 Junyu Kuang <lightscreen.app@gmail.com>
//
// 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.
import Foundation
public protocol JonnyKeyValueObservable {
}
extension NSObject: JonnyKeyValueObservable {
}
public extension JonnyKeyValueObservable where Self: NSObject {
func kjy_observe<Value>(_ keyPath: KeyPath<Self, Value>, options: NSKeyValueObservingOptions = [], referenceMode: JonnyKeyValueObservedObjectReferenceMode = .strong, changeHandler: @escaping (Self, JonnyKeyValueChange<Value>) -> Void) -> JonnyKeyValueObserver {
kjy_observe(keyPath._kvcKeyPathString!, valueType: Value.self, options: options, referenceMode: referenceMode, changeHandler: changeHandler)
}
func kjy_observe(_ keyPath: String, options: NSKeyValueObservingOptions = [], referenceMode: JonnyKeyValueObservedObjectReferenceMode = .strong, changeHandler: @escaping (Self, JonnyKeyValueChange<Any>) -> Void) -> JonnyKeyValueObserver {
kjy_observe(keyPath, valueType: Any.self, options: options, referenceMode: referenceMode, changeHandler: changeHandler)
}
func kjy_observe<Value>(_ keyPath: String, valueType: Value.Type, options: NSKeyValueObservingOptions, referenceMode: JonnyKeyValueObservedObjectReferenceMode = .strong, changeHandler: @escaping (Self, JonnyKeyValueChange<Value>) -> Void) -> JonnyKeyValueObserver {
_JonnyKeyValueObserver(observedObject: self, keyPath: keyPath, options: options, referenceMode: referenceMode) { object, changeInfo in
guard let object = object as? Self,
let change = JonnyKeyValueChange<Value>(info: changeInfo) else { return }
changeHandler(object, change)
}
}
}
/// Defines how an receiver of `kjy_observe()` (that is, the observed object) should be referenced by `JonnyKeyValueObserver`.
public enum JonnyKeyValueObservedObjectReferenceMode {
/// Strongly reference the observed object.
///
/// You should not use this case if `JonnyKeyValueObserver` is also strongly referenced by the observerd object, as it creates retain cycle (use `unownedUnsafe` instead).
case strong
/// Weakly reference the observed object, with the assumption that observed object will always exist throughout the life cycle of `JonnyKeyValueObserver`.
///
/// If `deinit` of observed object happens before `JonnyKeyValueObserver.invalidate()`, and `JonnyKeyValueObserver` is not strongly referenced by the observerd object, the program will crash.
///
/// This case is suitable where `JonnyKeyValueObserver` is strongly referenced by the observed object.
case unownedUnsafe
/// Weakly reference the observed object.
///
/// You should not pick this case in general.
///
/// In some cases, weakly reference causes KVO crash: `KVO_IS_RETAINING_ALL_OBSERVERS_OF_THIS_OBJECT_IF_IT_CRASHES_AN_OBSERVER_WAS_OVERRELEASED_OR_SMASHED`.
///
/// Also, you should not use this case if `JonnyKeyValueObserver` is strongly referenced by the observerd object and you're unable to `invalidate()` the observer **before** observerd object's `deinit` is called. Because by the time `deinit` is called, the observer's weak reference to observed object is already cleared by the system, which causing `removeObserver` failure and crashing program.
case weak
}
public protocol JonnyKeyValueObserver {
/// Clear `changeHandler` and stop observation.
/// This will be called automatically when the observer is deinited.
func invalidate()
}
private class _JonnyKeyValueObserver: NSObject, JonnyKeyValueObserver {
private var observedObjectContainer: ObservedObjectContainer?
private let keyPath: String
private var changeHandler: ((NSObject, [NSKeyValueChangeKey: Any]) -> Void)?
init(observedObject: NSObject, keyPath: String, options: NSKeyValueObservingOptions, referenceMode: JonnyKeyValueObservedObjectReferenceMode, changeHandler: @escaping (NSObject, [NSKeyValueChangeKey: Any]) -> Void) {
self.observedObjectContainer = ObservedObjectContainer(observedObject: observedObject, referenceMode: referenceMode)
self.keyPath = keyPath
self.changeHandler = changeHandler
super.init()
observedObject.addObserver(self, forKeyPath: keyPath, options: options, context: nil)
}
deinit {
invalidate()
}
func invalidate() {
changeHandler = nil
observedObjectContainer?.observedObject?.removeObserver(self, forKeyPath: keyPath)
observedObjectContainer = nil
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard let change = change,
keyPath == self.keyPath,
let object = object as? NSObject,
object === observedObjectContainer?.observedObject else { return }
changeHandler?(object, change)
}
}
private extension _JonnyKeyValueObserver {
class ObservedObjectContainer {
let referenceMode: JonnyKeyValueObservedObjectReferenceMode
private var strong: NSObject?
private weak var weak: NSObject?
private unowned(unsafe) var unownedUnsafe: NSObject?
init(observedObject: NSObject, referenceMode: JonnyKeyValueObservedObjectReferenceMode) {
self.referenceMode = referenceMode
switch referenceMode {
case .strong:
strong = observedObject
case .weak:
weak = observedObject
case .unownedUnsafe:
unownedUnsafe = observedObject
}
}
var observedObject: NSObject? {
switch referenceMode {
case .strong:
return strong
case .weak:
return weak
case .unownedUnsafe:
return unownedUnsafe
}
}
}
}
public struct JonnyKeyValueChange<Value> {
public let kind: NSKeyValueChange
/// Will only be non-nil if `NSKeyValueObservingOptions.new` is passed to `kjy_observe()`.
/// In general, get the most up to date value by accessing it directly on the observed object instead.
public let newValue: Value?
/// Will only be non-nil if `NSKeyValueObservingOptions.old` is passed to `kjy_observe()`.
public let oldValue: Value?
/// Will be nil unless the observed `KeyPath` refers to an ordered to-many property.
public let indexes: IndexSet?
/// Will be true if this change observation is being sent before the change happens, due to `NSKeyValueObservingOptions.prior` being passed to `kjy_observe()`.
public let isPrior: Bool
fileprivate init?(info: [NSKeyValueChangeKey : Any]) {
guard let kindValue = info[.kindKey] as? UInt,
let kind = NSKeyValueChange(rawValue: kindValue) else { return nil }
self.kind = kind
newValue = info[.newKey] as? Value
oldValue = info[.oldKey] as? Value
indexes = info[.indexesKey] as? IndexSet
isPrior = info[.notificationIsPriorKey] as? Bool ?? false
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment