Irbisa · Шпаргалка
Combine
Publisher, Subscriber, операторы, Subjects, мост с async/await
Что такое Combine и как в нём связаны
Publisher,Subscriberи операторы?import Combine let cancellable = [1, 2, 3, 4, 5].publisher // Publisher<Int, Never> .map { $0 * 2 } // operator .filter { $0 > 4 } // operator .sink( // subscriber…Чем
PassthroughSubjectотличается отCurrentValueSubject?Чем
debounceотличается отthrottle?Что такое
AnyCancellableи как управлять временем жизни и памятью подписки?private var bag = Set<AnyCancellable>()private var searchCancellable: AnyCancellable? // wrong: the token is released at once, the subscription is cancelledrequest.sink { _ in }…Почему эта view model никогда не печатает
deinitи чемassign(to:on:)отличается отassign(to:)?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") }}Найдите баг: после нескольких переходов в пост и обратно каждое обновление ленты рендерится и логируется по несколько раз.
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) }}Что делает
eraseToAnyPublisher()и почему API возвращаютAnyPublisher?// 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()…Чем отличаются
merge,combineLatestиzip?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)…Чем
flatMapотличается отswitchToLatestи что из них нужно полю поиска?final class SearchViewModel: ObservableObject { @Published var query = "" @Published private(set) var results: [Match] = [] init(api: SearchAPI) { $query…Что выведет этот код и как сохранить поток живым после ошибки?
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)Что такое горячие и холодные паблишеры и какие паблишеры Combine к каким относятся?
Найдите баг: после одного тоста «Сохранено» каждый новый экран, который открывает пользователь, снова показывает тот же тост.
@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) }}Что такое
Futureв Combine и почему его обычно оборачивают вDeferred?// 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) } }…Чем
receive(on:)отличается отsubscribe(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: [])…Что выведет этот код и почему sink видит старое значение
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"]Какие паблишеры Combine дают Foundation и UIKit и как получить паблишер для
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…Почему обратный отсчёт на
Timer.publishзамирает, пока пользователь скроллит, и как это исправить?// 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()…Как Combine и async/await работают вместе и когда Combine всё ещё лучший инструмент?
// Publisher -> async: any publisher is an AsyncSequence via .valuesTask { for await query in viewModel.$query.values { print(query) }}…Пайплайн Combine выдаёт два значения и замолкает. Как найти место, где останавливаются события?
$query .print("1-query") .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main) .map { api.search($0).print("2-request") } .switchToLatest() .print("3-results")…У одного дорогого паблишера три подписчика. Сколько раз выполнится работа и как её разделить?
let request = URLSession.shared.dataTaskPublisher(for: url) .map(\.data) .replaceError(with: Data()) .handleEvents(receiveSubscription: { _ in print("network hit") }) let a = request.sink { _ in }…Что такое backpressure в Combine и почему он не спасает, когда
sinkвстречает быстрый источник?// 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…Как написать собственный оператор Combine и когда нужны свои
PublisherиSubscription?// 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() }…Как протестировать пайплайн с
debounce, не засыпая в тесте?final class SearchViewModel { @Published var query = "" private(set) var results: [Match] = [] private var bag = Set<AnyCancellable>() init(api: SearchAPI, scheduler: AnySchedulerOf<DispatchQueue>) {…Что ломается, когда модуль, плотно использующий Combine, переходит на языковой режим Swift 6, и как это чинить?
@MainActorfinal class FeedViewModel { private(set) var posts: [Post] = [] private var bag = Set<AnyCancellable>() func start(_ updates: AnyPublisher<[Post], Never>) {…