В поисках чистой архитектуры (1-я часть) — Swift 3.0

в 14:32, , рубрики: clean architecture, clean swift, design patterns, swift, swift 3, tdd, viper, xcode, Программирование, разработка мобильных приложений, метки: ,

image

Приветствую уважаемых жителей Хабрахабра!

Не так давно я стал замечать, что мой код становится громоздким и даже в рамках одного контроллера мне все сложней удержать в голове то, что в нем происходит. Как следствие, на выходе не всегда ожидаемый результат, что я хотел реализовать, так как мозг “замылился” и я легко могу упустить существенную деталь. А после, ручной анализ кода, работа с отладчиком и так далее… Да что уж говорить, доходило до абсурда, при сборке приложения xcode падал замертво и я даже не успевал понять, что случилось в приложении! Нужно было что то менять и думать над архитектурой, так как я не хочу всю свою карьеру писать плохоподдерживаемый код…

Кому интересен вопрос архитектуры приложения, добро пожаловать под кат!

На Swift я перешел не так давно, потому казалось, что в данном языке, априори невозможны подобные реализации. А оказалось, что поговорка про плохого танцора все же имеет ко мне непосредственное отношение. Кстати, за переход на Swift я благодарен лично Ивану Акулову со swiftbook.ru, так как у меня был какой-то психологический барьер на изучение Swift, до версии 2.0 даже не пытался ковырять его, слишком все сырым казалось, да и Objective-C казался вполне легким и логичным. Так было до первой реализации моего первого свифт-приложения, теперь мне сложно при необходимости настроиться писать на “старичке”.

В этой же статье я буду использовать версию языка 3.0, так как массовый переход на него уже осенью, лучше сразу привыкать и освоить все основные “фишки” языка. В чем отличия, писать не буду, буду просто писать код примеров на этой версии. Для разработчика не составит труда самому разобраться с данным вопросом.

Итак, не будем сильно отвлекаться и вернемся к нашим баранам. Когда я решил всерьез улучшить качество кода, то первым делом подумал за VIPER, так как регулярно посещал все тусовки разработчиков в Рамблер и теории нахватался достаточно. Стоит заметить, что в Рамблер поощряют использование их наработок и охотно консультируют по всем сложным вопросам. Они в реальности фанатеют от таких вещей как: VIPER, TDD, Typhoon и слушать их доклады сплошное удовольствие, но остается маленькое но… Это все теория для слушателей! Нужно брать и писать код в реальности, а не виртуально обсуждать все сильные и слабые стороны паттерна. Особенно смущал тот факт, что Рамблер использует свиззлинг в роутинге, это как то размывало классическое определение паттерна. Что не так еще с VIPER? Существует множество модификаций паттерна, нет единого толкования, определения и практик использования. Каждая команда разработчиков понимает его на свой лад и проповедует свою реализацию, как единственно правильную и заслуживающую право на жизнь. В то время, когда я пытался понять, как же правильно использовать VIPER, мне довелось увидеть столько различных его модификаций, что это еще больше запутало меня и все усложнило!

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

Тогда и решил расширить поиск источников, очень сильно выручает знание английского языка, так как в нашем сегменте сети лучше не искать стоящей информации. Есть мнение, что у нас основной контент создают школьники, которые своими публикациями и роликами на youtube хотят подчеркнуть свою значимость и похвастаться перед друзьями. Исключение лишь Хабр и еще пара ресурсов! Но и на Хабре очень мало информации для практики, в основном обсуждается теория, подходы к реализации и так далее.

Так мы искали VIPER, а что нашли?!

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

Итак, кто владеет английским, могут почитать в оригинале: clean-swift.com. Зовут разработчика Рэймонд и он активно поддерживает связь со своими читателями через email и комментарии на страницах своего блога.

Коротко о паттерне

У Рэймонда свое видение чистой архитектуры. Как я понял, ему часто приходилось “фрилансить” и он искал решение быстрого и эффективного кода в своих приложениях, чтобы не сливать хорошие заказы, при этом минимизировать все тяготы общения с заказчиком.

Как он видит чистую архитектуру?

Изображение под спойлером

image

Вью -> Интерактор -> Презентер -> Вью. Роутинг у Рэймонда обособлен и достаточно универсален, позволяет использовать как связывание в коде, так и передачу данных через сегвеи.

