Async Task with Combine in SwiftUI

Async Task with Combine in SwiftUI

A tutorial on making a post list with Combine Framework

·

4 min read

Combine is a powerful framework introduced by Apple that provides a declarative and reactive approach to handling asynchronous events and data streams. In SwiftUI, Combine is used extensively for handling data flow and reactive programming. In this blog post, we'll explore the basics of Combine in SwiftUI and how it can be used to build flexible and reactive user interfaces.

The code in this post is available here.

What is Combine

Combine is based on the concept of publishers and subscribers. Publishers are objects that emit a stream of values or events over time, while subscribers are objects that receive and react to these values or events. Publishers and subscribers can be combined using operators to create powerful and flexible data flows.

Let’s create an example app and take a look at how we can use Combine in SwiftUI to handle asynchronous data loading. This app fetches posts in lorem ipsum texts from https://jsonplaceholder.typicode.com/posts and will display the title, body and id of each of these posts in a List view.

Step 1: Define the Post Model

As mentioned above, the content of a post includes a number as its identifier, a string as its title and a string as its body. So let’s create a Post structure containing these as the properties. Note that the struct needs to conform to Decodable (or Codable which is the combination of Encodable and Decodable), in order to decode from JSON.

struct Post: Codable {
    let id: Int
    let body: String
    let title: String
}

Step 2: Define the View Model with Combine

class ViewModel: ObservableObject {
    @Published var data: [Post] = []
    private var cancellables = Set<AnyCancellable>()

    func fetchData() {
        URLSession.shared.dataTaskPublisher(for: URL(string: "https://jsonplaceholder.typicode.com/posts")!)
            .map { $0.data }
            .decode(type: [Post].self, decoder: JSONDecoder())
            .sink { completion in
                switch completion {
                case .finished:
                    print("Data loading finished")
                case .failure(let error):
                    print("Data loading failed with error: \(error.localizedDescription)")
                }
            } receiveValue: { [weak self] data in
                self?.data = data
            }
            .store(in: &cancellables)
    }
}

First of this step, to use the Combine framework, we need to import Combine.

Then we define a ViewModel class that conforms to the ObservableObject protocol. We also define a data property to hold the posts fetched that is marked with the @Published property wrapper, indicating that it can be observed for changes.

In the fetchData method, we use the URLSession.shared.dataTaskPublisher method to create a publisher that emits a stream of data from a remote server.

We then use the map operator to transform the data into an array of strings and the decode operator to decode the JSON data using a JSONDecoder.

Finally, we use the sink method to subscribe to the publisher and perform some action whenever a new value is emitted. We update the data property with the received data, and we also handle any errors that may occur during the data loading process. We use the store method to store the cancellable object returned by sink in our cancellables property.

cancellables Explained

In Combine, a Cancellable is a protocol that defines a method to cancel the activity of a publisher, subscriber, or any other type that can produce or receive values. A cancellable object can be used to stop or prevent further processing of a data flow, which can be useful in cases such as network requests or long-running tasks.

When you subscribe to a publisher using the sink method, a cancellable object is returned. This object can be used to cancel the subscription, which will prevent the publisher from emitting any further values to the subscriber. For example, if you have a long-running task that is no longer needed or if the user navigates away from a view, you can use the cancellable object to cancel the subscription and stop the task.

In addition to the sink method, there are other methods in Combine that return cancellable objects, such as cancel, store. These methods allow you to combine and transform publishers while ensuring that you can cancel the subscription at any time.

Step 3: Create a Content View to display the posts

struct ContentView: View {
    @StateObject var viewModel = ViewModel()

    var body: some View {
        List(viewModel.data, id: \.id) { item in
            HStack {
                VStack(alignment: .leading) {
                    Text(item.title)
                        .bold()
                        .lineLimit(1)
                        .padding(.vertical)
                    Text(item.body)
                        .font(.footnote)
                        .lineLimit(2)
                }
                Spacer()
                Text("\(item.id)")
                    .bold()
                    .foregroundColor(.secondary)
            }
            .padding()
        }
        .onAppear {
            viewModel.fetchData()
        }
    }
}

In this example, we define a ContentView view that uses the @StateObject property wrapper to create a reactive viewModel object. We create a List view that displays the data property of the viewModel, and we add an onAppear modifier that triggers the fetchData method when the view appears.

Conclusion

In conclusion, Combine is an essential framework for building reactive and flexible user interfaces in SwiftUI. By using publishers, subscribers, and operators, we can create powerful data flows that allow us to handle asynchronous events and data streams in a declarative and reactive way.

That’s all of today’s post. I hope it helps and let me know if it is by leaving a comment. Don’t forget to subscribe to my newsletter if you’d like to receive posts like this via email.

I’ll see you in the next post!