Skip to content

Instantly share code, notes, and snippets.

@avaidyam
Last active June 28, 2017 10:09
Show Gist options
  • Save avaidyam/3cd0029a71ca18f2eade90bc7f205452 to your computer and use it in GitHub Desktop.
Save avaidyam/3cd0029a71ca18f2eade90bc7f205452 to your computer and use it in GitHub Desktop.
A test of a KVO-based bindings replacement.
import Foundation
/// A `Binding` connects two objects' properties such that if one object's property
/// value were to ever update, the other object's property value would do so as well.
/// Thus, both objects' properties are kept in sync. The objects and their properties
/// need not be the same, however, their individual properties' types must be the same.
public class Binding<T: NSObject, U: NSObject, X, Y> {
/// Describes the initial state the `Binding` should follow. That is, upon creation
/// whether to set the "left hand side" object's value to the "right hand side"'s
/// vice versa, or to do nothing and let the first event synchronize the values.
public enum InitialState {
/// Do nothing and let the first event synchronize the values.
case none
/// Set the "right hand side" object's value to the "left hand side"'s.
case left
/// Set the "left hand side" object's value to the "right hand side"'s.
case right
}
/// Describes a KVO observation from `A` -> `B`, where `A` is the object
/// and `B` is the KeyPath being observed on `A`. In a `Binding`, it is intended
/// that there exist two of these, for the "left and right handed sides".
private struct Descriptor<A: NSObject, B> {
fileprivate weak var object: A? = nil
fileprivate let keyPath: ReferenceWritableKeyPath<A, B>
fileprivate var observation: NSKeyValueObservation?
}
/// The "left-hand-side" `Descriptor` in the Binding.
/// See `Binding.Descriptor` for more information.
private var left: Descriptor<T, X>
/// The "right-hand-side" `Descriptor` in the Binding.
/// See `Binding.Descriptor` for more information.
private var right: Descriptor<U, Y>
/// Returns whether the `Binding` is currently being propogated.
/// This typically means something has triggered a KVO event.
public private(set) var propogating: Bool = false
/// Defines the `Transformer` to use when propogating this `Binding`. By
/// default, it attempts a cast between `X` and `Y`, otherwise faulting.
public let transformer: Transformer<X, Y>
/// Executes if present when propogation has completed between the two ends
/// of this `Binding`.
public var propogationHandler: (() -> ())? = nil
/// Creates a new `Binding<...>` between two objects on independent `KeyPath`s
/// whose types are identical. The `Binding` will be unbound automatically
/// when deallocated (set to `nil`).
public init(between left: (T, ReferenceWritableKeyPath<T, X>), and right: (U, ReferenceWritableKeyPath<U, Y>),
transformer: Transformer<X, Y> = Transformer(), with initialState: InitialState = .none) {
// Assign descriptors and transformer.
self.transformer = transformer
self.left = Descriptor(object: left.0, keyPath: left.1, observation: nil)
self.right = Descriptor(object: right.0, keyPath: right.1, observation: nil)
// Set up the "between" observations.
self.left.observation = left.0.observe(left.1) { _, _ in
self.perform { l, r in
r[keyPath: self.right.keyPath] = self.transformer.transform(x: l[keyPath: self.left.keyPath])
}
}
self.right.observation = right.0.observe(right.1) { _, _ in
self.perform { l, r in
l[keyPath: self.left.keyPath] = self.transformer.transform(y: r[keyPath: self.right.keyPath])
}
}
// Establish initial state.
switch initialState {
case .none: break
case .left: right.0[keyPath: right.1] = self.transformer.transform(x: left.0[keyPath: left.1])
case .right: left.0[keyPath: left.1] = self.transformer.transform(y: right.0[keyPath: right.1])
}
}
/// Manually invalidate the "left-hand-side" and "right-hand-side" observations on deallocation.
deinit {
self.left.observation?.invalidate()
self.right.observation?.invalidate()
}
/// Internally handles state management during propogation. The handler will
/// not be invoked if either object in the `Binding` have been deallocated.
private func perform(_ propogation: (T, U) -> ()) {
guard let l = self.left.object, let r = self.right.object, !self.propogating else { return }
self.propogating = true
propogation(l, r)
self.propogating = false
self.propogationHandler?()
}
}
import Cocoa
// Test for Binding<A, B, C> to see if it'll keep the window title, and two textfields in sync.
class ViewController: NSViewController, NSTextFieldDelegate {
@IBOutlet var text1: NSTextField! = nil
@IBOutlet var text2: NSTextField! = nil
private var syncBinding: Binding<NSTextField, NSTextField, String, String>? = nil
private var titleBinding: Binding<NSWindow, NSTextField, CGFloat, String>? = nil
override func viewWillAppear() {
self.syncBinding = Binding(between: (self.text1, \.stringValue), and: (self.text2, \.stringValue))
self.titleBinding = Binding(between: (self.view.window!, \.alphaValue), and: (self.text1, \.stringValue),
transformer: ReverseTransformer(from: LosslessStringTransformer(default: 0.0)))
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) {
self.view.window!.alphaValue = 0.5
}
}
// Because typing doesn't invoke KVO...
override func viewDidLoad() {
self.text1.delegate = self
self.text2.delegate = self
}
// Because typing doesn't invoke KVO...
override func controlTextDidChange(_ obj: Notification) {
let text = obj.object as! NSTextField
text.willChangeValue(for: \.stringValue)
text.didChangeValue(for: \.stringValue)
}
}
// Because CGFloat doesn't conform to this protocol for some reason.
extension CGFloat: LosslessStringConvertible {
public init?(_ description: String) {
if let x = Double(description) {
self.init(x)
} else { return nil }
}
}
import Foundation
/// A `Transformer` converts values between `X` and `Y` and can be chained to
/// other transformers to process an origin type to its destination type through
/// any number of intermediate `Transformer`s.
///
/// It is intended that this class be subclassed for implementation.
///
/// Implementation Note: It is required that `transform(x:)` and `transform(y:)`
/// remain separately typed functions, as subclasses with generic parameters
/// `where X == Y` will not compile otherwise due to conflicting overrides.
public class Transformer<X, Y> {
/// Transform the value `x` from origin type `X` to destination type `Y`.
/// Note: this is considered to be the reverse transformation of `transform(y:)`.
public func transform(x: X) -> Y {
if let x = x as? Y { //X.self is Y.Type
return x
}
fatalError("Cannot transform-cast \(X.self) to \(Y.self)! Define and use a Transformer<\(X.self),\(Y.self)>.")
}
/// Transform the value `y` from origin type `Y` to destination type `X`.
/// Note: this is considered to be the reverse transformation of `transform(x:)`.
public func transform(y: Y) -> X {
if let y = y as? X { //Y.self is X.Type
return y
}
fatalError("Cannot transform-cast \(Y.self) to \(X.self)! Define and use a Transformer<\(X.self),\(Y.self)>.")
}
}
/// A `ReverseTransformer` composes an original `Transformer` with its generic
/// parameters `X` and `Y` reversed. Using this will allow fitting a square block
/// into a circular hole, in essence.
///
/// Note: this is only necessary until a better "swappable generic parameter"
/// method is found.
public class ReverseTransformer<Y, X>: Transformer<Y, X> {
private let originalTransformer: Transformer<X, Y>
/// Create a `Transformer` that acts in reverse of the `from` `Transformer`.
public init(from originalTransformer: Transformer<X, Y>) {
self.originalTransformer = originalTransformer
}
/// Transform the value `x` from origin type `Y` to destination type `X`.
/// Note: this is considered to be the reverse transformation of `transform(y:)`.
public override func transform(x: Y) -> X {
return self.originalTransformer.transform(y: x)
}
/// Transform the value `y` from origin type `X` to destination type `Y`.
/// Note: this is considered to be the reverse transformation of `transform(x:)`.
public override func transform(y: X) -> Y {
return self.originalTransformer.transform(x: y)
}
}
/// A `Transformer` that allows custom transformation closure between `X` and `Y`.
public class CustomTransformer<X, Y>: Transformer<X, Y> {
/// The operation closure transforming `X` into `Y`.
public var forward: (X) -> (Y)
/// The operation closure transforming `Y` into `X`.
public var backward: (Y) -> (X)
/// Create a `CustomTransformer` providing closures.
public init(forward: @escaping (X) -> (Y), backward: @escaping (Y) -> (X)) {
self.forward = forward
self.backward = backward
}
/// Transform the value `x` from origin type `X` to destination type `Y`.
/// Note: this is considered to be the reverse transformation of `transform(y:)`.
public override func transform(x: X) -> Y {
return self.forward(x)
}
/// Transform the value `y` from origin type `Y` to destination type `X`.
/// Note: this is considered to be the reverse transformation of `transform(x:)`.
public override func transform(y: Y) -> X {
return self.backward(y)
}
}
/// Provides a wrapper type for `NSValueTransformer`. Not recommended for usage.
public class NSTransformer: Transformer<Any?, Any?> {
private let originalTransformer: ValueTransformer
/// Create a `Transformer` that wraps the given `NSValueTransformer`.
public init(from originalTransformer: ValueTransformer) {
self.originalTransformer = originalTransformer
}
public override func transform(x: Any?) -> Any? {
return self.originalTransformer.transformedValue(x)
}
public override func transform(y: Any?) -> Any? {
if !type(of: self.originalTransformer).allowsReverseTransformation() {
fatalError("The NSValueTransformer does not allow reverse transformation.")
}
return self.originalTransformer.reverseTransformedValue(y)
}
}
/// Transformes a `LosslessStringConvertible` to and from a `String`. For more
/// information, see the `LosslessStringConvertible` documentation. This will generally
/// work for any number/boolean to String (and vice versa) conversion.
public class LosslessStringTransformer<T: LosslessStringConvertible>: Transformer<String, T> {
private var defaultValue: () -> (T)
/// Create a `LosslessStringTransformer` with a given default value.
public init(default: @autoclosure @escaping () -> (T)) {
self.defaultValue = `default`
}
public override func transform(x: String) -> T {
return T(x) ?? self.defaultValue()
}
public override func transform(y: T) -> String {
return y.description
}
}
/// Transforms an `Optional` type to a non-`Optional` type, substituting `nil`
/// for a provided default value.
public class OptionalTransformer<A>: Transformer<A?, A> {
private var defaultValue: () -> (A)
/// Create an `OptionalTransformer` with a given default value.
public init(default: @autoclosure @escaping () -> (A)) {
self.defaultValue = `default`
}
public override func transform(x: A?) -> A {
return x ?? self.defaultValue()
}
public override func transform(y: A) -> A? {
return y
}
}
/// Similar to the `OptionalTransformer`, but instead, the `NilTransformer` infers
/// the `Optional`'s existence and transforms it into a true or false.
///
/// Note: a "reverse" transformation yields a useless Optional(Bool).
public class NilTransformer: Transformer<Any?, Bool> {
private var reversed: Bool
/// Create a `NilTransformer` that is optionally reversed. (That is, instead
/// of checking for a non-nil value, check for a nil value.)
public init(reversed: Bool = false) {
self.reversed = reversed
}
public override func transform(x: Any?) -> Bool {
return self.reversed ? x == nil : x != nil
}
public override func transform(y: Bool) -> Any? {
return Optional(y)
}
}
/// Negates a `Bool` value. Not very interesting. :(
public class NegateTransformer: Transformer<Bool, Bool> {
public override func transform(x: Bool) -> Bool {
return !x
}
public override func transform(y: Bool) -> Bool {
return !y
}
}
@avaidyam
Copy link
Author

Still needs support for NSEditor/Registration, NSValidation* and weird things like NSMenu (ContentPlacementTag), and ArrayController.

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