Приветствую уважаемых жителей Хабрахабра!
Не так давно я стал замечать, что мой код становится громоздким и даже в рамках одного контроллера мне все сложней удержать в голове то, что в нем происходит. Как следствие, на выходе не всегда ожидаемый результат, что я хотел реализовать, так как мозг “замылился” и я легко могу упустить существенную деталь. А после, ручной анализ кода, работа с отладчиком и так далее… Да что уж говорить, доходило до абсурда, при сборке приложения 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 и комментарии на страницах своего блога.
Коротко о паттерне
У Рэймонда свое видение чистой архитектуры. Как я понял, ему часто приходилось “фрилансить” и он искал решение быстрого и эффективного кода в своих приложениях, чтобы не сливать хорошие заказы, при этом минимизировать все тяготы общения с заказчиком.
Как он видит чистую архитектуру?
Вью -> Интерактор -> Презентер -> Вью. Роутинг у Рэймонда обособлен и достаточно универсален, позволяет использовать как связывание в коде, так и передачу данных через сегвеи.
Изначально я прошел мимо, не вникая в подробности, просто машинально отправил в закладки браузера, чтобы после почитать для саморазвития, не используя на практике. Но помог случай его попробовать. Откопал старый недоделанный проект для 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
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Комментарии (17)
rule
23.07.2016 03:52+1Не так давно я стал замечать, что мой код становится громоздким и даже в рамках одного контроллера мне все сложней удержать в голове то, что в нем происходит.
при сборке приложения xcode падал замертво и я даже не успевал понять, что случилось в приложении!
Ваши проблемы не решаются с помощью VIPER, я бы сказал, что они даже усугубляются.
Каждая команда разработчиков понимает его на свой лад и проповедует свою реализацию, как единственно правильную и заслуживающую право на жизнь.
Шаблон программироования (то что вы паттерном называете) — это не правильно определение VIPER. VIPER это более обширное понятие — это принцип, который проще всего реализовать определенным набором шаблонов проектирования/программирования: decoupling, facade, delegation, incapsulation, dependency injection… Но этот список не фиксированный, вы можете на свой вкус выбирать. Так-же вайпер в себя включает ряд шаблонов проектирования: Single Responsobility, YAGNI, Open/Close… По идее VIPER должен удовлетворять набору принципов SOLID. Но тоже не обязательно.
К чему это я. К тому, что это всё не обязательно, так как у VIPER есть основная задача: разделить озоны влияния куска вашей логики так, чтоб его можно было тестировать. То-есть это принцип структурный. Но это увеличивает количество файлов, поэтому увеличивает количество кода, время компиляции и сводит с ума Xcode сильнее.rule
23.07.2016 03:55А Если ваш ViewController перегружен — это скорее всего что у вас нарушен принцип Single Responsobility — Вью контроллер выполняет сильно много ролей.
rule
23.07.2016 03:57Каждая команда разработчиков понимает его на свой лад и проповедует свою реализацию, как единственно правильную и заслуживающую право на жизнь.
В связи с вышеописанным, не бывает правильной/неправильной. Код либо соответствует принципам VIPER, либо нет. Причем могут быть нарушены некоторые детали, ради упрощения. То что у разных команд разные подходы и реализации — это нормально, и я сомневаюсь что они считают это «единственным правильным». Главное чтоб внутри команды был единый стандарт, а не «кто в лес кто по дрова».
Whiskas333
23.07.2016 10:30Как по мне так это старый добрый Model View Controller. :)
InstaRobot
23.07.2016 11:02Возможно так оно и есть! Но элементы «чистой» в нем так же есть. Мне его сложно классифицировать. Я просто взял за определение слова Рэймонда
TheIseAse
23.07.2016 17:29Честно говоря, ничего не понял.
> Так как кода действительно мало, то можно просто дать готовую реализацию под спойлером
Нельзя. Вы же пишете статью для людей. Вы пытаетесь донести какую-то свою мысль. Зачем заставлять их тратить свое время на то, чтоб искать отличия между двумя сценами? Почему нельзя потратить один абзац текста, где объяснить подход, проиллюстрировав его несколькими кусочками кода? Да, ответите вы, это сделано на сайте clean-swift.com, но зачем тогда нужна ваша статья?
Все привыкли к stackoverflow, который открываешь, и за секунды видишь полезный код, который читаешь, копируешь, используешь. Это эффективно. А тут нужно выискивать крупицы мудрости среди шаблонного кода.
InstaRobot
23.07.2016 17:39Статья получилось достаточно объемная, вот и пришлось сворачиваться! Что именно Вам непонятно? Как составляющие сцены общаются между собой? Или что то иное?
TheIseAse
23.07.2016 19:06-1Если статья объемная, то значит, нужно разбивать на несколько статей :) Ничего личного, я тоже не знаю, как это лучше структурировать, но я и статьи не пишу.
Мне непонятна мысль, которую вы хотите донести. В чем заключается «чистая» архитектура? Я согласился с тем, почему традиционный подход — это плохо, но я не знаю, чем ваш лучше, и в чем он состоит.InstaRobot
23.07.2016 19:16Ну это не мой, а Рэймонда). Тут четко разделяются ответственности, меньше возможностей «размазать» код. Я бы тоже его с трудом отнес к классике чистой архитектуры, но пара элементов присутствует и от нее.
Вот классика от UncleBob: https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html
А вот уже VIPER: https://www.objc.io/issues/13-architecture/viper/
В любом случае, у Рэймонда есть и интерактор, и презентер, просто он изменил принципы взаимодействия между ними
kostyl
23.07.2016 18:52+1Вообщем, автор swiftclean не обьясняет толком и с расстановкой точек над чем ему не понравился VIPER, он просто берет и делает как он может, а не как правильнее.
InstaRobot
23.07.2016 19:07Ну с 15-м опытом разработки, наверное он делает как считает сам правильным, а не как может. В любом случае, у него упор на скорость кода и покрытия тестами, а не то, что ему VIPER не понравился! VIPER хорошая вещь, но уж слишком много приходится кода писать.
Никто не говорит, что нужно использовать шаблоны Рэймонда вместо VIPER, я же написал, что он также заслуживает право на жизнь. К примеру, пару дней назад, я реанимировал старое приложение, мне его нужно было переписать с Obj-C на Swift. Оно достаточно простое, не имеет много модулей. VIPER там вряд ли понадобится, А вот CleanSwift очень даже хорошо применяется. Создал пару сцен, расписал все зависимости и готово!kostyl
23.07.2016 19:37Кода почти столько же надо писать что у него что на VIPER, ну ладно я понял вас, его я не понял
Dr_Zoidberg
23.07.2016 19:13Спасибо за интересный материал. Все таки без знания VIPER у вас нифига не понятно что и как все эти классы делают. Как будет время скачаю сорцы и изучу подробно, но имхо стоило отобразить это в тексте для тех кто в танке.
InstaRobot
23.07.2016 19:19Замечание принял, во второй части я вернуть к этой теме и постараюсь расписать более подробно, что, как и почему работает! Но и это не VIPER )
InstaRobot
Сразу извинюсь перед сообществом. На комментарии отвечу позднее, мне нужно срочно отлучиться, а компьютера под рукой не будет!