Изначально я прошел мимо, не вникая в подробности, просто машинально отправил в закладки браузера, чтобы после почитать для саморазвития, не используя на практике. Но помог случай его попробовать. Откопал старый недоделанный проект для Apple TV, нужно было определить простую реализацию, чтобы код получился не громоздким и “читабельным”. Вот тут и вспомнил за блог Рэймонда, решил все же попробовать его подход в реализации.

Так это VIPER?

Однозначно, нет! Но он имеет право на жизнь, причем и в сложных проектах! Код достаточно просто покрывается тестами (одна из основных фишек VIPER!), причем покрывается как угодно. Это и TAD, TDD, BDD, простые юнит-тесты. Есть четкое понимание, что стоит покрыть тестами, а что можно пропустить. Такая динамичная штука на практике получилась! Почему на практике? Да просто достаточно легко этот пресловутый V-I-P лег на проект!

Итак, позвольте еще раз представить: Clean Swift! Первоисточник: clean-swift.com

Я пообщался с Рэймондом и попросил разрешения использовать его наработки в своих публикациях.

Что мы попробуем сделать?

Понятно, что для примера подойдет максимально простой проект, так же максимально приближенный к боевой реализации. Никаких «Hello World», только реальный код! В этом плане мне на Хабре понравился недавний цикл статей по CoreData, где angryscorp показал работу с CoreData как если бы писался реальный проект.

Значит и мы попробуем повторить его опыт.

ПЛАН ПУБЛИКАЦИЙ:

1-я часть) Будем делать приложение, которое разберет плейлист YouTube, подгрузит в таблицу и через сегвей мы будем использовать передачу кастомной сущности для просмотра в другом контроллере/сцене. Да, роутинг может быть легким и ненавязчивым!

2-я часть) Разберемся с TAD (Test After Development).

3-я часть) Реализуем правильный TDD с аналогичным проектом. Правда для этого я постараюсь добавить материал из еще одного крутого источника: Dr. Dominik Hauser — Test-Driven iOS Development with Swift (снова на версии языка 3.0).

4-я часть) Создадим свои шаблоны для быстрой реализации сцены/контроллеров (Generamba — Rambler&Co нам будет в помощь).

5-я часть) Разберем классические MVC, но только от слова Massive и разложим реально-массивный код через V-I-P.

Приступим?

Костыли не для нас, потому воспользуемся официальным DATA Api компании добра.

Перейдем по ссылке: developers.google.com/youtube/v3, нам потребуется аккаунт Google, у кого его нет, в конфиге тестового проекта оставлю рабочий ключ, специально созданный для этих целей.

Последовательность действия проста и понятна. На вкладке “GUIDES” перейдем по ссылке “Open the API Library”. Нам потребуется категория YouTube API, в ней нужно перейти по ссылке “YouTube Data API” и следовать указаниям мастера активации API и создания ключа.

Под спойлером изображения, как все должно происходить.

Получение ключа для сервиса YouTube Data API

image
image
image

После того, как у нас есть ключ, заимплементим его в файле Config.swift. Я вообще предпочитаю конфиги приложения и хелперы хранить в отдельной области проекта, но решение остается за вами. Самое главное — это ключ! При регистрации ключа, ни в коем случае не указываем бандл приложения, иначе YouTube начнет фильтровать запросы не в вашу пользу, почему то у Google это криво реализовано.

Что дальше?

Для облегчения работы, мы используем шаблоны, Рэймонд прислал увиверсальные шаблоны, включая версию и для Objective-C, на случай если кто-либо захочет попробовать подобную реализацию.

Архив с шаблонами, доступен для загрузки на странице проекта GitHub, стоит посмотреть папку “Extended”. Скачиваем шаблоны и через терминал переходим в папку с распакованными шаблонами. Далее простая команда “make install_templates” и шаблоны установлены. Их можно использовать для работы.

Как Рэймонд использует шаблоны можно посмотреть в этом небольшом видео

Давайте попробуем создать нашу первую сцену для таблицы с будущим списком видео из плейлиста. Воспользуемся для этого примером из видео и наследуемся от UITableViewController. С названием также особо мудрить не будем, пусть производное имя для сцены будет: TableScene, остальные названия шаблон сгенерирует сам.

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

Структура проекта

image

TableSceneConfigurator.swift

import UIKit

// MARK: Connect View, Interactor, and Presenter

