Приветствую уважаемых жителей Хабрахабра!
Не так давно я стал замечать, что мой код становится громоздким и даже в рамках одного контроллера мне все сложней удержать в голове то, что в нем происходит. Как следствие, на выходе не всегда ожидаемый результат, что я хотел реализовать, так как
Кому интересен вопрос архитектуры приложения, добро пожаловать под кат!
На Swift я перешел не так давно, потому казалось, что в данном языке, априори невозможны подобные реализации. А оказалось, что поговорка про плохого танцора все же имеет ко мне непосредственное отношение. Кстати, за переход на Swift я благодарен лично Ивану Акулову со swiftbook.ru, так как у меня был какой-то психологический барьер на изучение Swift, до версии 2.0 даже не пытался ковырять его, слишком все сырым казалось, да и Objective-C казался вполне легким и логичным. Так было до первой реализации моего первого свифт-приложения, теперь мне сложно при необходимости настроиться писать на “старичке”.
В этой же статье я буду использовать версию языка 3.0, так как массовый переход на него уже осенью, лучше сразу привыкать и освоить все основные “фишки” языка. В чем отличия, писать не буду, буду просто писать код примеров на этой версии. Для разработчика не составит труда самому разобраться с данным вопросом.
Итак, не будем сильно отвлекаться и вернемся к нашим баранам. Когда я решил всерьез улучшить качество кода, то первым делом подумал за VIPER, так как регулярно посещал все тусовки разработчиков в Рамблер и теории нахватался достаточно. Стоит заметить, что в Рамблер поощряют использование их наработок и охотно консультируют по всем сложным вопросам. Они в реальности фанатеют от таких вещей как: VIPER, TDD, Typhoon и слушать их доклады сплошное удовольствие, но остается маленькое но… Это все теория для слушателей! Нужно брать и писать код в реальности, а не виртуально обсуждать все сильные и слабые стороны паттерна. Особенно смущал тот факт, что Рамблер использует свиззлинг в роутинге, это как то размывало классическое определение паттерна. Что не так еще с VIPER? Существует множество модификаций паттерна, нет единого толкования, определения и практик использования. Каждая команда разработчиков понимает его на свой лад и проповедует свою реализацию, как единственно правильную и заслуживающую право на жизнь. В то время, когда я пытался понять, как же правильно использовать VIPER, мне довелось увидеть столько различных его модификаций, что это еще больше запутало меня и все усложнило!
А хотелось взять за основу реально правильный подход, чтобы не ломать свой код, если окажется, что я делал неправильно.
Тогда и решил расширить поиск источников, очень сильно выручает знание английского языка, так как в нашем сегменте сети лучше не искать стоящей информации. Есть мнение, что у нас основной контент создают школьники, которые своими публикациями и роликами на youtube хотят подчеркнуть свою значимость и похвастаться перед друзьями. Исключение лишь Хабр и еще пара ресурсов! Но и на Хабре очень мало информации для практики, в основном обсуждается теория, подходы к реализации и так далее.
Так мы искали VIPER, а что нашли?!
А нашли мы блог одного хорошего человека, который серьезно продвигает свое видение чистой архитектуры. Да, это модификация и некоторые громоздкие элементы им были выкинуты, но сделано очень качественно, применимо к различным способам и особенностям реализации приложений и самое главное, я впервые увидел как на практике расписали и показали TDD.
Итак, кто владеет английским, могут почитать в оригинале: clean-swift.com. Зовут разработчика Рэймонд и он активно поддерживает связь со своими читателями через email и комментарии на страницах своего блога.
Коротко о паттерне
У Рэймонда свое видение чистой архитектуры. Как я понял, ему часто приходилось “фрилансить” и он искал решение быстрого и эффективного кода в своих приложениях, чтобы не сливать хорошие заказы, при этом минимизировать все тяготы общения с заказчиком.
Как он видит чистую архитектуру?
Вью -> Интерактор -> Презентер -> Вью. Роутинг у Рэймонда обособлен и достаточно универсален, позволяет использовать как связывание в коде, так и передачу данных через сегвеи.
Изначально я прошел мимо, не вникая в подробности, просто машинально отправил в закладки браузера, чтобы после почитать для саморазвития, не используя на практике. Но помог случай его попробовать. Откопал старый недоделанный проект для 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 и создания ключа.
Под спойлером изображения, как все должно происходить.
После того, как у нас есть ключ, заимплементим его в файле Config.swift. Я вообще предпочитаю конфиги приложения и хелперы хранить в отдельной области проекта, но решение остается за вами. Самое главное — это ключ! При регистрации ключа, ни в коем случае не указываем бандл приложения, иначе YouTube начнет фильтровать запросы не в вашу пользу, почему то у Google это криво реализовано.
Что дальше?
Для облегчения работы, мы используем шаблоны, Рэймонд прислал увиверсальные шаблоны, включая версию и для Objective-C, на случай если кто-либо захочет попробовать подобную реализацию.
Архив с шаблонами, доступен для загрузки на странице проекта GitHub, стоит посмотреть папку “Extended”. Скачиваем шаблоны и через терминал переходим в папку с распакованными шаблонами. Далее простая команда “make install_templates” и шаблоны установлены. Их можно использовать для работы.
Давайте попробуем создать нашу первую сцену для таблицы с будущим списком видео из плейлиста. Воспользуемся для этого примером из видео и наследуемся от UITableViewController. С названием также особо мудрить не будем, пусть производное имя для сцены будет: TableScene, остальные названия шаблон сгенерирует сам.
Если вы все сделали правильно, то у вас должна получиться приблизительно такая картина, как на изображении под спойлером и базовая компоновка в коде для сцены. Примеры базовой компоновки классов также можно посмотреть под следующими спойлерами.
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
}
}
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)
}
}
import UIKit
struct TableSceneRequest {
}
struct TableSceneResponse {
}
struct TableSceneViewModel {
}
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)
}
}
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
}
}
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
}
}
import UIKit
class TableSceneWorker {
// MARK: Business Logic
func doSomeWork() {
// NOTE: Do the work
}
}
Теперь самое время позаботиться о сервисе, который возьмет на себя работу с YouTube Data API и вернет нам требуемый результат. Подробно на создании сервиса останавливаться не будем, так как это стандартная реализация в коде, к паттерну он практически не имеет отношения, он будет вызываться в интеракторе путем передачи управления в отдельный менеджер. Я под спойлером просто приведу готовую реализацию класса, которую мы и будем использовать. В любом случае, в конце статьи будет ссылка на GitHub с готовым проектов в этой части статьи, можно просто скачать или форкнуть проект, чтобы подробно во всем разобраться!
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
Чтобы не отплекаться после на создание второй сцены, давайте сделаем это сразу, а после дефолтные методы реализации сразу перепишем в обеих сценах.
На этот раз мы унаследуемся от простого UIViewController и назовем: DetailedScene. Она будет использоваться для отображения и проигрывания видео из основного списка видео файлов.
Если все сделано правильно, структура проекта выглядит приблизительно так:
А теперь займемся реализацией этих двух сцен. Самое интересное, что на реализацию у нас уйдет меньше времени, чем на саму подготовку проекта!
Конфигуратор нам трогать не нужно, там все уже сделано за нас. Нам остается лишь реализовать методы контроллера, интерактора, презентера, добавить пару строчек в модель и один сервисный метод в worker!
Так как кода действительно мало, то можно просто дать готовую реализацию под спойлером, код прокомментирован, а участки без комментариев будут понятны даже начинающим разработчикам.
TableScene
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
}
}
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)
}
}
}
import UIKit
/// Модель данных, специфичная для данного контроллера
/// Общий запрос
struct TableSceneRequest {}
/// Формат ответа
struct TableSceneResponse {
var array = Array<VideoEntity>()
}
/// Модель представления
struct TableSceneViewModel {
var array = Array<VideoEntity>()
}
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)
}
}
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
}
}
}
}
}
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!
}
}
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
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()
}
}
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)
}
}
import UIKit
/// Модель данных, специфичная для данного контроллера
/// Общий запрос
struct DetailedSceneRequest {}
/// Формат ответа
struct DetailedSceneResponse {
var video: VideoEntity
}
/// Модель представления
struct DetailedSceneViewModel {
var videoID: String!
}
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)
}
}
import UIKit
protocol DetailedSceneRouterInput {
func navigateToNextController()
}
class DetailedSceneRouter: DetailedSceneRouterInput {
weak var viewController: DetailedSceneViewController!
/// Здесь можно произвести переход без использования сегвея
func navigateToNextController() {
}
/// Передача данных в следующий контроллер через сегвей
func passDataToNextScene(segue: UIStoryboardSegue) {
if segue.identifier == "OtherScene" {
}
}
}
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()
}
}
import UIKit
class DetailedSceneWorker {
/// Сервис оставили на всякий случай, если придется в контроллере произвести обработку или форматирование данных
}
Вот и весь проект! Паттерн легко вписался в приложение, код читается, не перегружен и в следующей главе мы для него напишем тесты.
Единственное, что осталось за кадром, особо внимательные могут заметить использование сторонней библиотеки во второй сцене для проигрывания видео с YouTube.
Жаль, что такую обширную тему приходится так сухо излагать, но если затрагивать все аспекты, при этом комментировать причинно-следственное связи, то возможно не уложусь и в цикл из 40 статей.
Для опытных разработчиков, статья возможно и не дает нового материала, основная причина публикации, показать «молодым» на практике возможность использования достаточно интересного шаблона.
P.S.: Ссылка на проект в GitHub: github.com/InstaRobot/CleanApp-Swift3
Автор: InstaRobot