Pull to refresh

Общее представление об архитектуре Clean Swift

Reading time 6 min
Views 35K
Привет, читатель!

В этой статье я расскажу об архитектуре iOS приложений — Clean Swift. Мы рассмотрим основные теоретические моменты и разберем пример на практике.



Теория


Для начала разберем основную терминологию архитектуры. В Clean Swift приложение состоит из сцен, т.е. каждый экран приложения — это одна сцена. Основное взаимодействие в сцене идет через последовательный цикл между компонентами ViewController -> Interactor -> Presenter. Это называется VIP цикл.

Мостом между компонентами выступает файл Models, который хранит в себе передаваемые данные. Так же есть Router, отвечающий за переход и передачу данных между сценами, и Worker, который берет часть логики Interactor’a на себя.



View


Storyboard’ы, XIB’ы или UI элементы, написанные через код.

ViewController


Отвечает только за конфигурацию и взаимодействие с View. В контроллере не должно находиться никакой бизнес логики, взаимодействия с сетью, вычислений и так далее.
Его задача обрабатывать события с View, отображать или отправлять данные (без обработки и проверок) в Interactor.

Interactor


Содержит в себе бизнес логику сцены.

Он работает с сетью, базой данных и модулями устройства.

Interactor получает запрос из ViewController’a (с данными или пустой), обрабатывает его и, если это требуется, передает новые данные в Presenter.

Presenter


Занимается подготовкой данных для отображения.

Как пример, добавить маску на номер телефона или сделать первую букву в названии заглавной.
Обрабатывает данные, получение из Interactor’a, после чего отправляет их обратно во ViewController.

Models


Набор структур для передачи данных между компонентами VIP цикла. Каждый круг цикла имеет в себе 3 вида структур:

  • Request — Структура с данными (текст из TextField и т.д.) для передачи из ViewController'a в Interactor
  • Response — Структура с данными (загруженными из сети и т.д.) для передачи из Interactor в Presenter
  • ViewModel — Структура с обработанными данными (форматирование текста и т.д.) в Presenter’e для передачи обратно во ViewController

Worker


Разгружает Interactor, забирая на себя часть бизнес логики приложения, если Interactor стремительно разрастается.

Так же можно создавать общие для всех сцен Worker’ы, если их функционал используется в нескольких сценах.

Как пример, в Worker можно выносить логику работы с сетью или базой данных.

Router


В Router выносится вся логика, отвечающая за переходы и передачу данных между сценами.



Для прояснения картины работы VIP цикла приведу стандартный пример — авторизация.

  1. Пользователь ввел свой логин и пароль, нажал на кнопку авторизации
  2. У ViewController срабатывает IBAction, после чего создается структура с введенными, в TextField’ы, данными пользователя (Models -> Request)
  3. Созданная структура передается в метод fetchUser в Interactor’e
  4. Interactor отправляет запрос в сеть и получает ответ об успешности авторизации
  5. На основе полученных данных, создает структуру с результатом (Models -> Response) и передается в метод presentUser в Presenter’e
  6. Presenter форматирует данные по необходимости и возвращает их (Models -> ViewModel) в метод displayUser в ViewController’e
  7. ViewController отображает полученные данные пользователю. В случае с авторизацией, может выводиться ошибка или срабатывать переход на другую сцену с помощью Router

Таким образом, мы получаем единую и последовательную структуру, с распределением обязанностей на компоненты.

Практика


А теперь разберем небольшой практический пример, который покажет как проходит VIP цикл. В этом примере мы сымитируем подгрузку данных при открытии сцены (экрана). Основные участки кода я пометил комментариями.

Весь VIP цикл завязан на протоколах, что обеспечивает возможностью подмены каких-либо модулей без нарушения работы приложения.
Для ViewController’a создается протокол DisplayLogic, ссылка на который передается в Presenter для последующего вызова. Для Interactor’a создаются два протокола BusinessLogic, отвечающий за вызов методов из ViewController’a, и DataSource, для хранения данных и передачу через Router в Interactor другой сцены. Presenter подписывается под протокол PresentationLogic, для вызова из Interactor’a. Связующим элементом всего этого выступает Models. Он содержит в себе структуры, с помощью которых идет обмен информаций между компонентами VIP цикла. С него и начнем разбор кода.



Models


В примере ниже, для сцены Home, я создал файл HomeModels, который содержит в себе набор запросов для VIP цикла.
Запрос FetchUser будет отвечать за подгрузку данных о пользователе, который мы и будем рассматривать дальше.

// Models
/// Модель для передачи данных в VIP цикле
enum HomeModels {

  /// Набор запросов для одного VIP цикла
  enum FetchUser {

    /// Запрос к Interactor из View Controller
    struct Request {
      let userName: String
    }

    /// Запрос к Presentor из Interactor
    struct Response {
      let userPhone: String
      let userEmail: String
    }

    /// Запрос к View Controller из Presentor
    struct ViewModel {
      let userPhone: String
      let userEmail: String
    }
  }
}

ViewController


При инициализации класса мы создаем экземпляры классов Interactor’a и Presenter’a этой сцены и устанавливаем зависимости между ними.
Далее во ViewController’e остается ссылка только на Interactor. С помощью этой ссылки мы будем создавать запрос к методу fetchUser(request:) в Interactor’е, для запуска VIP цикла.

