> On Apr 17, 2017, at 1:09 PM, Quincey Morris 
> <quinceymor...@rivergatesoftware.com> wrote:
> 
> On Apr 17, 2017, at 05:40 , Jean-Daniel <mail...@xenonium.com> 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 <cocoa...@charlessoft.com> 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 (Cocoa-dev@lists.apple.com)

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:
https://lists.apple.com/mailman/options/cocoa-dev/archive%40mail-archive.com

This email sent to arch...@mail-archive.com

Reply via email to