Give your simulator superpowers

RocketSim: An Essential Developer Tool
as recommended by Apple

@Published risks and usage explained with code examples

@Published is one of the property wrappers in SwiftUI that allows us to trigger a view redraw whenever changes occur. You can use the wrapper combined with the ObservableObject protocol, but you can also use it within regular classes.

It’s essential to understand how the published property wrapper works since it can easily lead to unexpected behavior because it’s responding to a will set trigger. Let’s dive into what this means by starting with an explanation of how the wrapper works.

What is the @Published Property Wrapper?

You can use the @Published property wrapper just like other property wrappers by marking a property as follows:

final class ArticleViewModel {

    @Published
    var title: String = "An example title"
}

The wrapper is class constrained, meaning that you can only use it on instances of a class. An error will occur when used inside a struct:

The @Published property wrapper is class constrained and only available on properties of classes.
The @Published property wrapper is class constrained and only available on properties of classes.

The wrapped value of the @Published property wrapper represents the actual value of the property:

let viewModel = ArticleViewModel()
print(viewModel.title) // Prints: An example title

The projected value results in a publisher which you can use to observe changes:

var cancellable = viewModel.$title.sink(receiveValue: { newTitle in
    print("Title changed to \(newTitle)")
})
viewModel.title = "@Published explained"

// Prints:
// Title changed to An example title
// Title changed to @Published explained

Note that we’re using the dollar sign to access the projected value. If you’re new to this technique, I encourage you to read my article Property Wrappers in Swift explained with code examples.

The receive value closure emits both when the value changes and when we subscribe for the first time. You can use the sink operator to respond to any published property changes. For example, you could update a label to represent the initial value of the title and any future updates.

The importance of understanding the willSet trigger

Now that we know the basics of the @Published property wrapper, it’s essential to understand better when a change trigger is published. Publishing of changes occurs in the property’s willSet block, meaning that any subscribers will receive an update before the property is changed.

To better explain this behavior, we can look at the following code example:

var cancellable = viewModel.$title.sink(receiveValue: { newTitle in
    print("Title changed to: '\(newTitle)'")
    print("ViewModel title is: '\(viewModel.title)'")
})
viewModel.title = "@Published explained"

// Prints:
// Title changed to: '@Published explained'
// ViewModel title is: 'An example title'

As you can see, the received value correctly represents the new title. However, the reference of the view model property still returns the previous title.

The behavior described above can easily lead to unexpected bugs. A typical example would be to have a generic function to update a label with the new title:

override func viewDidLoad() {
    super.viewDidLoad()

    cancellable = viewModel.$title.sink(receiveValue: { [weak self] newTitle in
        // Update for title changes.
        self?.updateTitleLabel()
    })

    // Initially update the title label.
    updateTitleLabel()
}

func updateTitleLabel() {
    titleLabel.text = viewModel.title
    print("Title label text is now: '\(titleLabel.text ?? "empty")'")
}

The updateTitleLabel() exists to allow updating the title from multiple places by reusing the same code. However, since we’re calling the method from inside the sink operator, we will trigger the method during the willSet. As demonstrated before, it will represent the previous value instead of the newly published value.

The fix for the above example would always be to use the publisher as the source of truth:

override func viewDidLoad() {
    super.viewDidLoad()

    cancellable = viewModel.$title.sink(receiveValue: { [weak self] newTitle in
        self?.titleLabel.text = newTitle
        print("Title label text is now: '\(self?.titleLabel.text ?? "empty")'")
    })
}

Using the published title value, we’re constantly making sure to represent the latest published title. We also benefit from the initially published value we receive upon subscribing to the title publishers, making sure our title label initially represents the default title value.

Why willSet and not didSet?

A question you might have is why it’s using the willSet and not the didSet trigger. The reason is that SwiftUI needs to perform diffing between the old state and the new state to decide if a new rendering of the view is necessary. For this to happen we need to have a reference of the old state, which is why the willSet is used.

The love of @Published and @ObservableObject

While the above examples demonstrate the use of the @Published property wrapper inside a regular class, it’s more common to see the wrapper inside an observed object:

final class ArticleViewModel: ObservableObject {

    @Published
    var title: String = "An example title"
}

The ObservableObject is a special kind of protocol that synthesizes an objectWillChange publisher that emits when any of its contained @Published properties changes.

We can explain the above behavior using the following code example:

let viewModel = ArticleViewModel()

viewModel.objectWillChange.sink { _ in
    print("Articles view model changed!")
}

viewModel.title = "@Published explained"

// Prints:
// Articles view model changed!

SwiftUI uses the objectWillChange publisher to redraw its views in response to any change. You can learn more about this technique in my article @StateObject vs. @ObservedObject: The differences explained.

Conclusion

You can use the @Published property wrapper to observe property changes inside any class. Any new values will be published from the willSet method meaning that we won’t get the latest value when accessed directly. The ObservableObject protocol works closely together with the property wrapper and allows connecting SwiftUI views to any property changes.

If you like to improve your SwiftUI knowledge, even more, check out the SwiftUI category page. Feel free to contact me or tweet me on Twitter if you have any additional tips or feedback.

Thanks!

 

Stay updated with the latest in Swift & SwiftUI

The 2nd largest newsletter in the Apple development community with 18,327 developers.


Featured SwiftLee Jobs

Find your next Swift career step at world-class companies with impressive apps by joining the SwiftLee Talent Collective. I'll match engineers in my collective with exciting app development companies. SwiftLee Jobs