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