1

As a newbie in macOS programming and in particular with Swift, I’ve been disappointed to discover that the structure’s properties (attributes) do not work with Cocoa Bindings. In my data model (for my convenience) I made extensive use of the geometric structures exposed by CoreGraphics framework and I don’t want to restructure all of the code in order to use the native Binding mechanism with UI controls provided by Cocoa.

So I'm trying to extend the protocol KVC / KVO to support KeyPath in all basic geometric structures - exactly as does the CoreAnimation framework in CALayer class.

The result seems to work as I expected but I am worried about not having fully understood all the rules under the mechanism and having produced an implementation thus weak and prone to errors.

Below the code I’m working on. (For “easy-reading” this is a cut-off that only intercepts the CGSize structure but the full version is also identical for the other geometry types).

I turn to experts for advice and some suggestions over the weaknesses of this approach and eventually an alternative way that allows bindings between the individual properties of a geometric structure and the various user interface controls.

[Dev on Xcode 8.2.1 (Swift 3.0.1)]

Thank you all

1. GeometryBindableObject class: the NSObject subclass used to extend KVC/KVO capabilities

open class GeometryBindableObject: NSObject {

    open override func value(forKeyPath keyPath: String) -> Any? {

        var key = String(), subPath = String()

        if keyPath.contains(".") {
            let keys = keyPath.characters.split(separator: ".", maxSplits: 1).map { String($0) }
            (key, subPath) = (keys[0], keys[1])
        } else {
            key = keyPath
        }

        var value = self.value(forKey: key)

        if (!subPath.isEmpty) {
            if let object = value as? CGSize {
                value = getAttribute(subPath, for: object)
            } else
            ...
            if let object = value as? NSObject {
                value = object.value(forKeyPath: subPath)
            }
        }
        return value
    }

    open override func setValue(_ value: Any?, forKeyPath keyPath: String) {

        var key = String(), subPath = String()

        if keyPath.contains(".") {
            let keys = keyPath.characters.split(separator: ".", maxSplits: 1).map { String($0) }
            (key, subPath) = (keys[0], keys[1])
        } else {
            key = keyPath
        }

        if (!subPath.isEmpty) {
            let keyValue = self.value(forKey: key)
            if var object = keyValue as? CGSize {
                setAttribute(value, subPath, for: &object)
                setValue(object, forKey: key)
            } else
            ...
            if let object = keyValue as? NSObject {
                object.setValue(value, forKeyPath: subPath)
            }
        } else {
            setValue(value, forKey: key)
        }
    }

    open override func addObserver(_ observer: NSObject, forKeyPath keyPath: String, options: NSKeyValueObservingOptions = [], context: UnsafeMutableRawPointer?) {

        var hackedKeyPath = keyPath

        if keyPath.contains(".") {
            let keys = keyPath.characters.split(separator: ".", maxSplits: 1).map { String($0) }
            let key = keys[0]

            var value = self.value(forKey: key)

            if let object = value as? CGSize {
                hackedKeyPath = key
            }
        }
        super.addObserver(observer, forKeyPath: hackedKeyPath, options: options, context: context)
    }
}

2. GeometryBindableObject extension: helper functions to get and set properties on handled value types

private extension GeometryBindableObject {

    // getter & setter for CGSize attributes
    func getAttribute(_ key: String, for object: CGSize) -> Any? {
        switch key {
        case "width":
            return object.width
        case "height":
            return object.height
        default:
            return value(forUndefinedKey: key)
        }
    }
    func setAttribute(_ value: Any?, _ key: String, for object: inout CGSize) {
        switch key {
        case "width":
            object.width = CGFloat(value as! NSNumber)
        case "height":
            object.height = CGFloat(value as! NSNumber)
        default:
            setValue(value, forUndefinedKey: key)
        }
    }
}

...and just to try in a playground environment...

class myObject: GeometryBindableObject {

    var size: NSSize = NSMakeSize(100, 100)
}

class myContainer: NSObject {

    var object = myObject()
}

let aBox = myContainer()

aBox.value(forKeyPath: "object.size.width")            //100
aBox.setValue(200, forKeyPath: "object.size.width")
aBox.value(forKeyPath: "object.size.width")            //200
princi
  • 11
  • 3

0 Answers0