extension TableSceneViewController: TableScenePresenterOutput {
  override func prepare(for segue: UIStoryboardSegue, sender: AnyObject?) {
    router.passDataToNextScene(segue: segue)
  }
}

extension TableSceneInteractor: TableSceneViewControllerOutput {}
extension TableScenePresenter: TableSceneInteractorOutput {}

class TableSceneConfigurator {
  // MARK: Object lifecycle
  class var sharedInstance: TableSceneConfigurator {
    return TableSceneConfigurator()
  }
  
  // MARK: Configuration
  func configure(viewController: TableSceneViewController) {
    
    let router = TableSceneRouter()
    router.viewController = viewController
    
    let presenter = TableScenePresenter()
    presenter.output = viewController
    
    let interactor = TableSceneInteractor()
    interactor.output = presenter
    
    viewController.output = interactor
    viewController.router = router
  }
  
}

TableSceneInteractor.swift

import UIKit

protocol TableSceneInteractorInput {
  func doSomething(request: TableSceneRequest)
}

protocol TableSceneInteractorOutput {
  func presentSomething(response: TableSceneResponse)
}

class TableSceneInteractor: TableSceneInteractorInput {
  
  var output: TableSceneInteractorOutput!
  var worker: TableSceneWorker!
  // MARK: Business logic
  func doSomething(request: TableSceneRequest) {
    // NOTE: Create some Worker to do the work
    worker = TableSceneWorker()
    worker.doSomeWork()
    // NOTE: Pass the result to the Presenter
    let response = TableSceneResponse()
    output.presentSomething(response: response)
  }
  
}

TableSceneModels.swift

import UIKit

struct TableSceneRequest {

}

struct TableSceneResponse {

}

struct TableSceneViewModel {

}

TableScenePresenter.swift

import UIKit

protocol TableScenePresenterInput {
  func presentSomething(response: TableSceneResponse)
}

protocol TableScenePresenterOutput: class {
  func displaySomething(viewModel: TableSceneViewModel)
}

class TableScenePresenter: TableScenePresenterInput {
  
  weak var output: TableScenePresenterOutput!
  // MARK: Presentation logic
  func presentSomething(response: TableSceneResponse) {
    // NOTE: Format the response from the Interactor and pass the result back to the View Controller
    let viewModel = TableSceneViewModel()
    output.displaySomething(viewModel: viewModel)
  }
  
}

TableSceneRouter.swift

import UIKit

protocol TableSceneRouterInput {
  func navigateToSomewhere()
}

class TableSceneRouter: TableSceneRouterInput {
  
  weak var viewController: TableSceneViewController!
  // MARK: Navigation
  func navigateToSomewhere() {
    // NOTE: Teach the router how to navigate to another scene. Some examples follow:
    
    // 1. Trigger a storyboard segue
    // viewController.performSegueWithIdentifier("ShowSomewhereScene", sender: nil)
    
    // 2. Present another view controller programmatically
    // viewController.presentViewController(someWhereViewController, animated: true, completion: nil)
    
    // 3. Ask the navigation controller to push another view controller onto the stack
    // viewController.navigationController?.pushViewController(someWhereViewController, animated: true)
    
    // 4. Present a view controller from a different storyboard
    // let storyboard = UIStoryboard(name: "OtherThanMain", bundle: nil)
    // let someWhereViewController = storyboard.instantiateInitialViewController() as! SomeWhereViewController
    // viewController.navigationController?.pushViewController(someWhereViewController, animated: true)
  }
  
  // MARK: Communication
  func passDataToNextScene(segue: UIStoryboardSegue) {
    // NOTE: Teach the router which scenes it can communicate with
    if segue.identifier == "ShowSomewhereScene" {
      passDataToSomewhereScene(segue: segue)
    }
  }
  
  func passDataToSomewhereScene(segue: UIStoryboardSegue) {
    // NOTE: Teach the router how to pass data to the next scene
    // let someWhereViewController = segue.destinationViewController as! SomeWhereViewController
    // someWhereViewController.output.name = viewController.output.name
  }
}

TableSceneViewController.swift

import UIKit

protocol TableSceneViewControllerInput {
  func displaySomething(viewModel: TableSceneViewModel)
}

protocol TableSceneViewControllerOutput {
  func doSomething(request: TableSceneRequest)
}

