Observing Combine publishers in SwiftUI views
Discover page available: SwiftUISwiftUI offers multiple ways to connect a given view to the underlying state that it depends on, for example using property wrappers like @State
and @ObservedObject
. While using those property wrappers is certainly the preferred approach in the vast majority of cases, another option that can be good to keep in mind is that we can also observe Combine publishers directly within our SwiftUI views as well.
As an example, let’s say that we’re working on a view that plays a repeated animation for as long as the app remains in the foreground, and that we’d like to pause that animation when that’s no longer the case.
To do that, we could use NotificationCenter
, which (since iOS 13 + macOS Catalina) ships with a Combine-powered API that lets us easily create a Publisher
for any Notification
that we’d like to observe. Then, we could use the onReceive
modifier to connect that publisher to our SwiftUI view’s body
— like this:
struct AnimationView: View {
@State private var isAnimating = true
var body: some View {
VStack {
...
}
.onReceive(NotificationCenter.default.publisher(
for: UIApplication.didEnterBackgroundNotification
)) { _ in
// The app moved to the background
isAnimating = false
}
}
}
Note how we don’t need to make our view store an AnyCancellable
, or otherwise manage the connection to our publisher in any way — SwiftUI takes care of all of that for us!
However, the code required to observe a NotificationCenter
-provided publisher is quite verbose, so if the above is a pattern that we’re looking to follow in multiple places throughout our code base, then we could also implement a few convenience APIs that’ll let us do so more easily:
extension View {
func onNotification(
_ notificationName: Notification.Name,
perform action: @escaping () -> Void
) -> some View {
onReceive(NotificationCenter.default.publisher(
for: notificationName
)) { _ in
action()
}
}
func onAppEnteredBackground(
perform action: @escaping () -> Void
) -> some View {
onNotification(
UIApplication.didEnterBackgroundNotification,
perform: action
)
}
}
With the above in place, we’ll now be able to observe when our app moved to the background simply by doing this:
struct AnimationView: View {
@State private var isAnimating = true
var body: some View {
VStack {
...
}
.onAppEnteredBackground {
isAnimating = false
}
}
}
Finally, using the onReceive
modifier can also be a great way to observe our own, custom publishers as well — which in turn can act as a more lightweight alternative to something like an ObservedObject
when setting up a reference to a mutable piece of state.
For example, here an ItemList
is using a publisher to observe an external array of Item
values, which it then stores in a local @State
property to connect it to its body
:
struct ItemList: View {
var publisher: AnyPublisher<[Item], Never>
@State private var items = [Item]()
var body: some View {
List(items) { item in
ItemRow(item: item)
}
.onReceive(publisher) {
items = $0
}
}
}
Of course, that doesn’t mean that we should replace all uses of ObservedObject
with the above pattern, but it’s a neat technique for when we just want to observe a single event, rather than a complex object.