Re: Translating KVO-ed property to Swift
Re: Translating KVO-ed property to Swift
- Subject: Re: Translating KVO-ed property to Swift
- From: Charles Srstka <email@hidden>
- Date: Mon, 17 Apr 2017 14:03:36 -0500
> On Apr 17, 2017, at 1:09 PM, Quincey Morris <email@hidden> wrote:
>
> On Apr 17, 2017, at 05:40 , Jean-Daniel <email@hidden> wrote:
>
>> This is a good practice, but I don’t think this is required for computed property, especially if you take care of willChange/didChange manually, as the OP does.
>
> Here is what the Swift interoperability documentation says (https://developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/AdoptingCocoaDesignPatterns.html):
And the reason for this is because Cocoa swizzles the accessor and adds the willChangeValue() and didChangeValue() calls. If you’re calling these yourself (or if you’re a computed property that registers its dependencies via keyPathsForValuesAffecting<property> methods), you don’t need dynamic.
>> "You can use key-value observing with a Swift class, as long as the class inherits from the NSObject class. You can use these three steps to implement key-value observing in Swift.
>>
>> "1. Add the dynamic modifier to any property you want to observe. […]”
>
> Here is what the Swift language documentation says (https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Declarations.html):
>
>> “dynamic”
>>
>> "Apply this modifier to any member of a class that can be represented by Objective-C. When you mark a member declaration with the dynamic modifier, access to that member is always dynamically dispatched using the Objective-C runtime. Access to that member is never inlined or devirtualized by the compiler.”
>
> That is, unless you specify “dynamic” there’s no *guarantee* that invocations to the property accessors will use obj_msgSend, and since there’s no way in Swift to guarantee that obj_msgSend *won’t* be used for the property, the outcome for automatic KVO is unpredictable.
You cannot guarantee that the property will be called via objc_msgSend, which is important if you’re relying on the swizzled accessor to send the property notifications. If you’re sending them yourself, it doesn’t matter one way or another how the property was called, as long as you add @objc so that clients that do expect to be able to use objc_msgSend (most importantly, NSObject’s observation support) can do it.
> On Apr 17, 2017, at 08:07 , Charles Srstka <email@hidden> wrote:
>>
>> // Note that this doesn’t need to be dynamic, since we are not relying on Cocoa’s built-in automatic swizzling,
>> // which is only needed if we are not calling willChangeValue(forKey:) and didChangeValue(forKey:) ourselves.
>> @objc var version: String {
>> willSet {
>> // Send the willChange notification, if the value is different from its old value.
>> if newValue != self.version {
>> self.willChangeValue(forKey: #keyPath(version))
>> }
>> }
>> didSet {
>> // Send the didChange notification, if the value is different from its old value.
>> if oldValue != self.version {
>> self.didChangeValue(forKey: #keyPath(version))
>> }
>> }
>> }
>
> I tested what happens (in Swift 3.1, Xcode 8.3.1) using this code:
>
>> private var versionContext = 0
>>
>> class ViewController: NSViewController {
>> @objc /*dynamic*/ var version: String = “” {
>> willSet {
>> if newValue != self.version {
>> self.willChangeValue (forKey: #keyPath(version)) }
>> }
>> didSet {
>> if oldValue != self.version {
>> self.didChangeValue (forKey: #keyPath(version)) }
>> }
>> }
>> override func viewDidLoad () {
>> super.viewDidLoad ()
>> addObserver (self, forKeyPath: #keyPath(version), options: [], context: &versionContext)
>> }
>> override func observeValue (forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
>> print ("observedValue for \(version)")
>> }
>> @IBAction func buttonClicked (_ sender: Any?) { // There’s a button in the UI hooked up to this
>> version = version == "" ? "1" : "\(version)"
>> }
>> }
>
> This version of the code (with “dynamic” commented out) displays the observer message once, as desired, and then not again, as desired. Uncommenting “dynamic” causes the message to be displayed twice the first time, and then once more every subsequent button click.
>
> So, Charles’s approach *appears* to work, because the “version” property isn’t participating in automatic swizzling. However, it’s subtly wrong because there’s no way to prevent other source code from leading the compiler to *deduce* that the method is dynamic. Once that happens, there’s an extra unwanted notification every time the property is set.
“This approach *appears* to work, but it stops working if I change something that was deliberately set the way it was so that it would work.”
You also forgot the automaticallyNotifiesObserversOfVersion property in the first bit of my example. With that, your example logs only once, even with dynamic:
> import Foundation
>
> class Foo: NSObject {
> // Only needed if the property will be accessed by Objective-C code, since Swift code won’t see the swizzled accessors anyway for a non-dynamic property.
> // @objc annotation is needed on every method we write here, since otherwise it’ll quit working when @objc inference is removed in Swift 4 (SE-0160)
> @objc private static let automaticallyNotifiesObserversOfVersion: Bool = false
>
> // Our actual version property. If you want, you can create a private property named “mVersion” and it will function like an instance variable.
> // But I’d probably just use willSet and didSet.
> // Note that this doesn’t need to be dynamic, since we are not relying on Cocoa’s built-in automatic swizzling,
> // which is only needed if we are not calling willChangeValue(forKey:) and didChangeValue(forKey:) ourselves.
> @objc dynamic var version: String {
> willSet {
> // Send the willChange notification, if the value is different from its old value.
> if newValue != self.version {
> self.willChangeValue(forKey: #keyPath(version))
> }
> }
> didSet {
> // Send the didChange notification, if the value is different from its old value.
> if oldValue != self.version {
> self.didChangeValue(forKey: #keyPath(version))
> }
> }
> }
>
> private var kvoContext = 0
>
> override init() {
> self.version = "4K78"
>
> super.init()
>
> self.addObserver(self, forKeyPath: #keyPath(version), options: [], context: &kvoContext)
> }
>
> override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
> if context == &kvoContext {
> print("version is now \(self.version)")
> } else {
> super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
> }
> }
> }
>
> let foo = Foo()
>
> foo.version = "6C115"
> foo.version = "6C115"
outputs "version is now 6C115” once and only once.
> And again, in the converse scenario (automatic KVO, where you want notifications unconditionally) the “dynamic” keyword isn’t optional.
If you’re sending the notifications manually in the accessor, it’s pretty darn guaranteed that those notifications are going to be sent.
> The correct solution, I claim, is to replace the declaration of “version” with this:
>
>> static func automaticallyNotifiesObserversOfVersion () -> Bool { return false }
>> @objc dynamic var version: String = “” { … }
>
> and then use either Charles’ or Jean-Daniel’s logic to generate the notifications manually as desired.
>
> (BTW, the “@objc” is currently redundant, but will soon become required, via SE-0160.)
That example is incorrect, because you left out the @objc on automaticallyNotifiesObserversOfVersion, which will cause it to subtly stop working after SE-0160 is implemented.
Charles
_______________________________________________
Cocoa-dev mailing list (email@hidden)
Please do not post admin requests or moderator comments to the list.
Contact the moderators at cocoa-dev-admins(at)lists.apple.com
Help/Unsubscribe/Update your Subscription:
This email sent to email@hidden