【Xcode/Swift】Combine Cheat Sheet

If you’re working with Apple’s Swift programming language and Xcode, chances are that you’ll eventually come across Combine. Combine is a powerful framework introduced in iOS 13 and later, which allows you to handle asynchronous and event-driven programming using reactive programming principles. With Combine, you can create complex data flows and handle events easily, making it an essential tool for any Swift developer. In this cheat sheet, we’ll cover some of the most commonly used operators and concepts in Combine.

Publishers

In Combine, Publishers are responsible for emitting values or events. They can be thought of as a data source or a producer. Here are some commonly used publishers:

  • Just: Creates a publisher that emits a single value and then finishes.
  • Future: Creates a publisher that will eventually produce a single value or error.
  • Deferred: Creates a publisher that defers the creation of its underlying publisher until a subscriber requests values.
  • URLSession.DataTaskPublisher: A publisher that sends a URL request and emits a data object when the request completes.
// Just publisher
let justPublisher = Just("Hello, world!")

// Future publisher
let futurePublisher = Future<String, Error> { promise in
    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
        promise(Result.success("Future Publisher"))
    }
}

// Deferred publisher
let deferredPublisher = Deferred {
    futurePublisher
}

// URLSession.DataTaskPublisher
let url = URL(string: "https://jsonplaceholder.typicode.com/todos/1")!
let urlRequest = URLRequest(url: url)
let urlSessionPublisher = URLSession.shared.dataTaskPublisher(for: urlRequest)
    .map(\.data)
    .decode(type: Todo.self, decoder: JSONDecoder())

Subscribers

Subscribers are the consumers of Publishers. They receive the emitted values or events and can perform actions with them. Here are some commonly used subscribers:

  • Sink: A subscriber that receives a stream of elements and performs an action for each one.
  • Assign: A subscriber that assigns each received element to a given property of an object.
  • AnySubscriber: A type-erasing subscriber that can receive values of any type.
// Sink subscriber
let sinkSubscriber = urlSessionPublisher.sink { completion in
    switch completion {
    case .finished:
        print("Finished")
    case .failure(let error):
        print("Error: \(error.localizedDescription)")
    }
} receiveValue: { todo in
    print(todo)
}

// Assign subscriber
class ViewModel {
    @Published var value: String = ""
}

let viewModel = ViewModel()
let assignSubscriber = justPublisher.assign(to: &$viewModel.value)

// AnySubscriber
let anySubscriber = AnySubscriber(urlSessionPublisher)

Operators

Combine provides a variety of operators to transform and combine publishers. Here are some commonly used operators:

  • map: Transforms the elements emitted by a publisher by applying a given closure.
  • filter: Only emits elements from a publisher that satisfy a given predicate.
  • flatMap: Transforms each element emitted by a publisher into a new publisher and then flattens the resulting publishers into a single stream.
  • merge: Combines multiple publishers into a single publisher that emits all their elements.
  • zip: Combines the elements of multiple publishers into tuples.
// Map operator
let mapSubscriber = justPublisher
    .map { $0.count }
    .sink { value in
        print("The count of 'Hello, world!' is \(value)")
    }

// Filter operator
let filterSubscriber = urlSessionPublisher
    .filter { $0.completed }
    .sink { todo in
        print(todo)
    }

// FlatMap operator
let flatMapSubscriber = urlSessionPublisher
    .flatMap { todo -> AnyPublisher<User, Error> in
        let userId = todo.userId
        let url = URL(string: "https://jsonplaceholder.typicode.com/users/\(userId)")!
        let urlRequest = URLRequest(url: url)
        return URLSession.shared.dataTaskPublisher(for: urlRequest)
            .map(\.data)
            .decode(type: User.self, decoder: JSONDecoder())
            .eraseToAnyPublisher()
    }
    .sink { user in
        print(user)
    }

// Merge operator
let publisher1 = Just("Hello,")
let publisher2 = Just(" world!")
let mergeSubscriber = Publishers.Merge(publisher1, publisher2)
    .sink { value in
        print(value)
    }

// Zip operator
let zipPublisher1 = Just("Hello,")
let zipPublisher2 = Just(" world!")
let zipSubscriber = zipPublisher1.zip(zipPublisher2)
    .sink { value in
        print("\(value.0)\(value.1)")
    }

Subjects

A subject is a publisher that allows outside sources to send values. It can be thought of as both a Publisher and a Subscriber. Here are some commonly used subjects:

  • PassthroughSubject: A subject that allows any number of subscribers and simply passes through (or relays) any values it receives.
  • CurrentValueSubject: A subject that has an initial value and emits its current value whenever a new subscriber is added.
// PassthroughSubject
let passthroughSubject = PassthroughSubject<String, Never>()
let passthroughSubscriber1 = passthroughSubject.sink { value in
    print("Subscriber 1 received: \(value)")
}
let passthroughSubscriber2 = passthroughSubject.sink { value in
    print("Subscriber 2 received: \(value)")
}
passthroughSubject.send("Hello, world!")

// CurrentValueSubject
let currentValueSubject = CurrentValueSubject<Int, Never>(0)
let currentValueSubscriber = currentValueSubject.sink { value in
    print("Current value: \(value)")
}
currentValueSubject.send(1)
currentValueSubject.send(2)

Error handling

In Combine, errors are represented by the Error type. You can handle errors using the following operators:

  • catch: Catches and handles any error emitted by a publisher by replacing it with another publisher or a default value.
  • retry: Resubscribes to a publisher when it fails, up to a given number of attempts.
// Catch operator
let catchSubscriber = urlSessionPublisher
    .catch { error -> Just<Todo> in
        print("Error occurred: \(error.localizedDescription)")
        return Just(Todo(id: 0, userId: 0, title: "", completed: false))
    }
    .sink { todo in
        print(todo)
    }

Conclusion

Combine is a powerful framework that allows you to handle asynchronous and event-driven programming using reactive programming principles. This cheat sheet covers some of the most commonly used operators and concepts in Combine. However, there are many more operators and features available, so be sure to check out the official documentation for more information. With Combine, you can create complex data flows and handle events easily, making it an essential tool for any Swift developer.