Как стать автором
Обновить
887.65
OTUS
Цифровые навыки от ведущих экспертов

Сохранение бизнес-логики в Swift Combine. Часть 1

Время на прочтение6 мин
Количество просмотров3.4K
Автор оригинала: Kevin Cheng
Дата-ориентированный Combine





Перевод статьи подготовлен специально для студентов продвинутого курса «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.

Следовательно мы узнаем две вещи из наших наблюдений.

  1. Поток — не самая маленькая единица в структуре. Самая маленькая — значение потока.
  2. Каждое значение потока может иметь неограниченные операторы, которые управляют тем, когда и какое передается значение.

Создаем свой 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».
Теги:
Хабы:
Всего голосов 10: ↑10 и ↓0+10
Комментарии0

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS