Дата-ориентированный Combine
Перевод статьи подготовлен специально для студентов продвинутого курса «iOS Разработчик».
В предыдущей серии постов мы успешно построили платформу поверх SwiftUI, с помощью которой вы можете свободно наблюдать последовательности значений, проходящих через publisher Combine.
Мы также создали ряд примеров, демонстрирующих несколько операторов по умолчанию Combine, которые способны изменять и преобразовывать значения в последовательностях, таких
К этому моменту некоторые из вас могли устать от необходимости организовывать и поддерживать так много кода для каждого из примеров (по крайней мере я уже устал). Посмотрите, сколько их в репозитории combine-magic-swiftui в папке туториала? Каждый из примеров является представлением SwiftUI. Каждый из них просто передает одного или нескольких паблишеров в
Поэтому я должен иметь возможность программно генерировать список паблишеров при запуске приложения и повторно использовать
Однако проблема с этим решением — масштабируемость, когда нужно создать много паблишеров.
Мое решение этой проблемы заключается в том, что я должен как-то хранить эти паблишеры. Если я смогу как-то сериализовать их, я смогу их и сохранить. Если мне удастся сохранить их, я не только смогу модифицировать их без изменения кода, но также смогу поделиться ими с другими устройствами, поддерживающими Combine.
Теперь давайте рассмотрим здесь наши цели более конкретно. Поскольку у нас есть список потоков и операторов в формате
Очевидно, нам также необходимо иметь возможность преобразовывать сохраненные данные обратно в паблишер, но, кроме того, мы хотим иметь возможность обмениваться, передавать и распространять этих паблишеров с операторами из одного места в другое.
После того, как мы настроим такую структуру, как вы, возможно, уже догадались, в распределенной среде, централизованная служба может начать управлять вычислительной логикой для группы клиентов.
Так как нам это сделать? Мы начнем с разработки структуры, которая сериализуема и десериализуема. Протокол Swift
Прежде чем двигаться дальше, чтобы понять, какие компоненты необходимы для структур, которые мы собираемся создать, давайте вспомним основной поток, который мы создали в предыдущей серии постов.
Это самый простой поток; однако, если вы посмотрите глубже, вы заметите, что это не просто последовательность массивов. Каждый из круглых блоков имеет свой собственный оператор задержки (
И в целом все это выглядит так:
Каждое значение задерживается на секунду, и к следующему значению добавляется тот же оператор
Следовательно мы узнаем две вещи из наших наблюдений.
Поскольку значение потока и его операторы являются наименьшей единицей, начнем с создания его структуры. Давайте назовем ее
Значение потока должно быть универсальным, чтобы вмещать значения любого типа.
Мы обсудим структуру для операторов позже. Давайте соединим массив
Как мы упоминали ранее, операторы
Мы рассматриваем оператор
Разумеется, перечисление
Теперь у нас есть хорошая структура для представления этого последовательного потока, который генерирует значения от 1 до 4 с секундным интервалом задержки.
l
Теперь мы создали экземпляр потока; однако, если мы не преобразуем его в паблишер, все окажется бессмысленным. Давайте попробуем.
Прежде всего, каждая модель оператора ссылается на фактический оператор Combine, который должен добавить к данному паблишеру и вернуть эксплуатируемый паблишер.
На данный момент есть только один тип оператора —
Теперь мы можем начать задействовать паблишеры к каждому
Мы начинаем со значения
На уровне
Вы правильно догадались: мы используем метод
Теперь мы можем просто декодировать паблишер, передавать и создавать StreamView (посмотрите, как мы это делали в предыдущих постах). И последнее, но не менее важное: теперь мы можем просто редактировать
Смотрите демо ниже. Теперь мы можем вносить изменения в поток без изменения кода.
В следующей части мы собираемся добавить больше операторов в перечисление operator и начать применять их на уровне потока.
До следующего раза, вы можете найти исходный код здесь в этом репозитории combine-magic-swifui в папке combine-playground.
Ждем ваши комментарии и приглашаем на открытый вебинар по теме «iOS-приложение на SwiftUI с использованием Kotlin Mobile Multiplatform».
Перевод статьи подготовлен специально для студентов продвинутого курса «iOS Разработчик».
В предыдущей серии постов мы успешно построили платформу поверх SwiftUI, с помощью которой вы можете свободно наблюдать последовательности значений, проходящих через publisher Combine.
Мы также создали ряд примеров, демонстрирующих несколько операторов по умолчанию Combine, которые способны изменять и преобразовывать значения в последовательностях, таких
filter
, map
, drop
и scan
. Кроме того, мы представили еще несколько операторов, которые соединяют (Zip
и CombineLatest
) или унифицируют (Merge
и Append
) последовательности.К этому моменту некоторые из вас могли устать от необходимости организовывать и поддерживать так много кода для каждого из примеров (по крайней мере я уже устал). Посмотрите, сколько их в репозитории combine-magic-swiftui в папке туториала? Каждый из примеров является представлением SwiftUI. Каждый из них просто передает одного или нескольких паблишеров в
StreamView
, и StreamView
подписывает паблишеров по нажатию кнопки.Поэтому я должен иметь возможность программно генерировать список паблишеров при запуске приложения и повторно использовать
StreamView
, как на скриншоте ниже.Однако проблема с этим решением — масштабируемость, когда нужно создать много паблишеров.
Мое решение этой проблемы заключается в том, что я должен как-то хранить эти паблишеры. Если я смогу как-то сериализовать их, я смогу их и сохранить. Если мне удастся сохранить их, я не только смогу модифицировать их без изменения кода, но также смогу поделиться ими с другими устройствами, поддерживающими Combine.
Хранение и передача операторов Combine
Теперь давайте рассмотрим здесь наши цели более конкретно. Поскольку у нас есть список потоков и операторов в формате
Publisher
, мы хотели бы иметь возможность сохранять их в хранилища любого вида — например, на жестком диске или в базе данных.Очевидно, нам также необходимо иметь возможность преобразовывать сохраненные данные обратно в паблишер, но, кроме того, мы хотим иметь возможность обмениваться, передавать и распространять этих паблишеров с операторами из одного места в другое.
После того, как мы настроим такую структуру, как вы, возможно, уже догадались, в распределенной среде, централизованная служба может начать управлять вычислительной логикой для группы клиентов.
Структура Codable
Так как нам это сделать? Мы начнем с разработки структуры, которая сериализуема и десериализуема. Протокол Swift
Codable
позволяет нам делать это через JSONEncoder
и JSONDecoder
. Более того, структура должна правильно представлять данные и поведения для наименьшей единицы значения в потоке вплоть до сложных цепочек операторов.Прежде чем двигаться дальше, чтобы понять, какие компоненты необходимы для структур, которые мы собираемся создать, давайте вспомним основной поток, который мы создали в предыдущей серии постов.
Поток чисел
Это самый простой поток; однако, если вы посмотрите глубже, вы заметите, что это не просто последовательность массивов. Каждый из круглых блоков имеет свой собственный оператор задержки (
delay
), который определяет фактическое время, когда он должен быть передан. Каждое значение в Combine выглядит так:Just(value).delay(for: .seconds(1), scheduler: DispatchQueue.main)
И в целом все это выглядит так:
let val1 = Just(1).delay(for: .seconds(1), scheduler: DispatchQueue.main)
let val2 = Just(2).delay(for: .seconds(1), scheduler: DispatchQueue.main)
let val3 = ....
let val4 = ....
let publisher = val1.append(val2).append(val3).append(val4)
Каждое значение задерживается на секунду, и к следующему значению добавляется тот же оператор
delay
.Следовательно мы узнаем две вещи из наших наблюдений.
- Поток — не самая маленькая единица в структуре. Самая маленькая — значение потока.
- Каждое значение потока может иметь неограниченные операторы, которые управляют тем, когда и какое передается значение.
Создаем свой StreamItem
Поскольку значение потока и его операторы являются наименьшей единицей, начнем с создания его структуры. Давайте назовем ее
StreamItem
.struct StreamItem<T: Codable>: Codable {
let value: T
var operators: [Operator]
}
StreamItem
включает в себя значение потока и массив операторов. Согласно нашим требованиям, мы хотим иметь возможность сохранять все в структуре, чтобы и value
, и StreamItem
соответствовали протоколу Codable
.Значение потока должно быть универсальным, чтобы вмещать значения любого типа.
Создаем свою StreamModel
Мы обсудим структуру для операторов позже. Давайте соединим массив
StreamItem
в StreamModel
.struct StreamModel<T: Codable>: Codable, Identifiable {
var id: UUID
var name: String?
var description: String?
var stream: [StreamItem<T>]
}
StreamModel
содержит массив StreamItem
-ов. StreamModel
также имеет свойства идентификатора, имени и описания. Опять же, все в StreamModel
должно быть Codable для сохранения и распространения.Создаем структуру оператора
Как мы упоминали ранее, операторы
delay
могут изменять время передачи StreamItem
.enum Operator {
case delay(seconds: Double)
}
Мы рассматриваем оператор
delay
как перечисление (enum
) с одним связанным значением, чтобы хранить время задержки.Разумеется, перечисление
Operator
также должно соответствовать Codable
, что включает в себя кодирование и декодирование связанных значений. Смотрите полную реализацию ниже.enum Operator {
case delay(seconds: Double)
}
extension Operator: Codable {
enum CodingKeys: CodingKey {
case delay
}
struct DelayParameters: Codable {
let seconds: Double
}
enum CodingError: Error { case decoding(String) }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let delayParameters = try? container.decodeIfPresent(DelayParameters.self, forKey: .delay) {
self = .delay(seconds: delayParameters.seconds)
return
}
throw CodingError.decoding("Decoding Failed. \(dump(container))")
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .delay(let seconds):
try container.encode(DelayParameters(seconds: seconds), forKey: .delay)
}
}
}
Теперь у нас есть хорошая структура для представления этого последовательного потока, который генерирует значения от 1 до 4 с секундным интервалом задержки.
l
et streamA = (1...4).map { StreamItem(value: $0,
operators: [.delay(seconds: 1)]) }
let serialStreamA = StreamModel(id: UUID(), name: "Serial Stream A",
description: nil, stream: streamA)
Конвертируем StreamModel в Publisher
Теперь мы создали экземпляр потока; однако, если мы не преобразуем его в паблишер, все окажется бессмысленным. Давайте попробуем.
Прежде всего, каждая модель оператора ссылается на фактический оператор Combine, который должен добавить к данному паблишеру и вернуть эксплуатируемый паблишер.
extension Operator {
func applyPublisher<T>(_ publisher: AnyPublisher<T, Never>) -> AnyPublisher<T, Never> {
switch self {
case .delay(let seconds):
return publisher.delay(for: .seconds(seconds), scheduler: DispatchQueue.main).eraseToAnyPublisher()
}
}
}
На данный момент есть только один тип оператора —
delay
. Мы будем добавлять больше, по ходу дела.Теперь мы можем начать задействовать паблишеры к каждому
StreamItem
.extension StreamItem {
func toPublisher() -> AnyPublisher<T, Never> {
var publisher: AnyPublisher<T, Never> =
Just(value).eraseToAnyPublisher()
self.operators.forEach {
publisher = $0.applyPublisher(publisher)
}
return publisher
}
}
Мы начинаем со значения
Just
, обобщаем его с помощью метода eraseToAnyPublisher
, а затем задействуем паблишеры из всех связанных операторов.На уровне
StreamModel
мы получаем паблишера всего потока.extension StreamModel {
func toPublisher() -> AnyPublisher<T, Never> {
let intervalPublishers =
self.stream.map { $0.toPublisher() }
var publisher: AnyPublisher<T, Never>?
for intervalPublisher in intervalPublishers {
if publisher == nil {
publisher = intervalPublisher
continue
}
publisher =
publisher?.append(intervalPublisher).eraseToAnyPublisher()
}
return publisher ?? Empty().eraseToAnyPublisher()
}
}
Вы правильно догадались: мы используем метод
append
для объединения паблишеров.Визуализация, редактирование и снова визуализация потока
Теперь мы можем просто декодировать паблишер, передавать и создавать StreamView (посмотрите, как мы это делали в предыдущих постах). И последнее, но не менее важное: теперь мы можем просто редактировать
StreamModel
, добавить дополнительные StreamItem
с новыми значениями и даже поделиться этой моделью с другими устройствами через Интернет.Смотрите демо ниже. Теперь мы можем вносить изменения в поток без изменения кода.
Следующая глава: Сериализация/десериализация фильтров и операторов карт
В следующей части мы собираемся добавить больше операторов в перечисление operator и начать применять их на уровне потока.
До следующего раза, вы можете найти исходный код здесь в этом репозитории combine-magic-swifui в папке combine-playground.
Ждем ваши комментарии и приглашаем на открытый вебинар по теме «iOS-приложение на SwiftUI с использованием Kotlin Mobile Multiplatform».