NEW: Start my new Ultimate Portfolio App course with a free Hacking with Swift+ trial! >>

Manually publishing ObservableObject changes

Paul Hudson    @twostraws   

Classes that conform to the ObservableObject protocol can use SwiftUI’s @Published property wrapper to automatically announce changes to properties, so that any views using the object get their body property reinvoked and stay in sync with their data. That works really well a lot of the time, but sometimes you want a little more control and SwiftUI’s solution is called objectWillChange.

Every class that conforms to ObservableObject automatically gains a property called objectWillChange. This is a publisher, which means it does the same job as the @Published property wrapper: it notifies any views that are observing that object that something important has changed. As its name implies, this publisher should be triggered immediately before we make our change, which allows SwiftUI to examine the state of our UI and prepare for animation changes.

To demonstrate this we’re going to build an ObservableObject class that updates itself 10 times. You already met DispatchQueue.main.async() as a way of pushing work to the main thread, but here we’re going to use a similar method called DispatchQueue.main.asyncAfter(). This lets us specify when the attached closure should be run, which means we can say “do this work after 1 second” rather than “do this work now.”

In this test case, we’re going to use asyncAfter() inside a loop from 1 through 10, so we increment an integer 10 values. That integer will be wrapped using @Published so change announcements are sent out to any views that are watching it.

Add this class somewhere in your code:

class DelayedUpdater: ObservableObject {
    @Published var value = 0

    init() {
        for i in 1...10 {
            DispatchQueue.main.asyncAfter(deadline: .now() + Double(i)) {
                self.value += 1
            }
        }
    }
}

To use that, we just need to add an @ObservedObject property in ContentView, then show the value in our body, like this:

struct ContentView: View {
    @ObservedObject var updater = DelayedUpdater()

    var body: some View {
        Text("Value is: \(updater.value)")
    }
}

When you run that code you’ll see the value counts upwards until it reaches 10, which is exactly what you’d expect.

Now, if you remove the @Published property wrapper you’ll see the UI no longer changes. Behind the scenes all the asyncAfter() work is still happening, but it doesn’t cause the UI to refresh any more because no change notifications are being sent out.

We can fix this by sending the change notifications manually using the objectWillChange property I mentioned earlier. This lets us send the change notification whenever we want, rather than relying on @Published to do it automatically.

Try changing the value property to this:

var value = 0 {
    willSet {
        objectWillChange.send()
    }
}

Now you’ll get the old behavior back again – the UI will count to 10 as before. Except this time we have the opportunity to add extra functionality inside that willSet observer. Perhaps you want to log something, perhaps you want to call another method, or perhaps you want to clamp the integer inside value so it never goes outside of a range – it’s all under our control now.

Subscribe to Hacking with Swift+

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
Buy Pro Swift Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift (Vapor Edition) Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Server-Side Swift (Kitura Edition) Buy Beyond Code

Was this page useful? Let us know!

Average rating: 4.6/5

Link copied to your pasteboard.