KVC / KVO for Operation (Swift 4)

I have created an AsyncOperation which is a subclass of Operation.


I am overriding the following methods to use KVC:

- isExecuting

- isFinished


Problem:

When I use String in place of KeyPath, my completion block gets executed. If I use KeyPath the completion block of the operation is not getting executed.



Works ok:

willChangeValue(forKey: "isExecuting")
didChangeValue(forKey: "isExecuting")
willChangeValue(forKey: "isFinished")
didChangeValue(forKey: "isFinished")


Doesn't work (Compiles ok, but at runtime the completion block doesn't get executed):

willChangeValue(for: \.isExecuting)
didChangeValue(for: \.isExecuting)
willChangeValue(for: \.isFinished)
didChangeValue(for: \.isFinished)

Note: I am using the following:

- macOS 10.12.5 (16F73)

- Xcode 9.0 beta (9M136h)

Questions:

1. Why isn't KeyPath working when using the String works ?

2. How to fix using KeyPath ?

Complete Code:

class AsyncOperation : Operation {

    private var _executing  = false
    private var _finished  = false

    override var isExecuting : Bool {

        get {
            return _executing
        }

        set {
            willChangeValue(for: \.isExecuting
            _executing = newValue
            didChangeValue(for: \.isExecuting)
        }
    }

    override var isFinished : Bool {

        get {
            return _finished
        }

        set {
            willChangeValue(for: \.isFinished)
            _finished = newValue
            didChangeValue(for: \.isFinished)
        }
    }


    override var isAsynchronous : Bool {
        return true
    }

    func markAsCompleted() {

        isExecuting = false
        isFinished  = true
    }
}

Replies

If this is a pure swift 4 context, you may want to ask/discuss on swift.org - othewise I'm not sure that string (vs. keypath) isn't your friend in any case.

1. Why isn't KeyPath working when using the String works ?

The problem here is the traditional Cocoa weirdness between the

finished
property and the
isFinished
getter. You can see this in action by overriding
-willChangeValueForKey:
:
override func willChangeValue(forKey key: String) {
    NSLog("key: %@", key)
    super.willChangeValue(forKey: key)
}

When you use string syntax this prints

isFinished
but when you use key path syntax it prints
finished
, that is, the name of the property.

2. How to fix using KeyPath ?

I’m not sure there is a good way to fix this. Certainly this issue was not addressed in the proposal that introduced this feature (SE-0161 Smart KeyPaths: Better Key-Value Coding for Swift). I’m going to side with KMT here: you should try asking over on swift-users.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Thank you so much for pointing out the issue, never struck me till you pointed it out.


Questions:

1. So can I just drop a mail swift-users@swift.org or should I first subscribe to the mailing list ?

2. Is it preferred to just paste only the relevant code or should I attach it as a swift file ?

A couple of other things are (may be) going on here, too:


— SE-0161 is not marked as "implemented", and the Xcode 9 release notes describe the KeyPath family of types as "partially implemented", so there's no real way of knowing whether this particular scenario is meant to be working and bug-free yet.


— The preferred way to use a string keypath in Swift is #keyPath() , not a literal string.


— The OP's code is probably not the correct way to approach this. In general, setters do not use the will/didChange… methods explicitly, and overriding a property doesn't interfere with KVO conformance. The conformance comes from run-time swizzling of the setter method name, which is done invisibly to the source code.


This assumes that the override methodss inherit the "@objc" and "dynamic" attributes from the superclass. I know they inherit "@objc", but I'm not sure about "dynamic".


The only time a setter needs to issue will/didChange… is when the class (Operation/NSOperation in this case) explicitly returns false from the relevant automaticallyNotifiesObservers… class method. (Or, obviously, when the setter wants to issue a notification for a different property.)

In general, setters do not use the will/didChange… methods explicitly …

Indeed. But in the case of the NSOperation properties in play here there is no setter. These are read-only properties. The standard way of creating an async NSOperation is to override the getter to access some internal state, at which point you do need to get involved with {will,did}Change….

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Ah, I see what you're saying. However, it seems terribly unsafe to attempt this by writing a setter (as well as overriding the getter). We don't know if NSOperation has a private "setFinished:" method. (You might know, but we don't.) In that case, accidentally overriding it in your subclass would be a bad thing.


Even if it doesn't have a private setter but you provide one as the OP did, then it's still wrong to do the will/didChange… calls in the setter, unless automaticallyNotifiesObservers… happens to have been overridden in NSOperation to be false. We don't know about that either.


Putting a setter here just seems dangerous all round. And…


>> override the getter to access some internal state, at which point you do need to get involved with {will,did}Change….


Yes, you do, but they're going to be in the state handling that affects the "finished" property, not in the property itself.

Ah, I see what you're saying. However, it seems terribly unsafe to attempt this by writing a setter (as well as overriding the getter). We don't know if NSOperation has a private "setFinished:" method. (…) In that case, accidentally overriding it in your subclass would be a bad thing.

I can see why you’d be concerned about this but that’s not how async NSOperations work. The standard pattern (which is very poorly documented, alas) is that, when you build an async NSOperation, you take over handling of the

executing
and
finished
properties entirely. While NSOperation does have internal state for these things, they only apply in the case of sync operations.

Even if it doesn't have a private setter but you provide one as the OP did, then it's still wrong to do the will/didChange… calls in the setter, unless automaticallyNotifiesObservers… happens to have been overridden in NSOperation to be false.

… or you implement that

automaticallyNotifiesObserversXxx
override yourself.

Of course the worst case scenario here is that the KVO notifications fire twice, which clients should be resilient to (-:

To be clear I’m not a big fan of doing a setter here either. In my case I track the state of my operation internally and then derive the

executing
and
finished
properties from that. For a concrete example, take a look at the QRunLoopOperation class in LinkedImageFetcher sample code. One day I’ll have time to update that to Swift, probably when I retired (-:

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"
Update:
This is a old post, now you can use Keypath in Operations in willChangeValue and didChangeValue and works exactly like String