Irbisa · Cheatsheet
Combine
Publishers, Subscribers, operators, Subjects, bridging with async/await
What is Combine, and how do
Publisher,Subscriberand operators fit together?import Combine let cancellable = [1, 2, 3, 4, 5].publisher // Publisher<Int, Never> .map { $0 * 2 } // operator .filter { $0 > 4 } // operator .sink( // subscriber…What is the difference between
PassthroughSubjectandCurrentValueSubject?What is the difference between
debounceandthrottle?What is
AnyCancellable, and how do you manage the lifetime and memory of a subscription?private var bag = Set<AnyCancellable>()private var searchCancellable: AnyCancellable? // wrong: the token is released at once, the subscription is cancelledrequest.sink { _ in }…Why does this view model never print
deinit, and how doassign(to:on:)andassign(to:)differ?final class ProfileViewModel { @Published var title = "" private var bag = Set<AnyCancellable>() init(user: AnyPublisher<User, Never>) { user .map(\.name) .assign(to: \.title, on: self) .store(in: &bag) } deinit { print("deinit") }}Find the bug: after a few trips to a post and back, every feed update is rendered and logged several times.
final class FeedViewController: UIViewController { private let viewModel = FeedViewModel() private var bag = Set<AnyCancellable>() override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) viewModel.$posts .receive(on: DispatchQueue.main) .sink { [weak self] posts in self?.render(posts) Analytics.log("feed_shown", count: posts.count) } .store(in: &bag) }}What does
eraseToAnyPublisher()do, and why do APIs returnAnyPublisher?// The concrete type would be a long nest of Publishers.* genericsfunc posts() -> AnyPublisher<[Post], Error> { URLSession.shared.dataTaskPublisher(for: postsURL) .map(\.data) .decode(type: [Post].self, decoder: JSONDecoder()) .eraseToAnyPublisher()…What is the difference between
merge,combineLatestandzip?let a = PassthroughSubject<Int, Never>()let b = PassthroughSubject<String, Never>()var bag = Set<AnyCancellable>() a.combineLatest(b).sink { print("latest", $0, $1) }.store(in: &bag)a.zip(b).sink { print("zip", $0, $1) }.store(in: &bag)…What is the difference between
flatMapandswitchToLatest, and which one does a search field need?final class SearchViewModel: ObservableObject { @Published var query = "" @Published private(set) var results: [Match] = [] init(api: SearchAPI) { $query…What does this code print, and how do you keep the stream alive after an error?
let subject = PassthroughSubject<Int, Never>()let cancellable = subject .tryMap { value -> Int in if value == 2 { throw URLError(.badURL) } return value * 10 } .replaceError(with: -1) .sink { print($0) } subject.send(1)subject.send(2)subject.send(3)What are hot and cold publishers, and which Combine publishers are which?
Find the bug: after one "Saved" toast, every new screen the user opens shows the same toast again.
@MainActor final class ToastCenter { static let shared = ToastCenter() let messages = CurrentValueSubject<String?, Never>(nil) func show(_ text: String) { messages.send(text) }} class BaseViewController: UIViewController { private var bag = Set<AnyCancellable>() override func viewDidLoad() { super.viewDidLoad() ToastCenter.shared.messages .compactMap { $0 } .sink { [weak self] in self?.presentToast($0) } .store(in: &bag) }}What is
Futurein Combine, and why is it usually wrapped inDeferred?// wrong: the request starts when the Future is createdfunc eagerUser(id: String) -> AnyPublisher<User, Error> { Future { promise in print("request fired") // runs before anyone subscribes api.loadUser(id: id) { result in promise(result) } }…What is the difference between
receive(on:)andsubscribe(on:)?// wrong: the hop to main happens before the expensive workURLSession.shared.dataTaskPublisher(for: url) .receive(on: DispatchQueue.main) .map(\.data) .decode(type: [Post].self, decoder: JSONDecoder()) // decodes on the main thread .replaceError(with: [])…What does this code print, and why does the sink see the old value of
items?final class Cart: ObservableObject { @Published var items: [String] = [] private var bag = Set<AnyCancellable>() init() { $items .sink { [weak self] newItems in print(newItems.count, self?.items.count ?? -1) } .store(in: &bag) }} let cart = Cart()cart.items = ["apple", "pear"]Which Combine publishers do Foundation and UIKit give you, and how do you get one for a
UIButton?extension UIControl { func publisher(for event: UIControl.Event) -> AnyPublisher<Void, Never> { let subject = PassthroughSubject<Void, Never>() let action = UIAction { _ in subject.send(()) } addAction(action, for: event) return subject…Why does a
Timer.publishcountdown freeze while the user scrolls, and how do you fix it?// wrong: .default mode, stops while a scroll view is trackingTimer.publish(every: 1, on: .main, in: .default).autoconnect() // right: .common includes the tracking modeTimer.publish(every: 1, on: .main, in: .common) .autoconnect()…How do Combine and async/await interoperate, and when is Combine still the better tool?
// Publisher -> async: any publisher is an AsyncSequence via .valuesTask { for await query in viewModel.$query.values { print(query) }}…A Combine pipeline emits twice and then goes quiet. How do you find where the events stop?
$query .print("1-query") .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main) .map { api.search($0).print("2-request") } .switchToLatest() .print("3-results")…One expensive publisher has three subscribers. How many times does the work run, and how do you share it?
let request = URLSession.shared.dataTaskPublisher(for: url) .map(\.data) .replaceError(with: Data()) .handleEvents(receiveSubscription: { _ in print("network hit") }) let a = request.sink { _ in }…What is backpressure in Combine, and why doesn't it protect you when a
sinkmeets a fast producer?// wrong: unlimited demand, one main-queue block per messagesocket.messages .receive(on: DispatchQueue.main) .sink { [weak self] in self?.rows.append($0) } .store(in: &bag)// right: batch off the main thread, about 10 UI updates a second…How do you write a custom Combine operator, and when do you need your own
PublisherandSubscription?// Most custom operators are compositionsextension Publisher where Output == String, Failure == Never { func settledQuery() -> AnyPublisher<String, Never> { debounce(for: .milliseconds(300), scheduler: DispatchQueue.main) .removeDuplicates().eraseToAnyPublisher() }…How do you unit-test a pipeline that uses
debouncewithout sleeping in the test?final class SearchViewModel { @Published var query = "" private(set) var results: [Match] = [] private var bag = Set<AnyCancellable>() init(api: SearchAPI, scheduler: AnySchedulerOf<DispatchQueue>) {…What breaks when a Combine-heavy module moves to the Swift 6 language mode, and how do you fix it?
@MainActorfinal class FeedViewModel { private(set) var posts: [Post] = [] private var bag = Set<AnyCancellable>() func start(_ updates: AnyPublisher<[Post], Never>) {…