class TableSceneViewController: UITableViewController, TableSceneViewControllerInput {
  var output: TableSceneViewControllerOutput!
  var router: TableSceneRouter!
  // MARK: Object lifecycle
  override func awakeFromNib() {
    super.awakeFromNib()
    TableSceneConfigurator.sharedInstance.configure(viewController: self)
  }
  
  // MARK: View lifecycle
  override func viewDidLoad() {
    super.viewDidLoad()
    doSomethingOnLoad()
  }
  
  // MARK: Event handling
  func doSomethingOnLoad() {
    // NOTE: Ask the Interactor to do some work
    let request = TableSceneRequest()
    output.doSomething(request: request)
  }
  
  // MARK: Display logic
  func displaySomething(viewModel: TableSceneViewModel) {
    // NOTE: Display the result from the Presenter
    // nameTextField.text = viewModel.name
  }
  
}

TableSceneWorker.swiftt

import UIKit

class TableSceneWorker {
  // MARK: Business Logic
  func doSomeWork() {
    // NOTE: Do the work
  }
}

Теперь самое время позаботиться о сервисе, который возьмет на себя работу с YouTube Data API и вернет нам требуемый результат. Подробно на создании сервиса останавливаться не будем, так как это стандартная реализация в коде, к паттерну он практически не имеет отношения, он будет вызываться в интеракторе путем передачи управления в отдельный менеджер. Я под спойлером просто приведу готовую реализацию класса, которую мы и будем использовать. В любом случае, в конце статьи будет ссылка на GitHub с готовым проектов в этой части статьи, можно просто скачать или форкнуть проект, чтобы подробно во всем разобраться!

YoutubeManager.swift

import Foundation
import UIKit

class YoutubeManager {
  
  /// Синглтон для YoutubeManager
  static let sharedInstance = YoutubeManager()
  
  /**
   Получение массива с сущностями "Видео"
   - parameter playlistID: ID нашего плейлиста (не опциональная строка)
   */
  func getVideosForChannelWithPlaylistID(playlistID: String!, completion: (array: Array<VideoEntity>) -> Void) {
    let urlString = "https://www.googleapis.com/youtube/v3/playlistItems?part=snippet,contentDetails&maxResults=50&playlistId=(playlistID!)&key=(Config.GoogleDataKey)"
    let targetURL = URL(string: urlString)
    performGetRequest(targetURL: targetURL) { data, HTTPStatusCode, error -> Void in
      if HTTPStatusCode == 200 && error == nil {
        do {
          let resultsDict = try JSONSerialization.jsonObject(with: data! as Data, options: []) as! Dictionary<NSObject, AnyObject>
          let items: Array<Dictionary<NSObject, AnyObject>> = resultsDict["items"] as! Array<Dictionary<NSObject, AnyObject>>
          var array = Array<VideoEntity>()
          for i in 0 ..< items.count {
            let playlistSnippetDict = (items[i] as Dictionary<NSObject, AnyObject>)["snippet"] as! Dictionary<NSObject, AnyObject>
            if (playlistSnippetDict["thumbnails"] as? Dictionary<NSObject, AnyObject>) != nil {
              let publishedAt = playlistSnippetDict["publishedAt"] as! String!
              let title = playlistSnippetDict["title"] as! String!
              let description = playlistSnippetDict["description"] as! String!
              let videoID = (playlistSnippetDict["resourceId"] as! Dictionary<NSObject, AnyObject>)["videoId"] as! String!
              let thumbnail = ((playlistSnippetDict["thumbnails"] as! Dictionary<NSObject, AnyObject>)["default"] as! Dictionary<NSObject, AnyObject>)["url"] as! String!
              let videoItem = VideoEntity(publishedAt: publishedAt, title: title, description: description, videoID: videoID, thumbnail: thumbnail)
              array.append(videoItem)
            } else {
              continue
            }
          }
          completion(array: array)
        } catch {
          completion(array: [])
        }
      } else {completion(array: [])}
    }
  } // getVideosForChannelWithPlaylistID
  
} // class DataAPI