Здесь стоит обратить внимание, как происходит запрос к Interactor. В методе loadUserInfromation() мы создаем экземпляр структуры Request, куда передаем начальное значение. Оно может быть взято из TextField, таблицы и так далее. Экземпляр структуры Request передается в метод fetchUser(request:), который находится в протоколе BusinessLogic нашего Interactor’a.

// ViewController
/// Протокол логики для отображения подготовленной информации
protocol HomeDisplayLogic: class {

  /// Метод логики отображения данных
  func displayUser(_ viewModel: HomeModels.FetchUser.ViewModel)
}

final class HomeViewController: UIViewController {

  /// Ссылка на протокол бизнес логики Interactor'a сцены
  var interactor: HomeBusinessLogic?

  override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
    super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
    setup()
  }

  required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    setup()
  }

  /// Метод для стартовой настройки компонентов сцены
  private func setup() {
    // Создаем компоненты VIP цикла
    let interactor = HomeInteractor()
    let presenter = HomePresenter()

    // Связываем созданные компоненты
    interactor.presenter = presenter
    presenter.viewController = self

    // Указываем ссылку на Interactor для View Controller
    self.interactor = interactor
  }

  override func viewDidLoad() {
    super.viewDidLoad()

    // Запускаем метод подгрузки данных при показе сцены
    fetchUser()
  }

  /// Делаем запрос к Interactor для получения данных
  private func loadUserInfromation() {
    // Создаем экземпляр запроса к Interactor с параметрами
    let request = HomeModels.FetchUser.Request(userName: "Aleksey")

    // Вызываем метод бизнес логики у Interactor'a
    interactor?.fetchUser(request)
  }
}

/// Подписываем контроллер под протокол HomeDisplayLogic
extension HomeViewController: HomeDisplayLogic {

  func displayUser(_ viewModel: HomeModels.FetchUser.ViewModel) {
    print(viewModel)
  }
}

Interactor


Экземпляр класса Interactor’a содержит в себе ссылку на протокол PresentationLogic, под который подписан Presenter.

В методе fetchUser(request:), может содержаться любая логика подгрузки данных. Для примера я просто создал константы, с якобы полученными данными.

В этом же методе создается экземпляр структуры Response и заполняется, полученными ранее, параметрами. Response передается в PresentationLogic с помощью метода presentUser(response:). Другими словами, здесь мы получили сырые данные и передали их на обработку в Presenter.

// Interactor
/// Протокол бизнес логики Interactor'a
protocol HomeBusinessLogic: class {

  /// Метод получения данных из сети или других источников
  func fetchUser(_ request: HomeModels.FetchUser.Request)
}

final class HomeInteractor: HomeBusinessLogic {

  /// Ссылка на логику презентора сцены
  var presenter: HomePresentationLogic?

  func fetchUser(_ request: HomeModels.FetchUser.Request) {
    // Здесь должен быть код получения данных
    // Для примера просто создадим константы
    let userPhone = "+7 (999) 111-22-33"
    let userEmail = "im@alekseypleshkov.ru"
    // ...
    // Создаем запрос к Presentor'у с необходимыми данными
    let response = HomeModels.FetchUser.Response(userPhone: userPhone, userEmail: userEmail)

    // Вызываем метод логики презентации у Presentor'а
    presenter?.presentUser(response)
  }
}

Presenter


Имеет ссылку на протокол DisplayLogic, под который подписан ViewController. Не содержит никакой бизнес логики, а только форматирует полученные данные перед отображением. В примере мы отформатировали номер телефона, подготовили экземпляр структуры ViewModel и передали его на ViewController, с помощью метода displayUser(viewModel:) в протоколе DisplayLogic, где уже происходит отображение данных.

/// Протокол логики презентации
protocol HomePresentationLogic: class {

  /// Метод форматирования полученных данных с Interactor'a
  func presentUser(_ response: HomeModels.FetchUser.Response)
}

final class HomePresenter: HomePresentationLogic {

  /// Ссылка на логику отображения View Controller'a
  weak var viewController: HomeDisplayLogic?

  func presentUser(_ response: HomeModels.FetchUser.Response) {
    // Для примера отформатируем номер телефона
    let formattedPhone = response.userPhone.replacingOccurrences(of: "-", with: " ")

    // Создаем экземляр ViewModel для отправки в View Controller
    let viewModel = HomeModels.FetchUser.ViewModel(userPhone: formattedPhone, userEmail: response.userEmail)

    // Вызываем метод логики отображения у View Controller'a
    viewController?.displayUser(viewModel)
  }
}

Заключение


С данной архитектурой мы получили возможность распределить обязанности, улучшить удобство тестирования приложения, ввести заменяемость отдельных участков реализации и стандарт написания кода для работы в команде.

Спасибо, что дочитали до конца.

Серия статей


  1. Общее представление об архитектуре Clean Swift (вы здесь)
  2. Router и Data Passing в архитектуре Clean Swift
  3. Workers архитектуры Clean Swift
  4. Unit тестирование в архитектуре Clean Swift
  5. Пример простого интернет-магазина на архитектуре Clean Swift

Все компоненты сцены: Ссылка
Помощь в написании статьи: Bastien
Tags:
Hubs:
+12
Comments 5
Comments Comments 5

Articles