Best Practice to subscribe to a Publisher in SwiftUI

Hi,


I am experimenting with SwiftUI and Combine after watching https://developer.apple.com/videos/play/wwdc2019/226/ ( Data Flow Through SwiftUI ).


So I created a Publisher to load some data via network call, something like this:


var countriesPublisher: AnyPublisher<[Country], Never> = {
    let urlSession = URLSession.shared
    let url = URL(string: "https://www.ralfebert.de/examples/countries.json")!

    return urlSession
        .dataTaskPublisher(for: url)
        .map { $0.data }
        .decode(type: [Country].self, decoder: JSONDecoder())
        .assertNoFailure() // todo: no error handling right now
        .receive(on: RunLoop.main)
        .eraseToAnyPublisher()

}()


And I want to use this in a SwiftUI view. Now the WWDC talk recommended to use @State to hold the data, subscribe to it via onReceive and copy it to the internal state. This works, f.e.:


struct CountriesListView: View {

    @State var countries = [Country]()

    var body: some View {

        List(countries) { country in
            Text(country.name)
        }
        .onReceive(countriesPublisher) { countries in
            self.countries = countries
        }

    }
}


(the example project is available here: https://github.com/ralfebert/Countries/archive/ed73ddb5423548d54375f62904d8e81f7f7841d2.zip )


Ok, this works and it's pretty cool already. But:


  • I liked the 'single source of truth' principle mentioned in the talk, but this violates it by copying the state. I consider the output of the Publisher to be this source of truth.
  • For the PreviewProvider I'd like to be able to pass in static data, and in that case, I don't want the onReceive to be called.
  • It was recommended to keep @State private which made sense to me, but if I want to move the onReceive() call out / one level up, this cannot be private anymore.


Are there any best practices already how to connect a SwiftUI view to such a Publisher?


Greetings,


Ralf

Accepted Reply

It seems:


  • subscribing via View#onReceive + copying the new value to a @State to trigger the refresh
  • or subscribing to the Publisher in an ObservableObject + copying the new value to a @Published value to trigger the refresh


seems to be the way to go.

Replies

Take a look a the latest release notes for iOS, iPadOS for Beta 5. Scroll down to SwiftUI. BindableObject is replaced by ObservableObject and @ObjectBinding is replaced by @ObservedObject. There is a bit of code there that should help.

It seems:


  • subscribing via View#onReceive + copying the new value to a @State to trigger the refresh
  • or subscribing to the Publisher in an ObservableObject + copying the new value to a @Published value to trigger the refresh


seems to be the way to go.

I am tending to use the ObservableObject approach nowaways because it allows elegant setup in the PreviewProvider.


I extracted a full example here:

Using URLSession to load JSON data for SwiftUI Views

https://stackoverflow.com/a/61858358/128083