NSProgress with KVO-based Finish Callbacks

Here’s NSProgress subclass that allows block-based callbacks for finishing the progress in a new callback called finishHandler that should work the same as the existing handlers.

By default NSProgress (or with Swift just Progress) comes with handlers for resumeing, pausing, and cancellation events, but not for finishing. I don’t know why.

This subclass uses Key–Value-Observations on the isFinished property (that’s actually "finished" by its Objective-C property name).

Note that you need to add a convenience initializer init(totalUnitCount:) yourself in both subclass variants because the Progress.init(totalUnitCount:) is not inheritable. Sad.

RxSwift Variant

This one is pretty short because you can model the KVO and its “dispatch only once please” condition very succinct:

class FinishObservableProgress: Progress {
    private var disposeBag = DisposeBag()

    var finishHandler: (() -> Void)? {
        didSet {
            disposeBag = DisposeBag()

            guard let finishHandler = finishHandler else { return }

            self.rx.observeWeakly(Bool.self, "finished", options: [.new])
                .filter { $0 == true }
                .distinctUntilChanged()
                .map { _ in () }
                .subscribe(onNext: finishHandler)
                .disposed(by: disposeBag)
        }
    }

    convenience init(totalUnitCount: Int64) {
        self.init(parent: nil, userInfo: nil)
        self.totalUnitCount = totalUnitCount
    }
}

Regular KVO Variant

If you don’t have RxSwift handy, regular KVO will do the trick just as well. I’ll introduce a private helper class to actually handle the observation so the progress doesn’t have to self-observe, which I always find a bit odd.

class FinishObservableProgress: Progress {
    private class Observer: NSObject {
        let callback: (() -> Void)

        init(callback: @escaping (() -> Void)) {
            self.callback = callback
        }

        /// Prevents multiple completions.
        private var didFinish = false

        override func observeValue(
                forKeyPath keyPath: String?,
                of object: Any?,
                change: [NSKeyValueChangeKey : Any]?,
                context: UnsafeMutableRawPointer?) {
            guard didFinish == false else { return }
            guard keyPath == "finished",
                let value = change?[.newKey] as? Bool,
                value == true
                else { return }
            didFinish = true
            callback()
        }
    }

    private var observer: Observer?

    deinit {
        if let observer = observer {
            self.removeObserver(observer, forKeyPath: "isFinished")
        }
    }

    var finishHandler: (() -> Void)? {
        get {
            return observer?.callback
        }
        set {
            self.observer = {
                guard let newValue = newValue else { return nil }

                let newObserver = Observer(callback: newValue)
                self.addObserver(newObserver,
                                 forKeyPath: "finished",
                                 options: [.new],
                                 context: nil)
                return newObserver
            }()
        }
    }

    convenience init(totalUnitCount: Int64) {
        self.init(parent: nil, userInfo: nil)
        self.totalUnitCount = totalUnitCount
    }
}

Hire me for freelance macOS/iOS work and consulting.

Buy my apps.

new posts via email.