/// Helper for perform data request
extension YoutubeManager {
  /**
   Подготавливаем "GET" запрос к нашему YouTube сервису
   - parameter targetURL:  ссылка для запроса (NSURL!)
   - parameter completion: комплишен результата отработанного запроса
   */
  private func performGetRequest(targetURL: NSURL!, completion: (data: NSData?, HTTPStatusCode: Int, error: NSError?) -> Void) {
    let request = NSMutableURLRequest(url: targetURL! as URL)
    request.httpMethod = "GET"
    let sessionConfiguration = URLSessionConfiguration.default
    let session = URLSession(configuration: sessionConfiguration)
    let task = session.dataTask(with: request as URLRequest, completionHandler: { data, response, error -> Void in
      DispatchQueue.main.async(execute: { () -> Void in
        completion(data: data, HTTPStatusCode: (response as! HTTPURLResponse).statusCode, error: error)
      })
    })
    task.resume()
  } // performGetRequest
  
} // extension YoutubeManager

Вот так выглядит структура стандартного плейлиста, так что проблем вообще в принципе быть не должно

image

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

На этот раз мы унаследуемся от простого UIViewController и назовем: DetailedScene. Она будет использоваться для отображения и проигрывания видео из основного списка видео файлов.

Если все сделано правильно, структура проекта выглядит приблизительно так:

Структура проекта

image

А теперь займемся реализацией этих двух сцен. Самое интересное, что на реализацию у нас уйдет меньше времени, чем на саму подготовку проекта!

Конфигуратор нам трогать не нужно, там все уже сделано за нас. Нам остается лишь реализовать методы контроллера, интерактора, презентера, добавить пару строчек в модель и один сервисный метод в worker!

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

TableScene

TableSceneConfigurator.swift

import UIKit

extension TableSceneViewController: TableScenePresenterOutput {
  /// Переопределяем сегвей для контроллера
  override func prepare(for segue: UIStoryboardSegue, sender: AnyObject?) {
    router.passDataToNextScene(segue: segue)
  }
}

extension TableSceneInteractor: TableSceneViewControllerOutput {}
extension TableScenePresenter: TableSceneInteractorOutput {}

class TableSceneConfigurator {
  /// Настройка производится лишь один раз
  class var sharedInstance: TableSceneConfigurator {
    return TableSceneConfigurator()
  }
  
  /// Настройка и конфигурация контроллера
  func configure(viewController: TableSceneViewController) {
    /// Создаем роутер
    let router = TableSceneRouter()
    router.viewController = viewController
    /// Создаем презентер
    let presenter = TableScenePresenter()
    presenter.output = viewController
    /// Создаем интерактор
    let interactor = TableSceneInteractor()
    interactor.output = presenter
    /// Связываем контроллер с иницированными зависимостями
    viewController.output = interactor
    viewController.router = router
  }
  
}

TableSceneInteractor.swift

import UIKit

protocol TableSceneInteractorInput {
  func doRequest(request: TableSceneRequest)
  var videos: [VideoEntity]? { get }
}

protocol TableSceneInteractorOutput {
  func presentData(response: TableSceneResponse)
}

class TableSceneInteractor: TableSceneInteractorInput {
  
  var output: TableSceneInteractorOutput!
  var worker: TableSceneWorker!
  var videos: [VideoEntity]?
  
  /// Показали, что был послан запрос, запускаем сервис и обрабатываем результат
  func doRequest(request: TableSceneRequest) {
    worker = TableSceneWorker()
    worker.loadList { videos -> Void in
      self.videos = videos
      let response = TableSceneResponse(array: self.videos!)
      self.output.presentData(response: response)
    }
  }
  
}

TableSceneModels.swift

import UIKit

/// Модель данных, специфичная для данного контроллера

/// Общий запрос
struct TableSceneRequest {}
/// Формат ответа
struct TableSceneResponse {
  var array = Array<VideoEntity>()
}
/// Модель представления
struct TableSceneViewModel {
  var array = Array<VideoEntity>()
}

TableScenePresenter.swift

import UIKit

protocol TableScenePresenterInput {
  func presentData(response: TableSceneResponse)
}

protocol TableScenePresenterOutput: class {
  func displayData(viewModel: TableSceneViewModel)
}

class TableScenePresenter: TableScenePresenterInput {
  
  weak var output: TableScenePresenterOutput!
  
  /// Возвращаем полученные данные для отображения в контроллере
  func presentData(response: TableSceneResponse) {
    let viewModel = TableSceneViewModel(array: response.array)
    output.displayData(viewModel: viewModel)
  }
  
}

TableSceneRouter.swift

import UIKit

protocol TableSceneRouterInput {
  func navigateToNextController()
}

