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
}
}