Посадить свои контроллеры представлений на диету с помощью архитектуры Clean Swift (VIP), часть 2
Добро пожаловать обратно! В прошлый раз я говорил о массивных контроллерах представления и некоторых основных причинах. Я также представил один из способов смягчения этого с помощью архитектуры Clean Swift. Я ввел понятие сцена и как интерактор получает события от контроллера представления, такие как запросы API, нажатия кнопок и другие уведомления о событиях. Я также рассмотрел передачу данных с помощью запрос, отклика также просмотреть модель объекты между классами в цикле VIP и то, как эти объекты модели отделяют эту коммуникацию.
Пока мы рассматриваем приведенные ниже примеры кода, не стесняйтесь следовать исходному коду. здесь. Приложение отображает простое табличное представление с изображениями-заполнителями.
Настройка VIP-цикла
В части 1 я объяснил, как данные передаются между классами в цикле VIP посредством Запросы, ответыа также просмотреть моделино не то, как настроен цикл или как передаются зависимости во время инициализации.
Поскольку новый цикл VIP создается при создании нового контроллера представления и одновременно устанавливаются все зависимости, лучше всего настроить все в контроллере представления. Однако, чтобы избежать раздувания контроллера представления логикой установки, создание конфигуратор class может обрабатывать инициализацию цикла VIP.
class PhotoListConfigurator {
static let shared = PhotoListConfigurator()
func configure(viewController: PhotoListViewController) {
let interactor = PhotoListInteractor()
let presenter = PhotoListPresenter()
let router = PhotoListRouter()
viewController.interactor = interactor
viewController.router = router
interactor.presenter = presenter
presenter.viewController = viewController
router.viewController = viewController
router.dataStore = interactor
}
}
Отсюда экземпляр класса можно легко передать в качестве зависимости контроллеру представления.
class PhotoListViewController: UITableViewController {
...
init(configurator: PhotoListConfigurator = PhotoListConfigurator.shared) {
super.init(nibName: nil, bundle: nil)
configurator.configure(viewController: self)
...
}
...
}
Вы могли заметить, что в зависимость передается аргумент по умолчанию, который является общим экземпляром ФотоСписокКонфигуратор учебный класс. Передача аргумента с предоставленным значением по умолчанию позволяет выборочно переопределять зависимости, например, для передачи фиктивных объектов для модульного тестирования. Мы рассмотрим тестирование в следующей части этой серии, но эта идея работает для всех классов в цикле VIP.
class PhotoListInteractor {
private let apiClient: PhotoApiClient
private let imageCache: ImageCache
...
init(apiClient: PhotoApiClient = PhotoApiClient.shared, imageCache: ImageCache = ImageCache.shared) {
self.apiClient = apiClient
self.imageCache = imageCache
}
...
}
Интерактор и рабочие
Когда интерактор получает событие, ожидается, что он выполнит какую-то работу. Работа может выполняться непосредственно в интеракторе, где службы и другие зависимости передаются непосредственно в сам интерактор. Это можно увидеть на примере выше.
Однако при множественных зависимостях, а также при доступе к общим ресурсам, таким как клиенты API, можно утверждать, что интерактор делает больше, чем его доля единой ответственности, которая отвечает на события контроллера представления.
В рамках парадигмы VIP это решается путем выделения зависимостей и связанных с ними задач в отдельные классы, называемые рабочие. По моему опыту использования Clean Swift, большинству сцен, содержащих цикл VIP, нужен только один рабочий для его интерактора (обычно соответствующий соглашению об именах сцены, поэтому ФотоСписокИнтерактор будет иметь PhotoListWorker). Если для сцены требуется несколько зависимостей, и каждая из них имеет несколько задач, может быть хорошей идеей разбить их на еще большее количество рабочих классов. Помните, что простота тестирования также требует внимания, поэтому наличие небольших тестируемых компонентов упрощает написание тестов.
Еще один случай, когда требуются дополнительные работники, — это общие задачи в нескольких сценах. Рабочие предоставляют способ инкапсулировать зависимости а также общие задачи, использующие эти зависимости. Например, включая общий AuthenticationWorker поскольку зависимость нескольких интеракторов в нескольких сценах позволит вам повторно использовать один и тот же код, относящийся к общим задачам аутентификации, во всех областях кода, где необходимо выполнить какую-либо работу по аутентификации.
В любом случае, как только интерактор или один из его рабочих завершает задачу, связанную с событием, поступающим от контроллера представления, нам теперь нужен способ обновить представление, предполагая, что результат задачи требует обновления пользовательского интерфейса. Вот где ведущий приходит в.
Обновление представления
После того, как задача в интерактивном окне завершена, она почти всегда будет сопровождать обновление представления. Это может включать обновление UITableViewотключение кнопки, переход к другой сцене, содержащей другой цикл VIP, или любую комбинацию обновлений представления и маршрутизации.
Ведущий помогает координировать эти обновления представления и сцены. Ведущий занимает отклик объект из интерактора, содержащий все необходимые данные для обновления представления (опять же, подумайте о данных для UITableView и т. д.), и форматирует его в модель представления для передачи контроллеру представления.
Пара вещей, о которых следует помнить при рассмотрении объекта Presenter:
- Функция интерактора не обязательно нуждается в последующей функции презентатора для вызова, если нет обновлений представления, например, сохранение данных в фоновом режиме без фактического изменения чего-либо, что видит пользователь.
- Функция докладчика не всегда должна получать отклик от интерактора или предоставить просмотреть модель для контроллера представления, если никакие дополнительные данные не должны проходить через оставшийся цикл события VIP.
- Интерактору не нужно каждый раз вызывать одну и ту же функцию презентатора. Если запрос API возвращается с ошибкой в интеракторе, вместо вызова настоящее фото () в приведенном ниже примере настоящая ошибка () вместо этого сообщит ведущему, что контроллер представления должен показать пользователю какую-то ошибку.
class PhotoListInteractor: PhotoListBusinessLogic, PhotoListDataStore {
var presenter: PhotoListPresentationLogic?
private var worker: PhotoListWorker?
...
init (worker: PhotoListWorker = PhotoListWorker()) {
self.worker = worker
}
...
func loadPhotosList(request: PhotoList.Fetch.Request) {
worker?.getPhotos(page: request.page, limit: request.limit) { (response: ApiClientResult<[Photo]>) in
switch response {
case .success(let photos):
...
case .error(let error):
self.presenter?.showError(withMessage: error.localizedDescription)
break
default:
self.presenter?.showError(withMessage: "Error loading photos. Please try again later.")
}
}
}
}
Завершение цикла VIP
После того, как презентатор подготовит любые данные для обновления представления, контроллер представления должен применить эти изменения представления. Ведущий создает просмотреть модель struct для передачи любых данных от себя в контроллер представления. Отсюда применяются соответствующие изменения пользовательского интерфейса.
Подобно тому, как функция интерактора может вызывать одну или несколько функций презентатора (подумайте о пути успеха или пути неудачи в зависимости от результата задачи), более чем вероятно, что если бы интерактор мог вызывать одну из нескольких функций презентатора, каждая из этих функций презентатора, вероятно, нуждалась бы в своем собственная уникальная функция для вызова контроллера представления.
К этой ситуации относится упомянутый в предыдущем разделе пример загрузки фотографий или отображения ошибки на основе задачи интератора. Если определение протокола докладчика
protocol PhotoListPresenterInput {
func showPhotoList(response: PhotoList.Fetch.Response)
func showError(response: PhotoList.Error.Response)
...
}
Определение протокола контроллера представления может быть
protocol PhotoListViewControllerInput: class {
func renderPhotosList(viewModel: PhotoList.Fetch.ViewModel)
func showError(viewModel: PhotoList.Error.ViewModel)
}
Если вы заметили учебный класс выше, протокол объявляет, что любые объекты, соответствующие ему, должны быть учебный класс в отличие от структура, перечислениеили какая-либо другая структура данных, которая может соответствовать протоколам.
Глядя на частичное определение примера презентатора, который мы обсуждаем
class PhotoListPresenter: PhotoListPresenterInput {
weak var viewController: PhotoListViewControllerInput?
...
}
Вы заметите, что свойство, ссылающееся на контроллер представления, слабый. Это потому, что уже есть сильный ссылка на контроллер представления при создании и загрузке сцены. Контроллер представления имеет сильную ссылку на интерактора, а интерактор имеет сильную ссылку на презентатора. Если у докладчика была сильная ссылка на контроллер представления, сохранить цикл могут быть созданы, и классы в сцене никогда не будут освобождены при создании и переключении на другую сцену. Правильные типы ссылок на свойства должны выглядеть так:
И последний момент для обсуждения связан с шаблоном, который я обнаружил во время написания сцены; отображение ошибки из контроллера представления, вероятно, является общей задачей для большинства контроллеров представления в вашем приложении и может даже иметь настраиваемый стиль и рендеринг ошибок как часть «темы» вашего приложения.
Решение, которое хорошо сработало для меня, это создание расширение на UIViewController с функцией отображения оповещения, которое можно вызвать с любого контроллера в приложении. Кроме того, любые определения функций протокола для контроллера представления, касающиеся отображения сообщения об ошибке, могут быть извлечены в его собственный AlertDelegate которым может соответствовать это расширение.
import UIKit
protocol MessageBoxDelegate {
func showError(message: String)
func showSuccess(message: String)
}
enum MessageType {
case success
case error
case info
var color: UIColor {
switch self {
case .success: return .green
case .error: return .red
case .info: return .yellow
}
}
}
extension UIViewController: MessageBoxDelegate {
func showError(message: String) {
showMessage(withText: message, andType: .error)
}
func showSuccess(message: String) {
showMessage(withText: message, andType: .success)
}
private func showMessage(withText text: String, andType type: MessageType) {
DispatchQueue.main.async {
let v = MessageBoxView()
v.message = text
v.messageColor = type.color
v.alpha = 0
(self.navigationController?.view ?? self.view).insertSubview(v, at: self.view.subviews.count)
NSLayoutConstraint.activate([
v.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: 10),
v.rightAnchor.constraint(equalTo: self.view.rightAnchor, constant: -10),
v.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -50),
v.heightAnchor.constraint(equalToConstant: 50)
])
UIView.animate(withDuration: 0.5) {
v.alpha = 1
}
Timer.scheduledTimer(withTimeInterval: 3, repeats: false) { _ in
UIView.animate(withDuration: 0.5, animations: {
v.alpha = 0
}, completion: { (completed) in
if completed {
v.removeFromSuperview()
}
})
}
}
}
}
Теперь, когда необходимо отобразить какое-либо предупреждение, свойство, ссылающееся на контроллер представления в презентаторе, может соответствовать нескольким протоколам, а не включать какой-либо вид показатьОшибка() функция в каждом протоколе контроллера представления.
class PhotoListPresenter: PhotoListPresenterInput {
weak var viewController: (PhotoListViewControllerInput & MessageBoxDelegate)?
...
func showError(withMessage message: String) {
viewController?.showError(message: message)
}
}
Сцена с сообщением об ошибке
Переход от сцены к сцене
Последний компонент, о котором нужно рассказать в Clean Swift, — это «клей» между VIP-сценами: маршрутизатор. Объект маршрутизатора создается вместе с остальной частью сцены и живет как свойство в контроллере представления.
class PhotoListViewController: UITableViewController {
...
var router: (NSObjectProtocol & PhotoListRoutingLogic)?
...
func showDetailView(photoID: String) {
router?.showDetailView(withPhotoID: photoID)
}
}
Маршрутизатор имеет слабую ссылку на контроллер представления и использует ее для модального представления нового контроллера представления, доступа к контроллеру представления. UINavgationController для добавления нового контроллера представления в стек навигации или любой другой задачи, требующей отображения или удаления контроллера представления с экрана.
protocol PhotoListRoutingLogic {
func showDetailView(withPhotoID photoID: String)
}
...
class PhotoListRouter: NSObject, PhotoListRoutingLogic, PhotoListDataPassing {
weak var viewController: PhotoListViewController?
...
func showDetailView(withPhotoID photoID: String) {
if let navVC = viewController.navigationController {
let detailVC = PhotoDetailViewController(photoID: photoID)
navVC.pushViewController(detailVC, animated: true)
}
}
}
Если представление необходимо обновить без дополнительных задач, может быть достаточно прямого вызова маршрутизатора из контроллера представления. Однако было бы так же просто отправить задачу через цикл VIP до того, как контроллер представления вызовет метод на маршрутизаторе в конце.
Если вы используете раскадровки и переходы, а не программные представления, маршрутизатор также может обрабатывать логику для этих ситуаций. Ключевым моментом является передача объекта перехода в функцию маршрутизатора, вызываемую внутри prepareForSegue(_:отправитель) функция контроллера представления.
extension PhotoListViewController: PhotoListPresenterOutput {
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
router.passDataToNextScene(segue)
}
}
Дополнительная логика маршрутизации, которая обычно находится в prepareForSegue(_:отправитель) Функция теперь может быть извлечена в маршрутизатор для упрощения тестирования и более компактных контроллеров представления.
Что дальше?
В последних двух постах мы рассмотрели компоненты, составляющие цикл VIP в сцене, а также то, как эти компоненты взаимодействуют друг с другом и с другими сценами. Со всеми инструментами, предназначенными для создания «более чистого» приложения Swift, важно также протестировать эту функциональность. Я завершу эту серию в следующей статье, где мы будем делать именно это. А пока наслаждайтесь работой с Clean Swift!