Mastering Combine Framework in Swift

The Combine framework, introduced by Apple in iOS 13, is a powerful tool for handling asynchronous events in Swift. It provides a declarative Swift API for processing values over time, allowing you to write clean, readable, and maintainable code. In this blog post, we'll explore the basics of Combine and how you can use it to manage asynchronous tasks in your Swift applications.

What is Combine?

Combine is a reactive programming framework that allows you to work with asynchronous data streams. It provides a unified way to handle events, notifications, and data streams, making it easier to manage complex asynchronous code.

Key Concepts

Before diving into code, let's understand some key concepts in Combine:

  • Publisher: A publisher emits a sequence of values over time to one or more subscribers.
  • Subscriber: A subscriber receives values from a publisher and acts upon them.
  • Operators: Operators are methods that transform or combine publishers.

Getting Started with Combine

Let's start with a simple example to understand how Combine works. We'll create a publisher that emits a sequence of numbers and a subscriber that prints those numbers.

Step 1: Import Combine

First, import the Combine framework:

import Combine

Step 2: Create a Publisher

Next, create a publisher that emits a sequence of numbers:

let numbers = [1, 2, 3, 4, 5]
let publisher = numbers.publisher

Step 3: Create a Subscriber

Create a subscriber that prints the received values:

let subscriber = Subscribers.Sink<Int, Never>(
    receiveCompletion: { completion in
        print("Completed with: \(completion)")
    },
    receiveValue: { value in
        print("Received value: \(value)")
    }
)

Step 4: Connect Publisher to Subscriber

Finally, connect the publisher to the subscriber:

publisher.subscribe(subscriber)

When you run this code, you'll see the following output:

Received value: 1
Received value: 2
Received value: 3
Received value: 4
Received value: 5
Completed with: finished

Using Operators

Combine provides a rich set of operators to transform and combine publishers. Let's look at some common operators.

Map Operator

The map operator transforms the values emitted by a publisher. For example, you can use map to square each number in the sequence:

let squaredPublisher = publisher.map { $0 * $0 }
squaredPublisher.subscribe(subscriber)

Output:

Received value: 1
Received value: 4
Received value: 9
Received value: 16
Received value: 25
Completed with: finished

Filter Operator

The filter operator allows you to filter values emitted by a publisher. For example, you can use filter to only emit even numbers:

let evenPublisher = publisher.filter { $0 % 2 == 0 }
evenPublisher.subscribe(subscriber)

Output:

Received value: 2
Received value: 4
Completed with: finished

Handling Errors

Combine also provides a way to handle errors in the data stream. Let's create a publisher that can fail and handle the error using the catch operator:

enum MyError: Error {
    case somethingWentWrong
}

let failingPublisher = Fail<Int, MyError>(error: .somethingWentWrong)

let handledPublisher = failingPublisher.catch { error in
    Just(0) // Return a default value in case of an error
}

handledPublisher.subscribe(subscriber)

Output:

Received value: 0
Completed with: finished

Real-World Example

Okay, now that we've covered the basics, let's see how Combine can be used in a real-world scenario. Let's create a real-world example using Combine to fetch and display data from a remote API. We'll build a simple SwiftUI app that fetches a list of posts from a JSONPlaceholder API and displays them in a list.

Step-by-Step Plan

  1. Create a SwiftUI View: Set up a basic SwiftUI view.
  2. Define a Model: Create a model to represent the data.
  3. Create a Network Service: Use Combine to fetch data from the API.
  4. Bind Data to the View: Display the fetched data in the SwiftUI view.

Step 1: Create a SwiftUI View

First, set up a basic SwiftUI view:

import SwiftUI
import Combine

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

    var body: some View {
        NavigationView {
            List(viewModel.posts) { post in
                VStack(alignment: .leading) {
                    Text(post.title)
                        .font(.headline)
                    Text(post.body)
                        .font(.subheadline)
                }
            }
            .navigationTitle("Posts")
            .onAppear {
                viewModel.fetchPosts()
            }
        }
    }
}

Step 2: Define a Model

Create a model to represent the data:

import Foundation

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

Step 3: Create a Network Service

Use Combine to fetch data from the API:

import Foundation
import Combine

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

    func fetchPosts() {
        guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else {
            return
        }

        URLSession.shared.dataTaskPublisher(for: url)
            .map { $0.data }
            .decode(type: [Post].self, decoder: JSONDecoder())
            .receive(on: DispatchQueue.main)
            .sink(receiveCompletion: { completion in
                switch completion {
                case .finished:
                    break
                case .failure(let error):
                    print("Error fetching posts: \(error)")
                }
            }, receiveValue: { [weak self] posts in
                self?.posts = posts
            })
            .store(in: &cancellables)
    }
}

Step 4: Bind Data to the View

The ContentView already binds the fetched data to the view using the @StateObject property wrapper. When fetchPosts is called, the data is fetched and the view is updated automatically.

Complete Code

Here is the complete code for the example:

import SwiftUI
import Combine

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

    var body: some View {
        NavigationView {
            List(viewModel.posts) { post in
                VStack(alignment: .leading) {
                    Text(post.title)
                        .font(.headline)
                    Text(post.body)
                        .font(.subheadline)
                }
            }
            .navigationTitle("Posts")
            .onAppear {
                viewModel.fetchPosts()
            }
        }
    }
}

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

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

    func fetchPosts() {
        guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else {
            return
        }

        URLSession.shared.dataTaskPublisher(for: url)
            .map { $0.data }
            .decode(type: [Post].self, decoder: JSONDecoder())
            .receive(on: DispatchQueue.main)
            .sink(receiveCompletion: { completion in
                switch completion {
                case .finished:
                    break
                case .failure(let error):
                    print("Error fetching posts: \(error)")
                }
            }, receiveValue: { [weak self] posts in
                self?.posts = posts
            })
            .store(in: &cancellables)
    }
}

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

This example demonstrates how to use Combine to fetch data from a remote API and display it in a SwiftUI view. The PostsViewModel class handles the network request and updates the posts property, which is then displayed in the ContentView.

Conclusion

The Combine framework is a powerful tool for managing asynchronous events in Swift. By understanding the key concepts and using operators, you can write clean and maintainable code to handle complex asynchronous tasks. This blog post covered the basics of Combine, but there's much more to explore. Happy coding!

For more information on Combine, check out the official documentation.

Our mission

Effect UI for
your next project

We are a team of talented designers making iOS components to help developers build outstanding apps faster with less effort and best design.