[swift-evolution] [Pitch] Changing NSObject dispatch behavior

Charles Srstka cocoadev at charlessoft.com
Thu Dec 15 16:29:39 CST 2016


> On Dec 15, 2016, at 2:51 PM, Michael Ilseman via swift-evolution <swift-evolution at swift.org> wrote:
> 
>> I don't think we should ever make it possible to mark an entire class as `dynamic`. This just reintroduces the Obj-C problem, where many properties support KVO, but not all, and there's no indication on the property itself as to whether it supports it.
>> 
> 
> I’m not familiar enough with these kinds of bugs. Kevin, do you think the existing behavior aligns with or runs counter to safe-by-default?

The problem Kevin describes is still here; ‘dynamic’ itself is quite orthogonal to KVO as a concept. A method being declared ‘dynamic’ is no guarantee that it actually supports KVO, and likewise, a method does not need to be marked ‘dynamic’ in order to support KVO. You can, for example, either call willChangeValue() and didChangeValue() in your willSet and didSet accessors for a stored property, or for a computed property you can simply implement the proper accessor methods to describe the dependencies, as in the example below.

import Foundation

class Watcher: NSObject {
	var kvoContext = 0
	
	init(watched: Watched) {
		super.init()
		watched.addObserver(self, forKeyPath: "foo", options: [], context: &kvoContext)
	}
	
	override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
		if context == &kvoContext {
			print("foo changed; now it's \((object as! Watched).foo)")
		} else {
			super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
		}
	}
}

class Watched: NSObject {
	// not a single ‘dynamic’ member in here:

	class func keyPathsForValuesAffectingFoo() -> Set<String> { return ["bar"] }
	var foo: Int {
		get { return self.bar }
		set(foo) { self.bar = foo }
	}
	
	var bar: Int = 0 {
		willSet { self.willChangeValue(forKey: "bar") }
		didSet { self.didChangeValue(forKey: "bar") }
	}
}

let watched = Watched()
let watcher = Watcher(watched: watched)

watched.bar = 5 // outputs: "foo changed; now it's 5”

- - - - - -

All ‘dynamic’ does for you vis a vis KVO conformance is to automatically add the willChangeValue() and didChangeValue() calls, *for a stored property.* For a computed property, that doesn’t help you, if the dependencies aren’t properly set up. For example, if we replace the Watched class with:

class Watched: NSObject {
	dynamic var foo: Int {
		get { return self.bar }
		set(foo) { self.bar = foo }
	}
	
	dynamic var bar: Int = 0 {
		willSet { self.willChangeValue(forKey: "bar") }
		didSet { self.didChangeValue(forKey: "bar") }
	}
}

- - - - - -

and then change the “bar” property, no notifications will be fired, even though the value of ‘foo’ has indeed changed. So to a client of the class who can only see its interface and not the source code, ‘dynamic’ is useless on its own for determining KVO conformance.

Bottom line: we really do need a new KVO-replacement system for Swift, one that could actually advertise itself as part of the interface instead of relying on documentation (especially since Apple isn’t interested in writing it anymore; the number of classes and methods that just say “No overview available.” in the documentation viewer in Sierra is really quite astounding). Unfortunately, this is probably additive, and thus probably won’t be heard until the next phase of swift-evolution.

Charles



More information about the swift-evolution mailing list