Articles, podcasts and news about Swift development, by John Sundell.

Observing Combine publishers in SwiftUI views

Published on 04 Dec 2020
Discover page available: SwiftUI

SwiftUI 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.