Использование комбинирования для обработки асинхронных потоков данных в IOS
Понимание сочетания и его роли в современной разработке iOS
По мере того, как приложения iOS становятся все более сложными, управление асинхронными потоками данных становится центральной проблемой. Независимо от того, обрабатываете ли вы сетевые ответы, обрабатываете пользовательский ввод или координируете обновления через несколько компонентов пользовательского интерфейса, необходимость в надежном, декларативном подходе к асинхронному программированию никогда не была больше. Рамочная структура Apple Combine, представленная с iOS 13 и полностью интегрированная в экосистему Apple, предлагает унифицированную модель реактивного программирования, которая упрощает эти задачи. Моделируя потоки данных в качестве издателей и подписчиков, Combine позволяет разработчикам писать краткий, композитный и тестируемый код, который реагирует на события, когда они происходят с течением времени.
В этой статье представлено всестороннее исследование Combine, от его основных абстракций и операторов до практических моделей интеграции. Вы узнаете, как настроить издателей и подписчиков, операторов цепей для преобразования асинхронных данных, управления ресурсами и памятью с помощью Cancellable, изящно обрабатывать ошибки и интегрировать Combine с SwiftUI и UIKit. К концу вы будете оснащены для рефакторинга сложных цепочек обратного вызова в более чистые реактивные трубопроводы и создавать более устойчивые, отзывчивые приложения.
Что такое комбинирование? Декларативная основа для асинхронных событий
Combine — это первоклассная платформа Apple, которая предоставляет декларативный API Swift для обработки значений с течением времени. В основе своей Combine позволяет определить трубопровод, по которому проходят асинхронные события, и применять преобразования, фильтры или побочные эффекты на каждом этапе. Этот подход контрастирует с традиционными шаблонами, такими как делегирование, наблюдатели центра уведомлений или обработчики завершения, которые часто рассеивают логику по нескольким методам и затрудняют рассуждение о коде.
Структура построена вокруг трех основных абстракций: Издатели , которые излучают последовательность значений с течением времени; Абоненты , которые получают и реагируют на эти значения; и Операторы , которые сидят между издателями и подписчиками, чтобы преобразовать, объединить или заглушить поток.Вместе эти компоненты позволяют описывать сложные асинхронные потоки линейным, композитным способом.
Apple разработала Combine для бесшовной интеграции с системой типа Swift, обеспечивая безопасность компиляции времени. Это означает, что многие ошибки, которые в противном случае появились бы во время выполнения, улавливаются во время разработки. Combine также использует семантику ценности Swift и управление памятью, что делает его естественным для современных проектов iOS.
Для получения дополнительной информации обратитесь к Комбинированная документация Apple для обзора доступных типов и операторов.
Основные концепции: издатели, подписчики и операторы
Чтобы эффективно использовать Combine, вам нужно четкое понимание трех основных строительных блоков. Каждый из них играет определенную роль в конвейере данных, и понимание того, как они взаимодействуют, имеет важное значение для разработки эффективного реактивного кода.
Издатели
Публикат — это протокол, который определяет тип, способный испускать последовательность значений с течением времени., тип передаваемых им значений и , тип ошибки, которую он может выбросить (часто , если он не может выйти из строя).Публикатчики могут представлять множество источников асинхронных данных, включая сетевые запросы, события таймера, уведомления, наблюдения за ключевым значением или действия пользовательского интерфейса.
Некоторые общие встроенные издатели включают:
- — для сетевых ответов
- — для периодических событий
- — для наблюдения за уведомлениями
- — издатели, управляемые вручную, которые передают значения нескольким подписчикам
- — предмет, сохраняющий текущее значение и публикующий обновления.
Вы также можете создавать пользовательские издатели, соблюдая протокол FLT:8, хотя на практике многие потребности удовлетворяются путем объединения существующих издателей с операторами.
Подписчики
Подписчик Подписчик — это протокол, который получает значения от издателя. Подписчики определяют, как обрабатывать входящие данные и события завершения (либо успешное прекращение, либо сбой). В то время как вы можете писать пользовательские подписчики, большинство разработчиков используют встроенный метод, который принимает закрытия как для событий завершения, так и для событий ценности, или , который связывает вывод издателя с свойством на объекте.
Например:
let publisher = Just("Hello, Combine!")
publisher
.sink(receiveCompletion: { completion in
print("Completed: \(completion)")
}, receiveValue: { value in
print("Received: \(value)")
})
Здесь создается неявный абонент, который выполняет предоставленные закрытия каждый раз, когда появляется новое значение или поток завершается.
Операторы
Операторы — это методы на издателях, которые возвращают другого издателя, позволяя вам цепочкой трансформаций. Combine предоставляет богатый набор операторов, в том числе:
- , — значения преобразования
- — передают только значения, удовлетворяющие условию
- — трансформировать и разворачивать опциональные опции
- , — управление скоростью излучаемых значений
- , , — комбинировать несколько потоков
- , — обработка ошибок
- — плоско вложенные издатели
Операторы образуют основу реактивных трубопроводов. При помощи их цепей можно создавать сложные потоки данных, которые легко читать и поддерживать.
Настройка комбинаций в вашем проекте
Combine является частью структуры Foundation и доступен на iOS 13.0+, macOS 10.15+, watchOS 6.0+ и tvOS 13.0+. Дополнительного импорта не требуется, кроме . Однако, поскольку Combine в значительной степени зависит от дженериков и замыканий, вы обычно будете хранить подписки в для управления их жизненным циклом.
AnyCancellable — это стертый по типу объект, который автоматически отменяет подписку при ее размещении. Сохраняя эти токены в коллекции (часто установленной на контроллере просмотра или модели просмотра), вы гарантируете, что подписки очищаются при выпуске объекта-владельца.
var cancellables = Set<AnyCancellable>()
func setupObservation() {
NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)
.sink { _ in
print("App became active")
}
.store(in: &cancellables)
}
Практические примеры использования комбинаций
Чтобы понять силу Combine, он помогает увидеть его применение к реальным сценариям. Ниже приведены несколько примеров, демонстрирующих общие закономерности, от сетевых обновлений и обновлений пользовательского интерфейса до объединения нескольких асинхронных источников.
1. Получение сетевых данных с помощью обработки ошибок
Одно из наиболее распространенных применений для Combine — сетевые запросы. возвращает издателю, который издаёт набор . Затем вы можете использовать операторов для извлечения и декодирования данных.
import Combine
import Foundation
var cancellables = Set<AnyCancellable>()
struct User: Codable {
let id: Int
let name: String
}
func fetchUser(id: Int) {
let url = URL(string: "https://jsonplaceholder.typicode.com/users/\(id)")!
URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.decode(type: User.self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("Request completed successfully")
case .failure(let error):
print("Request failed with error: \(error.localizedDescription)")
}
}, receiveValue: { user in
print("Fetched user: \(user.name)")
})
.store(in: &cancellables)
}
В этом конвейере оператор пытается разобрать исходные данные на объект . Если декодирование не удается, ошибка передается обработчику завершения абонента. оператор обеспечивает закрытие выполняется на главной очереди, что необходимо для обновлений пользовательского интерфейса.
2. Обработка входа пользователя с помощью дебюнсации
Комбинируйте превосходные результаты при обработке событий пользовательского интерфейса, таких как ввод текстового поля. Используя , вы можете подождать, пока пользователь не приостановит ввод, прежде чем запускать поиск или валидацию.
class SearchViewModel: ObservableObject {
@Published var query: String = ""
@Published var results: [String] = []
private var cancellables = Set<AnyCancellable>()
init() {
$query
.debounce(for: .milliseconds(300), scheduler: RunLoop.main)
.removeDuplicates()
.sink { [weak self] searchText in
self?.performSearch(with: searchText)
}
.store(in: &cancellables)
}
private func performSearch(with text: String) {
// Simulate network search
print("Searching for: \(text)")
}
}
Обертка свойств создает издателя, который каждый раз издает новые значения изменяется. оператор ждет 300 миллисекунд после последнего события, и предотвращает дублирование поиска одного и того же текста. Этот шаблон уменьшает ненужные сетевые вызовы и улучшает производительность.
3.Слияние нескольких асинхронных источников
Часто приложению необходимо объединить данные из нескольких независимых потоков. Например, вам может потребоваться дождаться загрузки как профиля пользователя, так и списка друзей перед обновлением пользовательского интерфейса. Оператор позволяет объединить последние значения от двух издателей.
let profilePublisher = URLSession.shared.dataTaskPublisher(for: profileURL)
.map { $0.data }
.decode(type: UserProfile.self, decoder: JSONDecoder())
let friendsPublisher = URLSession.shared.dataTaskPublisher(for: friendsURL)
.map { $0.data }
.decode(type: [Friend].self, decoder: JSONDecoder())
profilePublisher
.combineLatest(friendsPublisher)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { completion in
// handle errors
}, receiveValue: { profile, friends in
// update UI with both profile and friends
print("Profile: \(profile.name), Friends: \(friends.count)")
})
.store(in: &cancellables)
С абонент получает набор, содержащий последние значения от обоих издателей, когда либо издает новое значение. Для сценариев, где вам нужно спаривать значения по порядку (первый с первым, второй со вторым), используйте .
4. Восстановление ошибок с помощью Catch and Retry
Сети ненадежны, и Combine предоставляет операторам возможность изящно справляться с отказами. Оператор позволяет вам заменить резервного издателя при возникновении ошибки, в то время как пытается повторно подписаться на определенное количество раз.
URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.decode(type: [String].self, decoder: JSONDecoder())
.retry(3)
.catch { error in
// Return a fallback empty array
Just([]).setFailureType(to: Error.self)
}
.sink(receiveCompletion: { completion in
// Will not receive failure because catch handled it
}, receiveValue: { data in
print("Received \(data.count) items")
})
.store(in: &cancellables)
Улавливая ошибку и предоставляя значение по умолчанию, поток успешно завершается с точки зрения абонента. Этот шаблон особенно полезен для обеспечения изящного ухудшенного опыта, когда данные недоступны.
5. Обязательство к UI с назначением
Оператор Combine позволяет привязать выход издателя непосредственно к свойству объекта, устраняя необходимость ручного назначения в замыкании раковины. Это особенно мощно при работе с SwiftUI.
class SettingsViewModel: ObservableObject {
@Published var isDarkMode: Bool = false
}
// In your view controller
let viewModel = SettingsViewModel()
NotificationCenter.default.publisher(for: .darkModeChanged)
.compactMap { $0.userInfo?["enabled"] as? Bool }
.assign(to: \.isDarkMode, on: viewModel)
.store(in: &cancellables)
Этот шаблон сохраняет ваш код кратким и декларативным. Однако будьте осторожны с , когда владелец имущества может быть размещен до окончания подписки. В таких случаях используйте слабые ссылки или подход с явным управлением жизненным циклом.
Управление памятью и жизненный цикл
Комбинированные подписки — это ресурсы, которыми необходимо тщательно управлять, чтобы избежать утечек памяти или неожиданного поведения.Каждый звонок в или возвращает токен . Если вы отбрасываете этот токен без его хранения, подписка немедленно отменяется и никакие значения не принимаются.
Рекомендуемая схема заключается в хранении всех отменяемых токенов в свойстве на объекте владения (например, контроллере просмотра, модели просмотра или менеджере). Когда этот объект размещен, набор деинициализируется, и все подписки автоматически отменяются. Это гарантирует, что ресурсы высвобождаются надлежащим образом.
class MyViewController: UIViewController {
private var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.publisher(for: .someNotification)
.sink { [weak self] notification in
self?.handleNotification(notification)
}
.store(in: &cancellables)
}
}
Одна тонкость заключается в том, что замыкания внутри абонентов захватывают ссылки сильно по умолчанию. Чтобы избежать сохранения циклов, захватывают , когда закрытие ссылается на объект-владелец. Это особенно важно в долгоживущих подписках, которые могут пережить объект.
Интеграция в сочетании с SwiftUI и UIKit
Комбинация работает как с SwiftUI, так и с UIKit, хотя модели интеграции немного отличаются.
SwiftUI
SwiftUI был разработан с учетом Combine. обертка свойств на автоматически создает издателя, а взгляды SwiftUI подписываются на изменения через или обертки свойств. Это устраняет необходимость вручную управлять подписками во многих случаях.
class TimerViewModel: ObservableObject {
@Published var timeString: String = ""
private var cancellables = Set<AnyCancellable>()
init() {
Timer.publish(every: 1.0, on: .main, in: .common)
.autoconnect()
.map { formatter.string(from: $0) }
.assign(to: &$timeString)
}
}
struct TimerView: View {
@StateObject private var viewModel = TimerViewModel()
var body: some View {
Text(viewModel.timeString)
}
}
Здесь оператор связывает отформатированное время непосредственно с опубликованным свойством, и SwiftUI автоматически обновляет вид при изменении строки.
Айкит
В UIKit Combine блистает заменой делегирования или шаблонов обратного вызова. Вы можете использовать , чтобы реагировать на события издателя и напрямую обновлять элементы пользовательского интерфейса, или использовать для привязки. Ключом является отправка работы по основной очереди с использованием перед обновлением интерфейса.
class LoginViewController: UIViewController {
@IBOutlet weak var usernameField: UITextField!
@IBOutlet weak var passwordField: UITextField!
@IBOutlet weak var loginButton: UIButton!
private var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
let usernamePublisher = NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: usernameField)
.compactMap { ($0.object as? UITextField)?.text }
let passwordPublisher = NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: passwordField)
.compactMap { ($0.object as? UITextField)?.text }
Publishers.CombineLatest(usernamePublisher, passwordPublisher)
.map { username, password in
!username.isEmpty && !password.isEmpty
}
.assign(to: \.isEnabled, on: loginButton)
.store(in: &cancellables)
}
}
В этом примере Combine слушает изменения текстового поля, объединяет последние значения и позволяет кнопку входа в систему только тогда, когда оба поля не пусты. Результатом является чистая, декларативная замена методов делегирования и шаблонов целевого действия.
Продвинутые темы: пользовательские издатели и обратное давление
Для более специализированных нужд вы можете создавать пользовательские издатели, соблюдая протокол . Для этого требуется реализовать метод , который соединяет вашего издателя с подписчиком ниже по течению. В то время как создание пользовательских издателей обеспечивает максимальную гибкость, большинство вариантов использования охватываются встроенными издателями и операторами Apple.
Одна концепция, которая заслуживает внимания, — это обратное давление. Комбинация — это управляемая тягой структура: подписчики запрашивают у издателя определенное количество значений, и издатель доставляет их по запрошенной скорости. Это мешает издателю подавлять подписчика. По умолчанию запрашивает неограниченное количество значений, но вы можете реализовать пользовательских подписчиков с контролем спроса, если это необходимо. На практике встроенные операторы правильно обрабатывают распространение спроса для подавляющего большинства приложений.
Для дальнейшего чтения по передовым схемам комбинирования обратитесь к сессии 722 WWDC 2019: Комбинируйте на практике для официального руководства по строительству реактивных трубопроводов.
Преимущества принятия комбината
Интеграция Combine в ваш рабочий процесс разработки предлагает несколько ощутимых преимуществ:
- Декларативная ясность — Трубопроводы выражаются как цепочка операторов, что делает поток данных очевидным с первого взгляда.
- Вместимость — Малые, сфокусированные операторы могут быть объединены для построения сложного поведения без запутанных гнезд обратного вызова.
- Безопасность типа — Проверка времени компиляции улавливает несоответствующие типы вывода и пробелы в обработке ошибок перед временем выполнения.
- Управление ресурсами (FLT:0) — Автоматическая отмена при размещении сделок уменьшает утечки памяти.
- Экосистемная интеграция — Объедините работы с SwiftUI, Foundation и многими сторонними фреймворками, которые выставляют издателей.
- Проверяемость — Издатели могут быть высмеяны, а подписки могут быть проверены с использованием ожиданий XCTest.
Команды, которые принимают Combine, часто сообщают о сокращении кода на основе boilerplate и меньшем количестве ошибок, связанных с условиями гонки или забытыми обратными вызовами. Реакционная парадигма также поощряет более управляемую данными архитектуру, где поток информации ясен и прослеживается.
Лучшие практики для производственного кодекса
Чтобы получить максимальную отдачу от комбината, избегая распространенных подводных камней, следуйте этим рекомендациям:
- Всегда управляйте подписками — Отменить магазин в наборе и использовать . Никогда не полагайтесь на длительные подписки без четкого владельца.
- Захват слабых ссылок — Используйте в замыканиях раковины, чтобы предотвратить сохранение циклов, особенно когда замыкание ссылается на объект, который владеет отменяемыми устройствами.
- Отправка в правильную очередь — Используйте , чтобы обеспечить обработку значений на соответствующем планировщике, обычно основной очереди для обновлений пользовательского интерфейса.
- Предпочтите для привязки — Когда вам просто нужно обновить свойство, используйте вместо для более чистого кода.
- Перенос с SwiftUI — В проектах SwiftUI сочетайте свойства с системой наблюдения SwiftUI для автоматических обновлений.
- Ошибки в работе с цепями на уровне трубопровода — Используйте , или для восстановления после сбоев, а не для распространения ошибок среди абонентов.
- Сохраняйте сфокусированные трубопроводы — избегайте построения монолитных цепей. Разбейте сложную логику на многоразовые помощники издателей или специализированные методы.
- Профиль с инструментами — Используйте санитарный фильтр и комбинированные инструменты для обнаружения гонок данных и утечек подписки.
Для более глубокого погружения в шаблоны обработки ошибок Combine см. руководство Apple по обработке ошибок в Combine [FLT: 1].
Заключение
Combine обеспечивает надежную, декларативную основу для управления асинхронными потоками данных в приложениях iOS, macOS, watchOS и tvOS. Его модель издателя-подписчика-оператора позволяет с ясностью и точностью выражать сложные реактивные потоки, в то время как его тесная интеграция с SwiftUI и Foundation снижает бойлерплейт и улучшает ремонтопригодность кода. От простых сетевых запросов до сложных многоисточниковых конвейеров Combine оснащает разработчиков инструментами для создания отзывчивых, устойчивых приложений, которые изящно обрабатывают неопределенности асинхронных данных.
Когда вы принимаете Combine, начните с замены простых делегированных или обратных шаблонов простыми конвейерами, а затем постепенно исследуйте более продвинутых операторов по мере роста вашей уверенности. Композибельность фреймворка означает, что даже скромное использование Combine может привести к немедленному улучшению читаемости и правильности кода. При внимательном внимании к управлению памятью и нажатию потоков, Combine станет неотъемлемой частью вашего инструментария разработки iOS, позволяя вам уверенно и элегантно обрабатывать поток событий, основанных на времени.