class TableSceneRouter: TableSceneRouterInput {
  
  weak var viewController: TableSceneViewController!
  
  /// Здесь можно произвести переход без использования сегвея
  func navigateToNextController() {
    
  }
  
  /// Передача данных в следующий контроллер через сегвей
  func passDataToNextScene(segue: UIStoryboardSegue) {
    /// Проверили сегвей, так как в одном роутере мы можем использовать несколько контроллеров для перехода и передачи данных
    if segue.identifier == "ShowDetailedScene" {
      if let selectedIndexPath = viewController.tableView.indexPathForSelectedRow {
        if let selectedVideo = viewController.output.videos?[(selectedIndexPath as NSIndexPath).row] {
          let detailedViewController = segue.destinationViewController as! DetailedSceneViewController
          detailedViewController.output.video = selectedVideo
        }
      }
    }
  }

}

TableSceneViewController.swift

import UIKit

protocol TableSceneViewControllerInput {
  func displayData(viewModel: TableSceneViewModel)
}

protocol TableSceneViewControllerOutput {
  func doRequest(request: TableSceneRequest)
  var videos: [VideoEntity]? { get }
}

class TableSceneViewController: UITableViewController, TableSceneViewControllerInput {
  
  var output: TableSceneViewControllerOutput!
  var router: TableSceneRouter!
  var videoArray: Array<VideoEntity>! = []
  
  /// Нстройка контроллера при старте
  override func awakeFromNib() {
    super.awakeFromNib()
    TableSceneConfigurator.sharedInstance.configure(viewController: self)
  }
  
  /// При полной загрузке делаем запрос данных
  override func viewDidLoad() {
    super.viewDidLoad()
    loadData()
  }
  
  /// Передаем запрос далее в интерактор
  func loadData() {
    let request = TableSceneRequest()
    output.doRequest(request: request)
  }
  
  /// Данные вернулись, их можно показать в контроллере
  func displayData(viewModel: TableSceneViewModel) {
    self.videoArray = viewModel.array
    tableView.reloadData()
  }
  
  // MARK: TableView DataSource
  
  override func numberOfSections(in tableView: UITableView) -> Int {
    return 1
  }
  
  override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return videoArray.count
  }
  
  override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let videoitem = videoArray[(indexPath as IndexPath).row]
    var cell = tableView.dequeueReusableCell(withIdentifier: "cell")
    if cell == nil {
      cell = UITableViewCell(style: .default, reuseIdentifier: "cell")
    }
    cell?.textLabel?.text = videoitem.title
    cell?.detailTextLabel?.text = videoitem.description
    return cell!
  }
  
}

TableSceneWorker.swift

import UIKit

class TableSceneWorker {
  
  /// Наш сервис обращается к менеджеру и позвращает результат в интерактор
  func loadList(callback: (videos: Array<VideoEntity>) -> Void) {
    let playlistID = VideoPlaylist()
    YoutubeManager.sharedInstance.getVideosForChannelWithPlaylistID(playlistID: playlistID) { array -> Void in
      callback(videos: array)
    }
  }
  
}

DetailedScene

DetailedSceneConfigurator.swift

import UIKit
import XCDYouTubeKit

extension DetailedSceneViewController: DetailedScenePresenterOutput {
  /// Переопределяем сегвей для контроллера
  override func prepare(for segue: UIStoryboardSegue, sender: AnyObject?) {
    router.passDataToNextScene(segue: segue)
  }
}

extension DetailedSceneInteractor: DetailedSceneViewControllerOutput {}
extension DetailedScenePresenter: DetailedSceneInteractorOutput {}

class DetailedSceneConfigurator {
  /// Настройка производится лишь один раз
  class var sharedInstance: DetailedSceneConfigurator {
    return DetailedSceneConfigurator()
  }
  
  /// Настройка и конфигурация контроллера
  func configure(viewController: DetailedSceneViewController) {
    /// Создаем роутер
    let router = DetailedSceneRouter()
    router.viewController = viewController
    /// Создаем презентер
    let presenter = DetailedScenePresenter()
    presenter.output = viewController
    /// Создаем интерактор
    let interactor = DetailedSceneInteractor()
    interactor.output = presenter
    /// Связываем контроллер с иницированными зависимостями
    viewController.output = interactor
    viewController.router = router
    /// Создаем плеер для последующей работы с ним
    viewController.videoPlayerViewController = XCDYouTubeVideoPlayerViewController()
  }
  
}

