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
Какие паттерны Вы используете в разработке?

Проголосовало 85 человек. Воздержалось 53 человека.

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

Поделиться с друзьями
-->

Комментарии (17)


  1. InstaRobot
    22.07.2016 17:35

    Сразу извинюсь перед сообществом. На комментарии отвечу позднее, мне нужно срочно отлучиться, а компьютера под рукой не будет!


  1. 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 сильнее.


    1. rule
      23.07.2016 03:55

      А Если ваш ViewController перегружен — это скорее всего что у вас нарушен принцип Single Responsobility — Вью контроллер выполняет сильно много ролей.


    1. rule
      23.07.2016 03:57

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


      В связи с вышеописанным, не бывает правильной/неправильной. Код либо соответствует принципам VIPER, либо нет. Причем могут быть нарушены некоторые детали, ради упрощения. То что у разных команд разные подходы и реализации — это нормально, и я сомневаюсь что они считают это «единственным правильным». Главное чтоб внутри команды был единый стандарт, а не «кто в лес кто по дрова».


      1. InstaRobot
        23.07.2016 10:44

        Спасибо за комментарии. Учту на будущее


  1. Whiskas333
    23.07.2016 10:30

    Как по мне так это старый добрый Model View Controller. :)


    1. InstaRobot
      23.07.2016 11:02

      Возможно так оно и есть! Но элементы «чистой» в нем так же есть. Мне его сложно классифицировать. Я просто взял за определение слова Рэймонда


  1. TheIseAse
    23.07.2016 17:29

    Честно говоря, ничего не понял.
    > Так как кода действительно мало, то можно просто дать готовую реализацию под спойлером
    Нельзя. Вы же пишете статью для людей. Вы пытаетесь донести какую-то свою мысль. Зачем заставлять их тратить свое время на то, чтоб искать отличия между двумя сценами? Почему нельзя потратить один абзац текста, где объяснить подход, проиллюстрировав его несколькими кусочками кода? Да, ответите вы, это сделано на сайте clean-swift.com, но зачем тогда нужна ваша статья?

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


    1. InstaRobot
      23.07.2016 17:39

      Статья получилось достаточно объемная, вот и пришлось сворачиваться! Что именно Вам непонятно? Как составляющие сцены общаются между собой? Или что то иное?


      1. TheIseAse
        23.07.2016 19:06
        -1

        Если статья объемная, то значит, нужно разбивать на несколько статей :) Ничего личного, я тоже не знаю, как это лучше структурировать, но я и статьи не пишу.

        Мне непонятна мысль, которую вы хотите донести. В чем заключается «чистая» архитектура? Я согласился с тем, почему традиционный подход — это плохо, но я не знаю, чем ваш лучше, и в чем он состоит.


        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/

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


  1. kostyl
    23.07.2016 18:52
    +1

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


    1. InstaRobot
      23.07.2016 19:07

      Ну с 15-м опытом разработки, наверное он делает как считает сам правильным, а не как может. В любом случае, у него упор на скорость кода и покрытия тестами, а не то, что ему VIPER не понравился! VIPER хорошая вещь, но уж слишком много приходится кода писать.

      Никто не говорит, что нужно использовать шаблоны Рэймонда вместо VIPER, я же написал, что он также заслуживает право на жизнь. К примеру, пару дней назад, я реанимировал старое приложение, мне его нужно было переписать с Obj-C на Swift. Оно достаточно простое, не имеет много модулей. VIPER там вряд ли понадобится, А вот CleanSwift очень даже хорошо применяется. Создал пару сцен, расписал все зависимости и готово!


      1. InstaRobot
        23.07.2016 19:18

        *С 15 летним хотел написать


      1. kostyl
        23.07.2016 19:37

        Кода почти столько же надо писать что у него что на VIPER, ну ладно я понял вас, его я не понял


  1. Dr_Zoidberg
    23.07.2016 19:13

    Спасибо за интересный материал. Все таки без знания VIPER у вас нифига не понятно что и как все эти классы делают. Как будет время скачаю сорцы и изучу подробно, но имхо стоило отобразить это в тексте для тех кто в танке.


    1. InstaRobot
      23.07.2016 19:19

      Замечание принял, во второй части я вернуть к этой теме и постараюсь расписать более подробно, что, как и почему работает! Но и это не VIPER )