Home

Making NSProgress SwiftUI compatible
Swift
iOS

Intro

I'm sure you've run into some issues when dealing with Foundation classes that won't publish UI updates when their properties changed in your SwiftUI app and possibly got mad. No worries, let's make that happen.

Observation method

We need a mechanism to listen to updates of the properties of the class.

In classes we create in our app, we usually achieve property observation by marking properties with @Published and subclassing ObservableObject.

However for Foundation classes we can't mark the properties we want as @Published since Foundation owns these classes and not us, the developer.

For classes that are not adjusted to be used with SwiftUI, we will use the OG method of key value observation, also known as the NSKeyValueObservation to bridge the gap.

You can find some material on Using Key-Value Observing in Swift in this tutorial from Apple.

Creating the wrapper class

Refer to the comments for each property to better understand what it's doing.

class ObservableProgress : NSObject, ObservableObject {

    /// Percentage representation of the progress.
    ///
    /// This is the public facing property that our SwiftUI views will use to display.
    @Published public var percent: String

    /// The NSProgress object that is observed.
    public var progress: Progress? {
        didSet {
            // When a new value for progress is set, invalidate the existing `observer`.
            observer?.invalidate()
            // Check if progress is set to a non nil value.
            if let progress {
                // Specify which property of the `progress` object we want to track.
                observer = progress.observe(\Progress.fractionCompleted, options: .new) {
                    // This block will be invoked each time `progress.fractionCompleted` is changed.
                    progress, change in
                    // See Double extension below to understand what `asPercent` does.
                    let percent = progress.fractionCompleted.asPercent()
                    // Assign new value to our instance variable `percent` that will be published.
                    self.percent = percent
                }
            }
        }
    }

    // This is the key-value observer we use to track updates.
    private var observer: NSKeyValueObservation?

    // MARK: - Init

    override init() {
        self.percent = Double(0).asPercent()
        super.init()
    }

    // Make sure we invalidate our observer if this progress gets deallocated to prevent memory leaks.
    deinit {
        observer?.invalidate()
    }
}

extension Double {
    func asPercent() -> String { "\(Int((self * 100.0).rounded()))%" }
}

Using in views

Simply convert the NSProgress object you acquire to ObservableProgress in your app and voila!

struct MyView: View {

    @ObservedObject private var observableProgress: ObservableProgress = ObservableProgress()

    init(progress: Progress) {
        observableProgress.progress = progress
    }

    var body: some View {
        Text(observableProgress.percent)
    }
}

Creating a ObservableProgress from a NSProgress

You can optionally create an extension on NSProgress (or Progress in Swift) to easily create our wrapper class ObservableProgress like this.

extension Progress {

    func observable() -> ObservableProgress {
        let observableProgress = ObservableProgress()
        // Reason we set progress here is because we want `didSet` handler of the property to be called.
        observableProgress.progress = self
        return observableProgress
    }

}