DetailedSceneInteractor.swift

import UIKit

protocol DetailedSceneInteractorInput {
  var video: VideoEntity! { get set }
  func getVideoID(request: DetailedSceneRequest)
}

protocol DetailedSceneInteractorOutput {
  func presentVideo(response: DetailedSceneResponse)
}

class DetailedSceneInteractor: DetailedSceneInteractorInput {
  
  var output: DetailedSceneInteractorOutput!
  var video: VideoEntity!
  
  /// Показали, что был послан запрос и обрабатываем результат
  func getVideoID(request: DetailedSceneRequest) {
    let response = DetailedSceneResponse(video: video)
    output.presentVideo(response: response)
  }
  
}

DetailedSceneModels.swift

import UIKit

/// Модель данных, специфичная для данного контроллера

/// Общий запрос
struct DetailedSceneRequest {}
/// Формат ответа
struct DetailedSceneResponse {
  var video: VideoEntity
}
/// Модель представления
struct DetailedSceneViewModel {
  var videoID: String!
}

DetailedScenePresenter.swift

import UIKit

protocol DetailedScenePresenterInput {
  func presentVideo(response: DetailedSceneResponse)
}

protocol DetailedScenePresenterOutput: class {
  func displayVideo(viewModel: DetailedSceneViewModel)
}

class DetailedScenePresenter: DetailedScenePresenterInput {
  
  weak var output: DetailedScenePresenterOutput!
  
  /// Возвращаем полученные данные для отображения в контроллере
  func presentVideo(response: DetailedSceneResponse) {
    let viewModel = DetailedSceneViewModel(videoID: response.video.videoID)
    output.displayVideo(viewModel: viewModel)
  }
  
}

DetailedSceneRouter.swift

import UIKit

protocol DetailedSceneRouterInput {
  func navigateToNextController()
}

class DetailedSceneRouter: DetailedSceneRouterInput {
  
  weak var viewController: DetailedSceneViewController!

  /// Здесь можно произвести переход без использования сегвея
  func navigateToNextController() {

  }
  
  /// Передача данных в следующий контроллер через сегвей
  func passDataToNextScene(segue: UIStoryboardSegue) {
    if segue.identifier == "OtherScene" {
      
    }
  }

}

DetailedSceneViewController.swift

import UIKit
import XCDYouTubeKit

protocol DetailedSceneViewControllerInput {
  func displayVideo(viewModel: DetailedSceneViewModel)
}

protocol DetailedSceneViewControllerOutput {
  var video: VideoEntity! { get set }
  func getVideoID(request: DetailedSceneRequest)
}

class DetailedSceneViewController: UIViewController, DetailedSceneViewControllerInput {
  
  @IBOutlet weak var videoContainerView: UIView!
  var videoPlayerViewController: XCDYouTubeVideoPlayerViewController!
  
  var output: DetailedSceneViewControllerOutput!
  var router: DetailedSceneRouter!
  
  /// Нстройка контроллера при старте
  override func awakeFromNib() {
    super.awakeFromNib()
    DetailedSceneConfigurator.sharedInstance.configure(viewController: self)
  }
  
  /// При полной загрузке делаем запрос данных
  override func viewDidLoad() {
    super.viewDidLoad()
    getVideoID()
  }
  
  /// Передаем запрос далее в интерактор
  func getVideoID() {
    let request = DetailedSceneRequest()
    output.getVideoID(request: request)
  }
  
  /// Данные вернулись, их можно показать в контроллере
  func displayVideo(viewModel: DetailedSceneViewModel) {
    videoPlayerViewController.present(in: videoContainerView)
    videoPlayerViewController.videoIdentifier = viewModel.videoID
    videoPlayerViewController.moviePlayer.prepareToPlay()
  }
  
}

DetailedSceneWorker.swift

import UIKit

class DetailedSceneWorker {
  
  /// Сервис оставили на всякий случай, если придется в контроллере произвести обработку или форматирование данных
  
}

Вот и весь проект! Паттерн легко вписался в приложение, код читается, не перегружен и в следующей главе мы для него напишем тесты.

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

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

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

P.S.: Ссылка на проект в GitHub: github.com/InstaRobot/CleanApp-Swift3

Автор: InstaRobot

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js