Last active
June 28, 2017 10:09
-
-
Save avaidyam/3cd0029a71ca18f2eade90bc7f205452 to your computer and use it in GitHub Desktop.
A test of a KVO-based bindings replacement.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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?() | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Still needs support for
NSEditor/Registration
,NSValidation*
and weird things likeNSMenu (ContentPlacementTag)
, andArrayController
.