<repo_name>fgulan/ios-infinum-academy-2019<file_sep>/TVShows/Modules/ShowDetails/ShowDetailsViewController.swift
//
//  ShowDetailsViewController.swift
//  TVShows
//
//  Created by <NAME> on 30/07/2018.
//  Copyright (c) 2018 <NAME>. All rights reserved.
//
//  This file was generated by the 🐍 VIPER generator
//

import UIKit
import RxSwift
import RxCocoa

final class ShowDetailsViewController: UIViewController {

    private let disposeBag = DisposeBag()

    // MARK: - Public properties -

    var presenter: ShowDetailsPresenterInterface!

    // MARK: - Lifecycle -

    override func viewDidLoad() {
        super.viewDidLoad()
        setupView()
    }
	
}

// MARK: - Extensions -

extension ShowDetailsViewController: ShowDetailsViewInterface {
}

private extension ShowDetailsViewController {

    func setupView() {
        let output = ShowDetailsViewOutput()
        _ = presenter.setup(with: output)
    }

}
<file_sep>/TVShows/Common/Networking/Routers/ShowRouter.swift
//
//  HomeRouter.swift
//  TVShows
//
//  Created by <NAME> on 29/07/2018.
//  Copyright © 2018 <NAME>. All rights reserved.
//

import Foundation
import Alamofire

extension Router {
    
    enum Show {
        
        static func getShows() -> Router {
            return Router(
                path: "/shows",
                method: .get,
                encoding: JSONEncoding.default
            )
        }
    }
}
<file_sep>/TVShows/Modules/Home/Cell/TVShowTableViewCell.swift
//
//  TVShowTableViewCell.swift
//  TVShows
//
//  Created by <NAME> on 24/07/2018.
//  Copyright © 2018 <NAME>. All rights reserved.
//

import UIKit
import Kingfisher

class TVShowTableViewCell: UITableViewCell {
    
    @IBOutlet private weak var _titleLabel: UILabel!
    @IBOutlet private weak var _imageView: UIImageView!
    
    override func awakeFromNib() {
        super.awakeFromNib()
        _setupImageView()
    }

    func configure(with item: Show) {
        _titleLabel.text = item.title
        if let imageUrl = item.imageUrl {
            let url = URL(string: "https://api.infinum.academy/")?
                        .appendingPathComponent(imageUrl)
            _imageView.kf.setImage(with: url)
        } else {
            _imageView.image = nil
        }
    }
    
    // MARK: - Private functions -
    
    private func _setupImageView() {
        _imageView.layer.shadowColor = UIColor.black.cgColor
        _imageView.layer.shadowOffset = CGSize(width: 0, height: 1)
        _imageView.layer.shadowOpacity = 1
        _imageView.layer.shadowRadius = 2.0
        _imageView.clipsToBounds = false
    }
}
<file_sep>/TVShows/Modules/Home/HomeInterfaces.swift
//
//  HomeInterfaces.swift
//  TVShows
//
//  Created by <NAME> on 29/07/2018.
//  Copyright (c) 2018 <NAME>. All rights reserved.
//
//  This file was generated by the 🐍 VIPER generator
//

import UIKit
import RxSwift
import RxCocoa

enum Home {
    
    struct ViewOutput {
        let refresh: Driver<Void>
        let logoutPressed: Driver<Void>
    }
    
    struct ViewInput {
        let items: Driver<[CollectionCellItem]>
    }
    
    enum NavigationOption {
        case showDetails(showId: String)
    }
}


protocol HomeWireframeInterface: WireframeInterface {
    func navigate(to option: Home.NavigationOption)
}

protocol HomeViewInterface: ViewInterface, Refreshable {
}

protocol HomePresenterInterface: PresenterInterface {
    func setup(with output: Home.ViewOutput) -> Home.ViewInput
}

protocol HomeInteractorInterface: InteractorInterface {
    
    func loadShows() -> Single<[Show]>
}
<file_sep>/TVShows/Modules/ShowDetails/ShowDetailsInterfaces.swift
//
//  ShowDetailsInterfaces.swift
//  TVShows
//
//  Created by <NAME> on 30/07/2018.
//  Copyright (c) 2018 <NAME>. All rights reserved.
//
//  This file was generated by the 🐍 VIPER generator
//

import UIKit
import RxSwift
import RxCocoa

enum ShowDetailsNavigationOption {
}

protocol ShowDetailsWireframeInterface: WireframeInterface {
    func navigate(to option: ShowDetailsNavigationOption)
}

protocol ShowDetailsViewInterface: ViewInterface {
}

protocol ShowDetailsPresenterInterface: PresenterInterface {
    func setup(with output: ShowDetailsViewOutput) -> ShowDetailsViewInput
}

protocol ShowDetailsInteractorInterface: InteractorInterface {
}

struct ShowDetailsInput {
}

struct ShowDetailsViewInput {
}

struct ShowDetailsViewOutput {
}
<file_sep>/TVShows/Application/Initializers/LoggieInitializer.swift
//
//  LoggieInitializer.swift
//  TVShows
//
//  Created by <NAME> on 28/07/2018.
//  Copyright © 2018 <NAME>. All rights reserved.
//

import Foundation
#if DEV
import Loggie
#endif

final class LoggieInitializer: Initializable {
    
    func initialize() {
        #if DEV
            URLProtocol.registerClass(LoggieURLProtocol.self)
        #endif
    }
}
<file_sep>/TVShows/Common/Networking/Services/ShowsSessionManager.swift
//
//  ShowsSessionManager.swift
//  TVShows
//
//  Created by <NAME> on 29/07/2018.
//  Copyright © 2018 <NAME>. All rights reserved.
//

import Foundation
import Alamofire
import RxCocoa
import RxSwift
#if DEV
import Loggie
#endif

class ShowsSessionManager {
    
    static var session: SessionManager = {
        #if DEV
            let configuration = URLSessionConfiguration.loggie
            configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
            return SessionManager(configuration: configuration)
        #else
            let configuration = URLSessionConfiguration.default
            configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
            return SessionManager(configuration: configuration)
        #endif
    }()
    
    private init() {}
}

class TokenAdapter: RequestAdapter {
    
    private let _token: String
    
    init(token: String) {
        _token = token
    }
    
    func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
        var urlRequest = urlRequest
        urlRequest.setValue(_token, forHTTPHeaderField: "Authorization")
        return urlRequest
    }
}
<file_sep>/TVShows/Modules/ShowDetails/ShowDetailsPresenter.swift
//
//  ShowDetailsPresenter.swift
//  TVShows
//
//  Created by <NAME> on 30/07/2018.
//  Copyright (c) 2018 <NAME>. All rights reserved.
//
//  This file was generated by the 🐍 VIPER generator
//

import UIKit
import RxSwift
import RxCocoa

final class ShowDetailsPresenter {

    // MARK: - Private properties -

    private unowned let view: ShowDetailsViewInterface
    private let interactor: ShowDetailsInteractorInterface
    private let wireframe: ShowDetailsWireframeInterface

    // MARK: - Lifecycle -

    init(view: ShowDetailsViewInterface, interactor: ShowDetailsInteractorInterface, wireframe: ShowDetailsWireframeInterface) {
        self.view = view
        self.interactor = interactor
        self.wireframe = wireframe
    }
}

// MARK: - Extensions -

extension ShowDetailsPresenter: ShowDetailsPresenterInterface {

    func setup(with output: ShowDetailsViewOutput) -> ShowDetailsViewInput {
        return ShowDetailsViewInput()
    }

}

<file_sep>/TVShows/Common/UI/Controller/ShowsViewController.swift
//
//  ShowsViewController.swift
//  TVShows
//
//  Created by <NAME> on 28/07/2018.
//  Copyright © 2018 <NAME>. All rights reserved.
//

import UIKit
import RxSwift

class ShowsViewController: UIViewController {

    #if DEBUG
        private let startResourceCount = Resources.total
    #endif
    
    // MARK: - Lifecycle -
    
    override func viewDidLoad() {
        super.viewDidLoad()
        #if DEBUG
            let logString = "⚠️ Number of start resources = \(Resources.total) ⚠️"
            log.info(logString)
        #endif
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        navigationController?.setNavigationBarHidden(false, animated: animated)
    }
    
    override var preferredStatusBarStyle: UIStatusBarStyle {
        return .`default`
    }
    
    deinit {
        #if DEBUG
        
        let mainQueue = DispatchQueue.main
        let when = DispatchTime.now() + DispatchTimeInterval.milliseconds(300)
        
        mainQueue.asyncAfter(deadline: when) {
            let logString = "⚠️ Number of resources after dispose = \(Resources.total) ⚠️"
            log.info(logString)
        }
        
        /*
         !!! This cleanup logic is adapted for example app use case. !!!
         
         It is being used to detect memory leaks during pre release tests.
         
         !!! In case you want to have some resource leak detection logic, the simplest
         method is just printing out `RxSwift.Resources.total` periodically to output. !!!
         
         
         /* add somewhere in
         func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool {
         */
         _ = Observable<Int>.interval(1, scheduler: MainScheduler.instance)
         .subscribe(onNext: { _ in
         print("Resource count \(RxSwift.Resources.total)")
         })
         
         Most efficient way to test for memory leaks is:
         * navigate to your screen and use it
         * navigate back
         * observe initial resource count
         * navigate second time to your screen and use it
         * navigate back
         * observe final resource count
         
         In case there is a difference in resource count between initial and final resource counts, there might be a memory
         leak somewhere.
         
         The reason why 2 navigations are suggested is because first navigation forces loading of lazy resources.
         */
        
        #endif
    }
}
<file_sep>/TVShows/Common/Extensions/Rx/DataRequest+Rx.swift
//
//  DataRequest+Rx.swift
//  TVShows
//
//  Created by <NAME> on 29/07/2018.
//  Copyright © 2018 <NAME>. All rights reserved.
//

import Foundation
import RxSwift
import Alamofire
import CodableAlamofire


extension DataRequest: ReactiveCompatible {}

extension Reactive where Base == DataRequest {
    
    func responseDecodableObject<T: Decodable>(
        queue: DispatchQueue? = nil,
        keyPath: String? = nil,
        decoder: JSONDecoder = JSONDecoder())
        -> Single<T> {
            
            return Single<T>.create { [weak base] (single) -> Disposable in
                
                let request = base?.responseDecodableObject(
                    queue: queue,
                    keyPath: keyPath,
                    decoder: decoder,
                    completionHandler: { (response: DataResponse<T>) in
                        switch response.result {
                        case .success(let value): single(.success(value))
                        case .failure(let error): single(.error(error))
                        }
                })
                
                return Disposables.create {
                    request?.cancel()
                }
            }
    }
    
    func response(queue: DispatchQueue? = nil) -> Completable {
        return Completable.create(subscribe: { [weak base] (completable) -> Disposable in
            let request = base?.response(
                queue: queue,
                completionHandler: { (response) in
                    if let error = response.error {
                        completable(.error(error))
                    } else {
                        completable(.completed)
                    }
            })
            
            return Disposables.create {
                request?.cancel()
            }
        })
    }
}

extension SessionManager: ReactiveCompatible {}
extension Reactive where Base == SessionManager {
    
    var manager: Single<SessionManager> {
        return Single.just(base)
    }
    
}
<file_sep>/Podfile
platform :ios, '11.0'
inhibit_all_warnings!
use_frameworks!

def reactive
    pod 'RxSwift'
    pod 'RxCocoa'
end

def networking
    pod 'Alamofire'
    pod 'AlamofireNetworkActivityIndicator'
    pod 'AlamofireNetworkActivityLogger'
    pod 'Kingfisher'
    pod 'Loggie'
    pod 'CodableAlamofire'
end

def ui
    pod 'SnapKit'
    pod 'SwiftI18n/I18n+Case'
    pod 'PKHUD'
    pod 'AHKNavigationController'
end

def other
    pod 'SwiftyBeaver'
end

def shared
    reactive
    networking
    ui
    other
end

target 'TVShows' do
    shared
end

post_install do |installer|
    installer.pods_project.targets.each do |target|
        if target.name == 'RxSwift'
            target.build_configurations.each do |config|
                if config.name == 'AppStore_Debug' || config.name == 'Development_Debug'
                    config.build_settings['OTHER_SWIFT_FLAGS'] ||= ['$(inherited)', '-D', 'TRACE_RESOURCES']
                end
            end
        end
    end
end
<file_sep>/TVShows/Common/UI/Keyboard/KeyboardHandler.swift
//
//  KeyboardHandler.swift
//  TVShows
//
//  Created by <NAME> on 29/07/2018.
//  Copyright © 2018 <NAME>. All rights reserved.
//

import UIKit
import RxSwift
import RxCocoa

class KeyboardHandler {
    
    private init() {}
    
    struct PresentingParams {
        let keyboardSize: CGSize
        let animationDuration: Double
        let state: State
        enum State {
            case show
            case hide
        }
    }
    
    enum Insets {
        case forShown(inset: CGFloat)
        case forHidden(inset: CGFloat)
        case always(inset: CGFloat)
        case never
        
        func add(to value: CGFloat, with state: PresentingParams.State) -> CGFloat {
            switch (self, state) {
            case (.always(let inset), _):
                return value + inset
            case (.forShown(let inset), .show):
                return value + inset
            case (.forHidden(let inset), .hide):
                return value + inset
            default:
                return value
            }
        }
    }
    
}

extension KeyboardHandler {
    
    static private(set) var keyboardPresenting: Observable<PresentingParams> = {
        
        let show = NotificationCenter.default.rx.notification(UIResponder.keyboardWillShowNotification).map { (notification) -> PresentingParams? in
            guard let userInfo = notification.userInfo,
                let keyboardSize = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue,
                let animationDuration = ((userInfo[UIResponder.keyboardAnimationDurationUserInfoKey]) as? NSNumber)?.doubleValue
            else { return nil }
            return PresentingParams(
                keyboardSize: keyboardSize.size,
                animationDuration: animationDuration,
                state: .show
            )
        }
        
        let hide = NotificationCenter.default.rx.notification(UIResponder.keyboardWillHideNotification).map { (notification) -> PresentingParams? in
            guard let userInfo = notification.userInfo,
                let animationDuration = ((userInfo[UIResponder.keyboardAnimationDurationUserInfoKey]) as? NSNumber)?.doubleValue
            else { return nil }
            return PresentingParams(
                keyboardSize: CGSize.zero,
                animationDuration: animationDuration,
                state: .hide
            )
            
        }
        
        return Observable
            .merge([show, hide])
            .share(replay: 1, scope: .forever)
            .compactMap { $0 }
    }()
    
    static func registerDidShow(animatedHandler: @escaping ()->()) -> Disposable {
        return NotificationCenter.default.rx
            .notification(UIResponder.keyboardDidShowNotification)
            .observeOn(MainScheduler.instance)
            .subscribe(onNext: { _ in animatedHandler() })
    }
    
    static func register(handler: @escaping (_ params: PresentingParams) -> ()) -> Disposable {
        return keyboardPresenting
            .observeOn(MainScheduler.instance)
            .subscribe(onNext: {  handler($0) })
    }
    
    static func register(animatedHandler: @escaping (_ params: PresentingParams) -> ()) -> Disposable {
        return KeyboardHandler.register(handler: { (params) in
            UIView.animate(withDuration: params.animationDuration, animations: {
                animatedHandler(params)
            })
        })
    }
    
    static func register(scrollView: UIScrollView, additionalInsets: Insets = .never) -> Disposable {
        
        return KeyboardHandler.register(animatedHandler: { (params) in
            let height = params.keyboardSize.height
            scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: height, right: 0)
            scrollView.scrollIndicatorInsets = UIEdgeInsets(top: 0, left: 0, bottom: height, right: 0)
        })
        
    }
    
    static func register(constraint: NSLayoutConstraint, additionalInsets: Insets = .never, aditionalAnimationBlock: (() -> ())? = nil) -> Disposable {
        return KeyboardHandler.register(animatedHandler: { (params) in
            let height = additionalInsets.add(to:  params.keyboardSize.height, with: params.state)
            constraint.constant = height
            aditionalAnimationBlock?()
        })
    }
}
<file_sep>/TVShows/Common/UI/CellsAndItems/Cells/TV Show/TVShowCollectionViewCell.swift
//
//  TVShowCollectionViewCell.swift
//  TVShows
//
//  Created by <NAME> on 24/07/2018.
//  Copyright © 2018 <NAME>. All rights reserved.
//

import UIKit

struct ShowCollectionCellItem {
    
    let title: String
    let imageUrl: URL?
    let didSelectAction: () -> ()
}

extension ShowCollectionCellItem: CollectionCellItem {
    
    func cell(from collectionView: UICollectionView, at indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(ofType: TVShowCollectionViewCell.self,
                                                      for: indexPath)
        cell.configure(with: self)
        
        return cell
    }
    
    func didSelect(at indexPath: IndexPath) {
        didSelectAction()
    }
}

class TVShowCollectionViewCell: UICollectionViewCell {
    
    @IBOutlet private weak var _titleLabel: UILabel!
    @IBOutlet private weak var _imageView: UIImageView!
    
    override func awakeFromNib() {
        super.awakeFromNib()
        _setupImageView()
    }
    
    func configure(with item: ShowCollectionCellItem) {
        _titleLabel.text = item.title
        _imageView.kf.setImage(with: item.imageUrl)
    }
    
    // MARK: - Private functions -
    
    private func _setupImageView() {
        _imageView.layer.shadowColor = UIColor.black.cgColor
        _imageView.layer.shadowOffset = CGSize(width: 0, height: 1)
        _imageView.layer.shadowOpacity = 1
        _imageView.layer.shadowRadius = 2.0
        _imageView.clipsToBounds = false
    }
}
<file_sep>/TVShows/Common/Extensions/Rx/Single+General.swift
//
//  Single+Utility.swift
//
//  Created by <NAME> on 03/02/2019.
//  Copyright © 2019 Infinum. All rights reserved.
//
import Foundation
import RxSwift
import RxCocoa

extension Single {

    /// Converts current Single sequence to Driver, completing on error event.
    ///
    /// - Returns: Driver - completing on error event
    func asDriverOnErrorComplete() -> Driver<Element> {
        return asDriver(onErrorDriveWith: .empty())
    }

}

extension PrimitiveSequenceType where Trait == SingleTrait {

    /// Maps sequence element to given value.
    ///
    /// - Parameter value: Value to map
    /// - Returns: Sequence where element is given value.
    func mapTo<T>(_ value: T) -> Single<T> {
        return map { _ in value }
    }

    /// Maps sequence element to Void type.
    ///
    /// - Returns: Sequence where element is of Void type.
    func mapToVoid() -> Single<Void> {
        return mapTo(())
    }

}

extension PrimitiveSequenceType where Trait == SingleTrait {

    /// Shows loading on subscribe event.
    func handleShowLoading(with progressable: Progressable) -> Single<Element> {
        return self
            .do(onSubscribe: { [unowned progressable] in progressable.showLoading() })
    }

    /// Hides loading on success event or error event.
    func handleHideLoading(with progressable: Progressable) -> Single<Element> {
        return self
            .do(onSuccess: { [unowned progressable] _ in progressable.hideLoading() })
            .do(onError: { [unowned progressable] _ in progressable.hideLoading() })
    }

    /// Shows loading on subscribe event and hides loading on success event or error event.
    func handleLoading(with progressable: Progressable) -> Single<Element> {
        return self
            .handleShowLoading(with: progressable)
            .handleHideLoading(with: progressable)
    }

    /// Shows failure on error event.
    func handleShowFailure(with progressable: Progressable) -> Single<Element> {
        return self
            .do(onError: { [unowned progressable] in progressable.showFailure(with: $0) })
    }

    /// Shows loading on subscribe event, hides loading on success
    /// event or error event and shows failure on error event.
    func handleLoadingAndError(with progressable: Progressable) -> Single<Element> {
        return self
            .handleLoading(with: progressable)
            .handleShowFailure(with: progressable)
    }

}
<file_sep>/TVShows/Modules/Home/HomeInteractor.swift
//
//  HomeInteractor.swift
//  TVShows
//
//  Created by <NAME> on 29/07/2018.
//  Copyright (c) 2018 <NAME>. All rights reserved.
//
//  This file was generated by the 🐍 VIPER generator
//

import Foundation
import Alamofire
import RxSwift
import RxCocoa

final class HomeInteractor {
    
    private let _service: Service
    private let _sessionManager: SessionManager
    
    init(service: Service = .instance, sessionManager: SessionManager = ShowsSessionManager.session) {
        _service = service
        _sessionManager = sessionManager
    }
}

// MARK: - Extensions -

extension HomeInteractor: HomeInteractorInterface {
    
    func loadShows() -> Single<[Show]> {
        return _service
            .request([Show].self, router: Router.Show.getShows())
    }
}
<file_sep>/TVShows/Modules/Login_StandardVIPER_NonRx/LoginSInterfaces.swift
//
//  LoginSInterfaces.swift
//  TVShows
//
//  Created by <NAME> on 29/07/2018.
//  Copyright (c) 2018 <NAME>. All rights reserved.
//
//  This file was generated by the 🐍 VIPER generator
//

import UIKit
import Alamofire

enum LoginSNavigationOption {
    case home
}

protocol LoginSWireframeInterface: WireframeInterface {
    func navigate(to option: LoginSNavigationOption)
}

protocol LoginSViewInterface: ViewInterface {
}

protocol LoginSPresenterInterface: PresenterInterface {
    func didSelectLoginWith(email: String, password: String)
    func didSelectRegisterWith(email: String, password: String)
}

protocol LoginSInteractorInterface: InteractorInterface {
    func login(email: String, password: String, completion: @escaping (Result<LoginData>) -> ())
    func register(email: String, password: String, completion: @escaping (Result<User>) -> ())
}
<file_sep>/TVShows/Common/Networking/Routers/UserRouter.swift
//
//  UserRouter.swift
//  TVShows
//
//  Created by <NAME> on 29/07/2018.
//  Copyright © 2018 <NAME>. All rights reserved.
//

import Foundation
import Alamofire

extension Router {
    
    enum User {
        
        static func loginWith(email: String, password: String) -> Router {
            let params = [
                "email": email,
                "password": <PASSWORD>
            ]
            
            return Router(
                path: "/users/sessions",
                method: .post,
                params: params,
                encoding: JSONEncoding.default
            )
        }

        static func registerWith(email: String, password: String) -> Router {
            let params = [
                "email": email,
                "password": <PASSWORD>
            ]

            return Router(
                path: "/users",
                method: .post,
                params: params,
                encoding: JSONEncoding.default
            )
        }
    }
}
<file_sep>/README.md
# Infinum Academy - iOS 2019
<file_sep>/TVShows/Application/Initializers/AlamofireInitializer.swift
//
//  AlamofireInitializer.swift
//  TVShows
//
//  Created by <NAME> on 28/07/2018.
//  Copyright © 2018 <NAME>. All rights reserved.
//

import Foundation
import AlamofireNetworkActivityLogger
import AlamofireNetworkActivityIndicator

final class AlamofireInitializer: Initializable {
    
    func initialize() {
        #if DEBUG
            NetworkActivityLogger.shared.startLogging()
            NetworkActivityLogger.shared.level = .debug
        #endif
            NetworkActivityIndicatorManager.shared.isEnabled = true
    }
}
<file_sep>/TVShows/Application/AppDelegate.swift
//
//  AppDelegate.swift
//  TVShows
//
//  Created by <NAME> on 11/07/2018.
//  Copyright © 2018 <NAME>. All rights reserved.
//

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        
        initializeInitializers()
        window = createAndSetupWindow()
        
        return true
    }
}

// MARK: - Private methods -

// MARK: - Initializers

private extension AppDelegate {
    
    func initializeInitializers() {
        let initializers: [Initializable] = [
            LoggingInitializer(),
            AlamofireInitializer(),
            LoggieInitializer()
        ]
        
        initializers.forEach { $0.initialize() }
    }
}

// MARK: - UI Setup

private extension AppDelegate {
    
    func createAndSetupWindow() -> UIWindow {
        let window = UIWindow(frame: UIScreen.main.bounds)
        let navigationController = ShowsNavigationController()
        
        navigationController.setRootWireframe(LoginWireframe())
        window.rootViewController = navigationController
        window.makeKeyAndVisible()
        
        return window
    }
}
<file_sep>/TVShows/Modules/Login/LoginViewController.swift
//
//  LoginViewController.swift
//  TVShows
//
//  Created by <NAME> on 29/07/2018.
//  Copyright (c) 2018 <NAME>. All rights reserved.
//
//  This file was generated by the 🐍 VIPER generator
//

import UIKit
import RxSwift
import RxCocoa

final class LoginViewController: ShowsViewController {

    // MARK: - Private properties -

    private let _disposeBag = DisposeBag()

    // MARK: - IBOutlets
    
    @IBOutlet private weak var _scrollView: UIScrollView!
    
    @IBOutlet private weak var _loginButton: UIButton!
    @IBOutlet private weak var _registerButton: UIButton!
    @IBOutlet private weak var _checkmarkButton: UIButton!
    
    @IBOutlet private weak var _emailField: UITextField!
    @IBOutlet private weak var _passwordField: UITextField!
    
    // MARK: - Public properties -

    var presenter: LoginPresenterInterface!

    // MARK: - Lifecycle -

    override func viewDidLoad() {
        super.viewDidLoad()
        _setupView()
        _setupUI()
    }
	
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        navigationController?.setNavigationBarHidden(true, animated: animated)
    }
}

// MARK: - Extensions -

extension LoginViewController: LoginViewInterface {
}

private extension LoginViewController {

    func _setupView() {

        let toggleRememberMe = _checkmarkButton.rx.tap.asDriver()
            .scan(false) { state, _ in !state }
            .startWith(false)
        
        toggleRememberMe
            .drive(_checkmarkButton.rx.isSelected)
            .disposed(by: _disposeBag)
        
        let output = Login.ViewOutput(email: _emailField.rx.text.asDriver().unwrap(),
                                      password: _passwordField.rx.text.asDriver().unwrap(),
                                      rememberMe: toggleRememberMe,
                                      loginPressed: _loginButton.rx.tap.asDriver(),
                                      registerPressed: _registerButton.rx.tap.asDriver())
        
        let input = presenter.setupBinding(with: output)
        
        input
            .loginEnabled
            .drive(_loginButton.rx.isEnabled)
            .disposed(by: _disposeBag)
        
        input
            .registerEnabled
            .drive(_registerButton.rx.isEnabled)
            .disposed(by: _disposeBag)
    }
    
    func _setupUI() {
        _loginButton.layer.cornerRadius = 6.0
        _loginButton.setBackgroundImage(.from(color: UIColor.TVShows.pink), for: .normal)
        _loginButton.setBackgroundImage(.from(color: UIColor.TVShows.pink.withAlphaComponent(0.5)),
                                        for: .disabled)
        
        KeyboardHandler
            .register(scrollView: _scrollView)
            .disposed(by: _disposeBag)
    }
}
<file_sep>/TVShows/Modules/Login/LoginPresenter.swift
//
//  LoginPresenter.swift
//  TVShows
//
//  Created by <NAME> on 29/07/2018.
//  Copyright (c) 2018 <NAME>. All rights reserved.
//
//  This file was generated by the 🐍 VIPER generator
//

import UIKit
import RxSwift
import RxCocoa

final class LoginPresenter {

    // MARK: - Private properties -
    
    private unowned let _view: LoginViewInterface
    private let _interactor: LoginInteractorInterface
    private let _wireframe: LoginWireframeInterface

    // MARK: - Lifecycle -

    init(view: LoginViewInterface, interactor: LoginInteractorInterface, wireframe: LoginWireframeInterface) {
        _view = view
        _interactor = interactor
        _wireframe = wireframe
    }
}

// MARK: - Extensions -

extension LoginPresenter: LoginPresenterInterface {

    func setupBinding(with output: Login.ViewOutput) -> Login.ViewInput {
        let loginModel = Driver
            .combineLatest(output.email, output.password, output.rememberMe)
            .map(Login.Model.init)
        
        let hasEmailAndPassword = loginModel
            .map { !$0.email.isEmpty && !$0.password.isEmpty }
        
        output
            .loginPressed
            .withLatestFrom(loginModel)
            .pipe(to: loginWith(loginModel:))
        
        output
            .registerPressed
            .withLatestFrom(loginModel)
            .pipe(to: _registerWith(loginModel:))
        
        return Login.ViewInput(loginEnabled: hasEmailAndPassword,
                               registerEnabled: hasEmailAndPassword)
    }
}

private extension LoginPresenter {
    
    func _registerWith(loginModel: Driver<Login.Model>) {
        loginModel
            .flatMapLatest { [unowned _interactor, unowned _wireframe] model -> Driver<Login.Model> in
                return _interactor
                    .registerWith(email: model.email, password: model.password)
                    .handleLoadingAndError(with: _wireframe)
                    .asDriverOnErrorComplete()
                    .mapTo(model)
            }
            .pipe(to: loginWith(loginModel:))
    }
    
    func loginWith(loginModel: Driver<Login.Model>) {
        loginModel
            .flatMapLatest { [unowned _interactor, unowned _wireframe] model -> Driver<Void> in
                return _interactor
                    .loginWith(email: model.email, password: model.password)
                    .handleLoadingAndError(with: _wireframe)
                    .asDriverOnErrorComplete()
            }
            .mapTo(Login.NavigationOption.home)
            .pipe(to: handle(navigation:))
    }
    
    func handle(navigation: Driver<Login.NavigationOption>) {
        navigation.pipe(to: _wireframe.navigate(using: ))
    }
}

<file_sep>/TVShows/Common/Extensions/UI/UIColor+Inits.swift
//
//  UIColor+Inits.swift
//  TVShows
//
//  Created by <NAME> on 28/07/2018.
//  Copyright © 2018 <NAME>. All rights reserved.
//

import UIKit

extension UIColor {
    
    convenience init(rgbHex: Int, alpha: CGFloat = 1) {
        let r = (rgbHex >> 16) & 0xFF
        let g = (rgbHex >> 8) & 0xFF
        let b = rgbHex & 0xFF

        self.init(
            red: CGFloat(r) / 255.0,
            green: CGFloat(g) / 255.0,
            blue: CGFloat(b) / 255.0,
            alpha: alpha
        )
    }
    
    convenience init?(rgbHex: String, alpha: CGFloat = 1) {
        
        let string = rgbHex
            .trimmingCharacters(in: .whitespacesAndNewlines)
            .uppercased()
        
        guard
            string.count == 6,
            let int = Int(string, radix: 16)
        else {
            return nil
        }
        
        self.init(rgbHex: int, alpha: alpha)
    }
}
<file_sep>/TVShows/Modules/Login_StandardVIPER_NonRx/LoginSInteractor.swift
//
//  LoginSInteractor.swift
//  TVShows
//
//  Created by <NAME> on 29/07/2018.
//  Copyright (c) 2018 <NAME>. All rights reserved.
//
//  This file was generated by the 🐍 VIPER generator
//

import Foundation
import Alamofire
import CodableAlamofire

final class LoginSInteractor {
    
    private let _sessionManager: SessionManager
    
    init(sessionManager: SessionManager = ShowsSessionManager.session) {
        _sessionManager = sessionManager
    }
}

// MARK: - Extensions -

extension LoginSInteractor: LoginSInteractorInterface {
    
    func login(email: String, password: String, completion: @escaping (Result<LoginData>) -> ()) {
        let sessionManager = _sessionManager
        let request = Router.User.loginWith(email: email, password: <PASSWORD>)
        sessionManager
            .request(request)
            .validate()
            .responseDecodableObject(keyPath: "data") { (response: DataResponse<LoginData>) in
                if let token = response.result.value?.token {
                    sessionManager.adapter = TokenAdapter(token: token)
                }
                completion(response.result)
            }
    }
    
    func register(email: String, password: String, completion: @escaping (Result<User>) -> ()) {
        let request = Router.User.registerWith(email: email, password: <PASSWORD>)
        _sessionManager
            .request(request)
            .validate()
            .responseDecodableObject(keyPath: "data") { (response: DataResponse<User>) in
                completion(response.result)
            }
    }
}
<file_sep>/TVShows/Common/Networking/Services/Service.swift
//
//  Service.swift
//  TVShows
//
//  Created by <NAME> on 29/07/2018.
//  Copyright © 2018 <NAME>. All rights reserved.
//

import Foundation
import Alamofire
import RxSwift

final class Service {
    static var instance = Service()
}

extension Service {
    
    func request<T: Decodable>(router: Router, keyPath: String = "data") -> Single<T> {
        return Single.just(ShowsSessionManager.session)
            .map { $0.request(router) }
            .map { $0.validate() }
            .flatMap { $0.rx.responseDecodableObject(keyPath: keyPath) }
    }
    
    func request<T: Decodable>(_: T.Type, router: Router) -> Single<T> {
        return request(router: router)
    }
    
    func requestCompletion(router: Router) -> Single<Void> {
        return Single.just(ShowsSessionManager.session)
            .map { $0.request(router) }
            .map { $0.validate() }
            .flatMap { $0.rx.response().andThen(Single.just(())) }
    }
    
}
<file_sep>/TVShows/Common/UI/CellsAndItems/Basic/Reactive/ReactiveDataSourceDelegates.swift
//
//  ReactiveDataSourceDelegates.swift
//
//

import UIKit
import RxSwift
import RxCocoa

extension Reactive where Base: TableDataSourceDelegate {
    
    var items: Binder<[TableCellItem]?> {
        return Binder(self.base) { dataSourceDelegate, items in
            dataSourceDelegate.items = items
        }
    }
    
    var sections: Binder<[TableSectionItem]?> {
        return Binder(self.base) { dataSourceDelegate, sections in
            dataSourceDelegate.sections = sections
        }
    }
}

extension Reactive where Base: CollectionDataSourceDelegate {
    
    var items: Binder<[CollectionCellItem]?> {
        return Binder(self.base) { dataSourceDelegate, items in
            dataSourceDelegate.items = items
        }
    }
    
    var sections: Binder<[CollectionSectionItem]?> {
        return Binder(self.base) { dataSourceDelegate, sections in
            dataSourceDelegate.sections = sections
        }
    }
}
<file_sep>/TVShows/Common/Extensions/UI/UIBarButtonItem+General.swift
//
//  UIBarButtonItem+General.swift
//  TVShows
//
//  Created by <NAME> on 29/07/2018.
//  Copyright © 2018 <NAME>. All rights reserved.
//

import UIKit
import RxSwift

extension UIBarButtonItem {
    
    convenience init(barButtonSystemItem: UIBarButtonItem.SystemItem, action: (()->())? = nil) {
        self.init(barButtonSystemItem: barButtonSystemItem, target: nil, action: nil)
        self.rx.tap
            .asDriver()
            .drive(onNext: { action?() })
            .disposed(by: disposeBag)
    }
    
    convenience init(title: String?, style: UIBarButtonItem.Style = .plain, action: (()->())? = nil) {
        self.init(title: title, style: style, target: nil, action: nil)
        self.rx.tap
            .asDriver()
            .drive(onNext: { action?() })
            .disposed(by: disposeBag)
    }
    
    convenience init(image: UIImage?, style: UIBarButtonItem.Style = .plain, action: (()->())? = nil) {
        self.init(image: image, style: style, target: nil, action: nil)
        self.rx.tap
            .asDriver()
            .drive(onNext: { action?() })
            .disposed(by: disposeBag)
    }
    
    private var disposeBag: DisposeBag {
        let lookup = objc_getAssociatedObject(self, associatedKeysDisposeBag) as? DisposeBag
        if let lookup = lookup {
            return lookup
        }
        let newDisposeBag = DisposeBag()
        objc_setAssociatedObject(self, associatedKeysDisposeBag, newDisposeBag, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        return newDisposeBag
    }
    
    private var associatedKeysDisposeBag: String {
        return "UIBarButtonItem.rx.disposeBag"
    }
}
<file_sep>/TVShows/Common/UI/CellsAndItems/Basic/Protocols/CollectionViewReloader.swift
//
//  CollectionViewReloader.swift
//
//

import UIKit

protocol CollectionViewReloader {
    func reload(_ collectionView: UICollectionView, oldSections: [CollectionSectionItem]?, newSections: [CollectionSectionItem]?)
}

struct DefaultCollectionViewReloader: CollectionViewReloader {
    
    func reload(_ collectionView: UICollectionView, oldSections: [CollectionSectionItem]?, newSections: [CollectionSectionItem]?) {
        collectionView.reloadData()
    }
    
}
<file_sep>/TVShows/Modules/Login/LoginInterfaces.swift
//
//  LoginInterfaces.swift
//  TVShows
//
//  Created by <NAME> on 29/07/2018.
//  Copyright (c) 2018 <NAME>. All rights reserved.
//
//  This file was generated by the 🐍 VIPER generator
//

import UIKit
import RxSwift
import RxCocoa

enum Login {
    
    struct ViewOutput {
        let email: Driver<String>
        let password: Driver<String>
        let rememberMe: Driver<Bool>
        let loginPressed: Driver<Void>
        let registerPressed: Driver<Void>
    }
    
    struct ViewInput {
        let loginEnabled: Driver<Bool>
        let registerEnabled: Driver<Bool>
    }
    
    struct Model {
        let email: String
        let password: String
        let rememberMe: Bool
    }
    
    enum NavigationOption {
        case home
    }
}

protocol LoginWireframeInterface: WireframeInterface {
    func navigate(to option: Login.NavigationOption)
    func navigate(using option: Driver<Login.NavigationOption>)
}

protocol LoginViewInterface: ViewInterface {
}

protocol LoginPresenterInterface: PresenterInterface {
    func setupBinding(with output: Login.ViewOutput) -> Login.ViewInput
}

protocol LoginInteractorInterface: InteractorInterface {
    func loginWith(email: String, password: String) -> Single<Void>
    func registerWith(email: String, password: String) -> Single<Void>
}
<file_sep>/TVShows/Common/UI/Other/OnePixelConstraint.swift
//
//  OnePixelConstraint.swift
//  TVShows
//
//  Created by <NAME> on 29/07/2018.
//  Copyright © 2018 <NAME>. All rights reserved.
//

import UIKit

class OnePixelConstraint: NSLayoutConstraint {
    
    override func awakeFromNib() {
        super.awakeFromNib()
        constant = 1 / UIScreen.main.scale
    }
}
<file_sep>/TVShows/Modules/Login_StandardVIPER_NonRx/LoginSPresenter.swift
//
//  LoginSPresenter.swift
//  TVShows
//
//  Created by <NAME> on 29/07/2018.
//  Copyright (c) 2018 <NAME>. All rights reserved.
//
//  This file was generated by the 🐍 VIPER generator
//

import Foundation

final class LoginSPresenter {

    // MARK: - Private properties -

    private unowned let _view: LoginSViewInterface
    private let _interactor: LoginSInteractorInterface
    private let _wireframe: LoginSWireframeInterface

    // MARK: - Lifecycle -

    init(wireframe: LoginSWireframeInterface, view: LoginSViewInterface, interactor: LoginSInteractorInterface) {
        _wireframe = wireframe
        _view = view
        _interactor = interactor
    }
}

// MARK: - Extensions -

extension LoginSPresenter: LoginSPresenterInterface {
    
    func didSelectLoginWith(email: String, password: String) {
        _wireframe.showLoading()
        _interactor.login(email: email, password: <PASSWORD>) { [weak _wireframe] result in
            _wireframe?.hideLoading()
            switch result {
            case .success:
                _wireframe?.navigate(to: .home)
            case .failure(let error):
                _wireframe?.showFailure(with: error)
            }
        }
    }
    
    func didSelectRegisterWith(email: String, password: String) {
        _wireframe.showLoading()
        _interactor.register(email: email, password: <PASSWORD>) { [weak _wireframe, weak self] result in
            _wireframe?.hideLoading()
            switch result {
            case .success:
                self?.didSelectLoginWith(email: email, password: <PASSWORD>)
            case .failure(let error):
                _wireframe?.showFailure(with: error)
            }
        }
    }
}
<file_sep>/TVShows/Application/Constants.swift
//
//  Constants.swift
//  TVShows
//
//  Created by <NAME> on 28/07/2018.
//  Copyright © 2018 <NAME>. All rights reserved.
//

import Foundation

enum Constants {
    
    enum URL {
        static let baseDomainUrl = "https://api.infinum.academy"
        static let baseUrl = baseDomainUrl + "/api"
    }
}
<file_sep>/TVShows/Modules/Home/HomeViewController.swift
//
//  HomeViewController.swift
//  TVShows
//
//  Created by <NAME> on 29/07/2018.
//  Copyright (c) 2018 <NAME>. All rights reserved.
//
//  This file was generated by the 🐍 VIPER generator
//

import UIKit
import RxSwift
import RxCocoa

final class HomeViewController: UIViewController {

    // MARK: - Private properties -
    
    private let _disposeBag = DisposeBag()
    
    private lazy var _dataSourceDelegate: CollectionDataSourceDelegate = {
        return CollectionDataSourceDelegate(collectionView: _collectionView)
    }()
    
    lazy var refreshControl: UIRefreshControl = {
        let refreshControl = UIRefreshControl()
        _collectionView.refreshControl = refreshControl
        
        return refreshControl
    }()
    
    // MARK: - IBOutlets
    
    @IBOutlet private weak var _collectionView: UICollectionView!

    // MARK: - Public properties -

    var presenter: HomePresenterInterface!

    // MARK: - Lifecycle -

    override func viewDidLoad() {
        super.viewDidLoad()
        setupView()
        setupUI()
        _collectionView.collectionViewLayout.invalidateLayout()
        _collectionView.setCollectionViewLayout(ShowsGridFlowLayout(), animated: false)
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        navigationController?.setNavigationBarHidden(false, animated: true)
        _collectionView.refreshControl?.endRefreshing()
    }
}

// MARK: - Extensions -

extension HomeViewController: HomeViewInterface {
}

private extension HomeViewController {

    func setupView() {
        let logoutButton = UIBarButtonItem(image: #imageLiteral(resourceName: "ic-logout"))
        navigationItem.leftBarButtonItem = logoutButton
                
        let refresh = refreshControl.rx
            .controlEvent(.valueChanged)
            .asDriver()

        let output = Home.ViewOutput(refresh: refresh,
                                     logoutPressed: logoutButton.rx.tap.asDriver())
        let input = presenter.setup(with: output)
        
        input.items
            .drive(_dataSourceDelegate.rx.items)
            .disposed(by: _disposeBag)
        
        input.items
            .mapTo(false)
            .drive(refreshControl.rx.isRefreshing)
            .disposed(by: _disposeBag)
    }
    
    func setupUI() {
        title = "Shows"
    }
}
<file_sep>/TVShows/Common/UI/Controller/ShowsNavigationController.swift
//
//  ShowsNavigationController.swift
//  TVShows
//
//  Created by <NAME> on 28/07/2018.
//  Copyright © 2018 <NAME>. All rights reserved.
//

import UIKit
import AHKNavigationController

class ShowsNavigationController: UINavigationController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        if #available(iOS 11.0, *) {
            navigationBar.prefersLargeTitles = false
        }
        
        navigationBar.backgroundColor = .white
        navigationBar.tintColor = UIColor.TVShows.gray
        navigationBar.isTranslucent = false
    }
}
<file_sep>/TVShows/Common/UI/CellsAndItems/Basic/DataSourceDelegates/TableDataSourceDelegate.swift
//
//  TableDataSourceDelegate.swift
//
//

import UIKit
import RxSwift

class EmptyTableSection: TableSectionItem {
    
    var tableItems: [TableCellItem]
    
    init(tableItems: [TableCellItem]) {
        self.tableItems = tableItems
    }
    
    convenience init?(tableItems: [TableCellItem]?) {
        if let item = tableItems {
            self.init(tableItems: item)
            return
        }
        return nil
    }
    
    var headerHeight: CGFloat {
        if #available(iOS 11.0, *) {
            return .leastNonzeroMagnitude
        } else {
            return 1
        }
    }
    
    var estimatedHeaderHeight: CGFloat {
        return headerHeight
    }
    
    var footerHeight: CGFloat {
        if #available(iOS 11.0, *) {
            return .leastNonzeroMagnitude
        } else {
            return 1
        }
    }
    
    var estimatedFooterHeight: CGFloat {
        return footerHeight
    }
}

class TableDataSourceDelegate: NSObject {
    
    private let tableView: UITableView
    private let reloader: TableViewReloader
    private let disposeBag = DisposeBag()
    
    var sections: [TableSectionItem]? {
        didSet(oldValue) { reloader.reload(tableView, oldSections: oldValue, newSections: sections) }
    }
    
    var items: [TableCellItem]? {
        get { return sections?.map({ $0.tableItems }).reduce([TableCellItem](), +) }
        set {
            guard let tableItems = newValue else { return }
            sections = [EmptyTableSection(tableItems: tableItems) as TableSectionItem]
        }
    }
    
    init(tableView: UITableView, reloader: TableViewReloader = DefaultTableViewReloader()) {
        self.tableView = tableView
        self.reloader = reloader
        super.init()
        tableView.dataSource = self
        tableView.rx
            .setDelegate(self)
            .disposed(by: disposeBag)
    }

}

extension TableDataSourceDelegate: UITableViewDataSource {
    
    func numberOfSections(in tableView: UITableView) -> Int {
        return sections?.count ?? 0
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return sections?[section].tableItems.count ?? 0
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        return sections![indexPath].cell(from: tableView, at: indexPath)
    }
    
//    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
//        return sections![section].titleForHeader(from: tableView, at: section)
//    }
//
//    func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
//        return sections![section].titleForFooter(from: tableView, at: section)
//    }
    
}

extension TableDataSourceDelegate: UITableViewDelegate {
    
    func tableView(_ tableView: UITableView, estimatedHeightForHeaderInSection section: Int) -> CGFloat {
        return sections![section].estimatedHeaderHeight
    }
    
    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        return sections![section].headerHeight
    }
    
    func tableView(_ tableView: UITableView, estimatedHeightForFooterInSection section: Int) -> CGFloat {
        return sections![section].estimatedFooterHeight
    }
    
    func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
        return sections![section].footerHeight
    }
    
    func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
        return sections![indexPath].estimatedHeight
    }
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return sections![indexPath].height
    }
    
    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        return sections?[section].headerView(from:tableView, at:section)
    }
    
    func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
        return sections?[section].footerView(from:tableView, at:section)
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        return sections![indexPath].didSelect(at: indexPath)
    }
    
}
<file_sep>/TVShows/Common/UI/CellsAndItems/Basic/Protocols/SectionItem.swift
//
//  SectionItem.swift
//
//

import UIKit

protocol SectionItem {

}

protocol TableSectionItem: SectionItem {
    
    //Requred
    var tableItems: [TableCellItem] { get }
    
    //Optional
    var headerHeight: CGFloat { get }
    var estimatedHeaderHeight: CGFloat { get }
    var footerHeight: CGFloat { get }
    var estimatedFooterHeight: CGFloat { get }
    
    func headerView(from tableView: UITableView, at index: Int) -> UIView?
    func titleForHeader(from tableView: UITableView, at index: Int) -> String?
    
    func footerView(from tableView: UITableView, at index: Int) -> UIView?
    func titleForFooter(from tableView: UITableView, at index: Int) -> String?
    
}

protocol CollectionSectionItem: SectionItem {
    
    //Requred
    var collectionItems: [CollectionCellItem] { get }
    
    //Optional
    
}

extension TableSectionItem {
    
    var headerHeight: CGFloat {
        return UITableView.automaticDimension
    }
    
    var estimatedHeaderHeight: CGFloat {
        return 0
    }
    
    var footerHeight: CGFloat {
        return UITableView.automaticDimension
    }
    
    var estimatedFooterHeight: CGFloat {
        return 0
    }
    
    func headerView(from tableView: UITableView, at index: Int) -> UIView? {
        return nil
    }
    
    func titleForHeader(from tableView: UITableView, at index: Int) -> String? {
        return nil
    }
    
    func footerView(from tableView: UITableView, at index: Int) -> UIView? {
        return nil
    }
    
    func titleForFooter(from tableView: UITableView, at index: Int) -> String? {
        return nil
    }
    
}

extension Array where Element: TableSectionItem {
    subscript(indexPath: IndexPath) -> TableCellItem {
        return self[indexPath.section].tableItems[indexPath.row]
    }
}

extension Array where Element == TableSectionItem {
    subscript(indexPath: IndexPath) -> TableCellItem {
        return self[indexPath.section].tableItems[indexPath.row]
    }
}

extension Array where Element: CollectionSectionItem {
    subscript(indexPath: IndexPath) -> CollectionCellItem {
        return self[indexPath.section].collectionItems[indexPath.row]
    }
}

extension Array where Element == CollectionSectionItem {
    subscript(indexPath: IndexPath) -> CollectionCellItem {
        return self[indexPath.section].collectionItems[indexPath.row]
    }
}
<file_sep>/TVShows/Application/Initializers/Initializable.swift
//
//  Initializable.swift
//  TVShows
//
//  Created by <NAME> on 28/07/2018.
//  Copyright © 2018 <NAME>. All rights reserved.
//

import Foundation

protocol Initializable {
    func initialize()
}
<file_sep>/TVShows/Modules/Home/HomeWireframe.swift
//
//  HomeWireframe.swift
//  TVShows
//
//  Created by <NAME> on 29/07/2018.
//  Copyright (c) 2018 <NAME>. All rights reserved.
//
//  This file was generated by the 🐍 VIPER generator
//

import UIKit
import RxSwift
import RxCocoa

final class HomeWireframe: BaseWireframe {

    // MARK: - Private properties -

    private let storyboard = UIStoryboard(name: "Home", bundle: nil)

    // MARK: - Module setup -

    init() {
        let moduleViewController = storyboard.instantiateViewController(ofType: HomeViewController.self)
        super.init(viewController: moduleViewController)
        
        let interactor = HomeInteractor()
        let presenter = HomePresenter(view: moduleViewController, interactor: interactor, wireframe: self)
        moduleViewController.presenter = presenter
    }

}

// MARK: - Extensions -

extension HomeWireframe: HomeWireframeInterface {
    
    func navigate(to option: Home.NavigationOption) {
        switch option {
        case .showDetails(showId: let id):
            navigationController?.pushWireframe(ShowDetailsWireframe(showId: id))
        }
    }
}
<file_sep>/TVShows/Common/Extensions/UI/UICollectionView+General.swift
//
//  UICollectionView+General.swift
//  TVShows
//
//  Created by <NAME> on 29/07/2018.
//  Copyright © 2018 <NAME>. All rights reserved.
//

import UIKit

extension UICollectionView {
    
    func dequeueReusableCell<T: UICollectionViewCell>(ofType _: T.Type, withReuseIdentifier identifier: String? = nil, for indexPath: IndexPath) -> T {
        let identifier = identifier ?? String(describing: T.self)
        return dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath) as! T
    }
    
    func register<T>(cellOfType _: T.Type, withReuseIdentifier identifier: String? = nil) {
        let cellName = String(describing: T.self)
        let identifier = identifier ?? cellName
        let nib = UINib(nibName: cellName, bundle: nil)
        register(nib, forCellWithReuseIdentifier: identifier)
    }
}
<file_sep>/TVShows/Modules/Login/LoginWireframe.swift
//
//  LoginWireframe.swift
//  TVShows
//
//  Created by <NAME> on 29/07/2018.
//  Copyright (c) 2018 <NAME>. All rights reserved.
//
//  This file was generated by the 🐍 VIPER generator
//

import UIKit
import RxSwift
import RxCocoa

final class LoginWireframe: BaseWireframe {

    // MARK: - Private properties -

    private let storyboard = UIStoryboard(name: "Login", bundle: nil)

    // MARK: - Module setup -

    init() {
        let moduleViewController = storyboard.instantiateViewController(ofType: LoginViewController.self)
        super.init(viewController: moduleViewController)
        
        let interactor = LoginInteractor()
        let presenter = LoginPresenter(view: moduleViewController, interactor: interactor, wireframe: self)
        moduleViewController.presenter = presenter
    }

}

// MARK: - Extensions -

extension LoginWireframe: LoginWireframeInterface {
    
    func navigate(using option: Driver<Login.NavigationOption>) {
        subscribe(to: option, unowning: self, navigationBlock: LoginWireframe.navigate(to: ))
    }
    
    func navigate(to option: Login.NavigationOption) {
        switch option {
        case .home:
            navigationController?.pushWireframe(HomeWireframe())
        }
    }
}
<file_sep>/TVShows/Common/UI/CellsAndItems/Basic/DataSourceDelegates/CollectionDataSourceDelegate.swift
//
//  CollectionDataSourceDelegate.swift
//
//

import UIKit
import RxSwift

class EmptyCollectionSection: CollectionSectionItem {
    
    var collectionItems: [CollectionCellItem]
    
    init(collectionItems: [CollectionCellItem]) {
        self.collectionItems = collectionItems
    }
    
    convenience init?(collectionItems: [CollectionCellItem]?) {
        if let item = collectionItems {
            self.init(collectionItems: item)
            return
        }
        return nil
    }
}

class CollectionDataSourceDelegate: NSObject {
    
    private let collectionView: UICollectionView
    private let reloader: CollectionViewReloader
    
    private let disposeBag = DisposeBag()
    
    var sections: [CollectionSectionItem]? {
        didSet { reloader.reload(collectionView, oldSections: oldValue, newSections: sections) }
    }
    
    var items: [CollectionCellItem]? {
        get { return sections?.map({ $0.collectionItems }).reduce([CollectionCellItem](), +) }
        set {
            guard let tableItems = newValue else { return }
            sections = [EmptyCollectionSection(collectionItems: tableItems) as CollectionSectionItem]
        }
    }
    
    init(collectionView: UICollectionView, reloader: CollectionViewReloader = DefaultCollectionViewReloader()) {
        self.collectionView = collectionView
        self.reloader = reloader
        super.init()
        collectionView.dataSource = self
        collectionView.rx
            .setDelegate(self)
            .disposed(by: disposeBag)
    }
    
}

extension CollectionDataSourceDelegate: UICollectionViewDataSource {
    
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return sections?.count ?? 0
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return sections?[section].collectionItems.count ?? 0
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        return sections![indexPath].cell(from: collectionView, at: indexPath)
    }
    
}

extension CollectionDataSourceDelegate: UICollectionViewDelegate {
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        collectionView.deselectItem(at: indexPath, animated: true)
        sections![indexPath].didSelect(at: indexPath)
    }

    func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
        sections![indexPath].willDisplay(cell: cell, at: indexPath)
    }

}

extension CollectionDataSourceDelegate: UIScrollViewDelegate {
        
}
<file_sep>/TVShows/Common/UI/CellsAndItems/Basic/Protocols/CellItem.swift
//
//  CellItem.swift
//
//

import UIKit

protocol CellItem {
    
}

protocol TableCellItem: CellItem {
    
    //Required
    func cell(from tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell
    
    //Optional
    var height: CGFloat { get }
    var estimatedHeight: CGFloat { get }
    
    func didSelect(at indexPath: IndexPath)

}

protocol CollectionCellItem: CellItem {
    
    //Required
    func cell(from collectionView: UICollectionView, at indexPath: IndexPath) -> UICollectionViewCell
    
    //Optional
    func sizeWithCollectionView(size: CGSize, numOfImtemsInSection: Int) -> CGSize
    
    func willDisplay(cell: UICollectionViewCell, at indexPath: IndexPath)
    
    func didSelect(at indexPath: IndexPath)
    
}

extension TableCellItem {
    
    var height: CGFloat {
        return UITableView.automaticDimension
    }
    
    var estimatedHeight: CGFloat {
        return 44
    }
    
    func didSelect(at indexPath: IndexPath) {
        //pass
    }
    
}

extension CollectionCellItem {
    
    func sizeWithCollectionView(size: CGSize, numOfImtemsInSection: Int) -> CGSize {
        return UICollectionViewFlowLayout.automaticSize
    }
    
    func willDisplay(cell: UICollectionViewCell, at indexPath: IndexPath) {
        //pass
    }
    
    func didSelect(at indexPath: IndexPath) {
        //pass
    }
    
}
<file_sep>/TVShows/Modules/Login_StandardVIPER_NonRx/LoginSWireframe.swift
//
//  LoginSWireframe.swift
//  TVShows
//
//  Created by <NAME> on 29/07/2018.
//  Copyright (c) 2018 <NAME>. All rights reserved.
//
//  This file was generated by the 🐍 VIPER generator
//

import UIKit

final class LoginSWireframe: BaseWireframe {

    // MARK: - Private properties -

    private let _storyboard = UIStoryboard(name: "LoginS", bundle: nil)

    // MARK: - Module setup -

    init() {
        let moduleViewController = _storyboard.instantiateViewController(ofType: LoginSViewController.self)
        super.init(viewController: moduleViewController)
        
        let interactor = LoginSInteractor()
        let presenter = LoginSPresenter(wireframe: self, view: moduleViewController, interactor: interactor)
        moduleViewController.presenter = presenter
    }

}

// MARK: - Extensions -

extension LoginSWireframe: LoginSWireframeInterface {

    func navigate(to option: LoginSNavigationOption) {
        switch option {
        case .home:
            navigationController?.pushWireframe(HomeWireframe())
        }
    }
}
<file_sep>/TVShows/Common/Networking/Routers/Router.swift
//
//  Router.swift
//  TVShows
//
//  Created by <NAME> on 29/07/2018.
//  Copyright © 2018 <NAME>. All rights reserved.
//

import Foundation
import Alamofire

struct Router: URLRequestConvertible {
    
    private static let base = Constants.URL.baseUrl
    
    private let base: String
    private let path: String
    private let method: HTTPMethod
    private let params: Parameters?
    private let headers: HTTPHeaders?
    private let encoding: ParameterEncoding
    
    private init(base: String, path: String, method: HTTPMethod, params: Parameters?, headers: HTTPHeaders?, encoding: ParameterEncoding) {
        self.base = base
        self.path = path
        self.method = method
        self.params = params
        self.headers = headers
        self.encoding = encoding
    }
    
    init(path: String, method: HTTPMethod = .get, params: Parameters? = nil, encoding: ParameterEncoding = URLEncoding.default, headers: HTTPHeaders? = nil) {
        self.init(base: Router.base,
                  path: path, method: method,
                  params: params,
                  headers: headers,
                  encoding: encoding)
    }
    
    private func pathURL() throws -> URL {
        if path.starts(with: "http") {
            return try path.asURL()
        }
        return try base.asURL()
            .appendingPathComponent(path)
    }
    
    func asURLRequest() throws -> URLRequest {
        let url = try pathURL()
        let originalRequest = try URLRequest(url: url, method: method, headers: headers)
        return try encoding.encode(originalRequest, with: params)
    }
}
<file_sep>/TVShows/Common/Extensions/UI/UIColor+AppColors.swift
//
//  UIColor+AppColors.swift
//  TVShows
//
//  Created by <NAME> on 28/07/2018.
//  Copyright © 2018 <NAME>. All rights reserved.
//

import UIKit

extension UIColor {
    
    enum TVShows {
        static var gray = UIColor(rgbHex: 0x505050)
        static var pink = UIColor(rgbHex: 0xFF758C)
    }
}
<file_sep>/TVShows/Modules/ShowDetails/ShowDetailsWireframe.swift
//
//  ShowDetailsWireframe.swift
//  TVShows
//
//  Created by <NAME> on 30/07/2018.
//  Copyright (c) 2018 <NAME>. All rights reserved.
//
//  This file was generated by the 🐍 VIPER generator
//

import UIKit
import RxSwift
import RxCocoa

final class ShowDetailsWireframe: BaseWireframe {

    // MARK: - Private properties -

    private let storyboard = UIStoryboard(name: "ShowDetails", bundle: nil)

    // MARK: - Module setup -

    init(showId: String) {
        let moduleViewController = storyboard.instantiateViewController(ofType: ShowDetailsViewController.self)
        super.init(viewController: moduleViewController)
        
        let interactor = ShowDetailsInteractor(showId: showId)
        let presenter = ShowDetailsPresenter(view: moduleViewController, interactor: interactor, wireframe: self)
        moduleViewController.presenter = presenter
    }

}

// MARK: - Extensions -

extension ShowDetailsWireframe: ShowDetailsWireframeInterface {

    func navigate(to option: ShowDetailsNavigationOption) {
    }
}
<file_sep>/TVShows/Modules/Login_StandardVIPER_NonRx/LoginSViewController.swift
//
//  LoginSViewController.swift
//  TVShows
//
//  Created by <NAME> on 29/07/2018.
//  Copyright (c) 2018 <NAME>. All rights reserved.
//
//  This file was generated by the 🐍 VIPER generator
//

import UIKit

final class LoginSViewController: UIViewController {

    // MARK: - Private properties -
    
    // MARK: - IBOutlets
    
    @IBOutlet private weak var _scrollView: UIScrollView!
    
    @IBOutlet private weak var _loginButton: UIButton!
    @IBOutlet private weak var _registerButton: UIButton!
    @IBOutlet private weak var _checkmarkButton: UIButton!
    
    @IBOutlet private weak var _emailField: UITextField!
    @IBOutlet private weak var _passwordField: UITextField!
    
    // MARK: - Public properties -

    var presenter: LoginSPresenterInterface!
    
    // MARK: - Public methods -

    // MARK: - Lifecycle

    override func viewDidLoad() {
        super.viewDidLoad()
        _setupUI()
        _setupKeyboardObservers()
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        navigationController?.setNavigationBarHidden(true, animated: animated)
    }
    
    // MARK: - Private methods -

    // MARK: - UI Setup
    
    private func _setupUI() {
        _loginButton.layer.cornerRadius = 6.0
        _loginButton.setBackgroundImage(.from(color: UIColor.TVShows.pink), for: .normal)
        _loginButton.setBackgroundImage(.from(color: UIColor.TVShows.pink.withAlphaComponent(0.5)),
                                        for: .disabled)
    }
}

// MARK: - Extensions -

extension LoginSViewController: LoginSViewInterface {
}

// MARK: - IBActions

private extension LoginSViewController {
    
    @IBAction func _checkmarkButtonActionHandler() {
        _checkmarkButton.isSelected = !_checkmarkButton.isSelected
    }
    
    @IBAction private func _loginButtonActionHandler() {
        guard
            let email = _emailField.text,
            let password = _passwordField.text,
            !email.isEmpty, !password.isEmpty
        else {
            return
        }
        view.endEditing(true)
        presenter.didSelectLoginWith(email: email, password: <PASSWORD>)
    }
    
    @IBAction private func _registerButtonActionHandler() {
        guard
            let email = _emailField.text,
            let password = _passwordField.text,
            !email.isEmpty, !password.isEmpty
        else {
            return
        }
        view.endEditing(true)
        presenter.didSelectRegisterWith(email: email, password: password)
    }
}

// MARK: UITextFieldDelegate

extension LoginSViewController: UITextFieldDelegate {

    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        view.endEditing(true)
        return true
    }
}

// MARK: - Keyboard handling

private extension LoginSViewController {
    
    private func _setupKeyboardObservers() {
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(_keyboardWillShow(_:)),
                                               name: UIResponder.keyboardWillShowNotification,
                                               object: nil)
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(_keyboardWillHide(_:)),
                                               name: UIResponder.keyboardWillHideNotification,
                                               object: nil)
    }

    @objc func _keyboardWillShow(_ notification: Notification) {
        guard
            let userInfo = notification.userInfo,
            let keyboardSize = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
        else { return }
        _scrollView.contentInset.bottom = keyboardSize.size.height
    }

    @objc func _keyboardWillHide(_ notification: Notification) {
        _scrollView.contentInset = .zero
    }
}
<file_sep>/TVShows/Common/Base VIPER/Progressable.swift
//
//  Progressable.swift
//  TVShows
//
//  Created by <NAME> on 28/07/2018.
//  Copyright © 2018 <NAME>. All rights reserved.
//

import UIKit
import PKHUD

protocol Progressable: class {
    func showLoading()
    func showLoadingInView()
    func hideLoading()
    func hideLoadingInView()
    func showSuccess()
    func showFailure(with error: Error)
}

protocol Refreshable {
    var refreshControl: UIRefreshControl { get }
}

extension Refreshable {
    func endRefreshing() {
        refreshControl.endRefreshing()
    }
}

extension Progressable where Self: WireframeInterface {
    
    func showLoading() {
        guard shouldShowSpinner() else { return }
        PKHUD.sharedHUD.dimsBackground = false
        HUD.show(.progress)
    }
    
    func showLoadingInView() {
        guard shouldShowSpinner() else { return }
        PKHUD.sharedHUD.contentView = PKHUDProgressView()
        PKHUD.sharedHUD.dimsBackground = false
        PKHUD.sharedHUD.show(onView: viewController.view)
    }
    
    func hideLoading() {
        (viewController as? Refreshable)?.endRefreshing()
        HUD.hide()
    }
    
    func hideLoadingInView() {
        (viewController as? Refreshable)?.endRefreshing()
        HUD.hide()
    }
    
    func showSuccess() {
        HUD.flash(.success, delay: 0.7)
    }
    
    func showFailure(with error: Error) {
        log.error((error as CustomDebugStringConvertible).debugDescription)
        let alertView = UIAlertController(title: nil, message: error.localizedDescription, preferredStyle: .alert)
        alertView.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
        viewController.present(alertView, animated: true, completion: nil)
    }

    private func shouldShowSpinner() -> Bool {
        if
            let refreshable = viewController as? Refreshable,
            refreshable.refreshControl.isRefreshing
        {
            return false
        }
        return true
    }
}

<file_sep>/TVShows/Modules/Login/LoginInteractor.swift
//
//  LoginInteractor.swift
//  TVShows
//
//  Created by <NAME> on 29/07/2018.
//  Copyright (c) 2018 <NAME>. All rights reserved.
//
//  This file was generated by the 🐍 VIPER generator
//

import Foundation
import Alamofire
import RxSwift

final class LoginInteractor {
    
    private let _service: Service
    private let _sessionManager: SessionManager
    
    init(service: Service = .instance, sessionManager: SessionManager = ShowsSessionManager.session) {
        _service = service
        _sessionManager = sessionManager
    }
}

// MARK: - Extensions -

extension LoginInteractor: LoginInteractorInterface {
    
    func loginWith(email: String, password: String) -> Single<Void> {
        let service = _service
        let sessionManager = _sessionManager
        
        return Single.just(())
            .map { Router.User.loginWith(email: email, password: <PASSWORD>) }
            .flatMap { service.request(LoginData.self, router: $0) }
            .do(onSuccess: { sessionManager.adapter = TokenAdapter(token: $0.token) })
            .mapToVoid()
    }
    
    func registerWith(email: String, password: String) -> Single<Void> {
        let service = _service
        
        return Single.just(())
            .map { Router.User.registerWith(email: email, password: <PASSWORD>) }
            .flatMap { service.request(User.self, router: $0) }
            .mapToVoid()
    }
}
<file_sep>/TVShows/Application/Initializers/LoggingInitializer.swift
//
//  LoggingInitializer.swift
//  TVShows
//
//  Created by <NAME> on 28/07/2018.
//  Copyright © 2018 <NAME>. All rights reserved.
//

import Foundation
import SwiftyBeaver

let log = SwiftyBeaver.self

class LoggingInitializer: Initializable {
    
    func initialize() {
        
        let console = ConsoleDestination()
        
        #if DEBUG
            log.addDestination(console)
        #endif
    }
}
<file_sep>/TVShows/Common/Base VIPER/BaseWireframe.swift
import UIKit
import RxSwift
import RxCocoa

protocol WireframeInterface: Progressable {
    var viewController: UIViewController { get }

    func open(url: URL)
    func open(stringUrl: String)
    func dismiss()
    func exit()
    func pop()
}

class BaseWireframe {

    let disposeBag = DisposeBag()

    private unowned var _viewController: UIViewController
    
    // To retain view controller reference upon first access
    private var _temporaryStoredViewController: UIViewController?

    init(viewController: UIViewController) {
        _temporaryStoredViewController = viewController
        _viewController = viewController
    }
}

extension BaseWireframe: WireframeInterface {
    
    func open(url: URL) {
        UIApplication.shared.open(url)
    }
    
    func open(stringUrl: String) {
        guard let url = URL(string: stringUrl) else { return }
        open(url: url)
    }
    
    func dismiss() {
        viewController.dismiss(animated: true)
    }
    
    func pop() {
        navigationController?.popViewController(animated: true)
    }
    
    func exit() {
        guard let navigationController = navigationController else {
            dismiss()
            return
        }
        
        if navigationController.viewControllers.count > 1 {
            pop()
        } else {
            dismiss()
        }
    }
}

extension BaseWireframe {
    
    var viewController: UIViewController {
        defer { _temporaryStoredViewController = nil }
        return _viewController
    }
    
    var navigationController: UINavigationController? {
        return viewController.navigationController
    }
    
}

extension BaseWireframe {
    
    func subscribe<W: BaseWireframe, T>(to navigationOptionDriver: Driver<T>, unowning object: W, navigationBlock: @escaping (W) -> ((T) -> ())) {
        navigationOptionDriver
            .drive(onNext: { [unowned object] in navigationBlock(object)($0) })
            .disposed(by: disposeBag)
    }
}

extension UIViewController {
    
    func presentWireframe(_ wireframe: BaseWireframe, animated: Bool = true, completion: (()->())? = nil) {
        present(wireframe.viewController, animated: animated, completion: completion)
    }
}

extension UINavigationController {
    
    func pushWireframe(_ wireframe: BaseWireframe, animated: Bool = true) {
        pushViewController(wireframe.viewController, animated: animated)
    }
    
    func setRootWireframe(_ wireframe: BaseWireframe, animated: Bool = true) {
        setViewControllers([wireframe.viewController], animated: animated)
    }
}
<file_sep>/TVShows/Modules/Home/HomePresenter.swift
//
//  HomePresenter.swift
//  TVShows
//
//  Created by <NAME> on 29/07/2018.
//  Copyright (c) 2018 <NAME>. All rights reserved.
//
//  This file was generated by the 🐍 VIPER generator
//

import UIKit
import RxSwift
import RxCocoa

final class HomePresenter {

    // MARK: - Private properties -

    private unowned let view: HomeViewInterface
    private let interactor: HomeInteractorInterface
    private let wireframe: HomeWireframeInterface
    private let _disposeBag = DisposeBag()

    // MARK: - Lifecycle -

    init(view: HomeViewInterface, interactor: HomeInteractorInterface, wireframe: HomeWireframeInterface) {
        self.view = view
        self.interactor = interactor
        self.wireframe = wireframe
    }
}

// MARK: - Extensions -

extension HomePresenter: HomePresenterInterface {

    func setup(with output: Home.ViewOutput) -> Home.ViewInput {
        output
            .logoutPressed
            .drive(onNext: { [unowned wireframe] in
                wireframe.pop()
            })
            .disposed(by: _disposeBag)
    
        let items = interactor
            .loadShows()
            .handleLoadingAndError(with: wireframe)
            .map { [unowned self] shows -> [CollectionCellItem] in
                return self.setupItems(shows)
            }
            .asDriverOnErrorComplete()

        let itemsReloader = output
            .refresh
            .startWith(()) // Manually invoke call at start
            .flatMapLatest { items }
        
        return Home.ViewInput(items: itemsReloader)
    }
}

private extension HomePresenter {
    
    func setupItems(_ shows: [Show]) -> [CollectionCellItem] {
        let items = shows.map { show -> ShowCollectionCellItem in
            var imageUrl: URL? = nil
            if let imageId = show.imageUrl {
                imageUrl = URL(string: "https://api.infinum.academy" + imageId)
            }
            let item = ShowCollectionCellItem(title: show.title, imageUrl: imageUrl) { [unowned wireframe] in
                wireframe.navigate(to: .showDetails(showId: show.id))
            }
            return item
        }
        return items
    }
}

<file_sep>/TVShows/Common/Networking/Models/User.swift
//
//  User.swift
//  TVShows
//
//  Created by <NAME> on 11/07/2018.
//  Copyright © 2018 <NAME>. All rights reserved.
//

import Foundation

struct User: Codable { /* typealias Codable = Encodable & Decodable */
    let email: String
    let type: String
    let id: String
    
    enum CodingKeys: String, CodingKey {
        case email
        case type
        case id = "_id"
    }
}

struct LoginData: Codable {
    let token: String
}
<file_sep>/TVShows/Modules/ShowDetails/ShowDetailsInteractor.swift
//
//  ShowDetailsInteractor.swift
//  TVShows
//
//  Created by <NAME> on 30/07/2018.
//  Copyright (c) 2018 <NAME>. All rights reserved.
//
//  This file was generated by the 🐍 VIPER generator
//

import Foundation
import Alamofire
import RxSwift

final class ShowDetailsInteractor {
    
    private let _service: Service
    private let _sessionManager: SessionManager
    
    init(showId: String, service: Service = .instance, sessionManager: SessionManager = ShowsSessionManager.session) {
        _service = service
        _sessionManager = sessionManager
    }
}

// MARK: - Extensions -

extension ShowDetailsInteractor: ShowDetailsInteractorInterface {
}
<file_sep>/TVShows/Common/UI/CellsAndItems/Common/DataSourceDelegate/AnimatedSelectionCollectionDataSourceDelegate.swift
//
//  AnimatedCollectionViewDataSourceDelegate.swift
//

import UIKit
import RxSwift

class AnimatedSelectionCollectionDataSourceDelegate: CollectionDataSourceDelegate {
    
    func collectionView(_ collectionView: UICollectionView, didHighlightItemAt indexPath: IndexPath) {
        guard let cell = collectionView.cellForItem(at: indexPath) else { return }
        UIView.animate(withDuration: 0.1) {
            cell.transform = CGAffineTransform(scaleX: 0.95, y: 0.95)
        }
    }
    
    func collectionView(_ collectionView: UICollectionView, didUnhighlightItemAt indexPath: IndexPath) {
        guard let cell = collectionView.cellForItem(at: indexPath) else { return }
        UIView.animate(withDuration: 0.1) {
            cell.transform = .identity
        }
    }
    
}
<file_sep>/TVShows/Common/Networking/Models/Episode.swift
//
//  Episode.swift
//  TVShows
//
//  Created by <NAME> on 24/07/2018.
//  Copyright © 2018 <NAME>. All rights reserved.
//

import Foundation

struct Episode: Codable {
    let showId: String
    let mediaId: String?
    let title: String
    let description: String
    let episodeNumber: String
    let season: String
    let type: String
    let imageUrl: String?
    let id: String
    
    enum CodingKeys: String, CodingKey {
        case showId
        case mediaId
        case title
        case description
        case episodeNumber
        case season
        case type
        case imageUrl
        case id = "_id"
    }
}
<file_sep>/TVShows/Common/UI/CellsAndItems/Common/DataSourceDelegate/IndexTitleTableDataSourceDelegate.swift
//
//  IndexTitleTableDataSourceDelegate.swift
//
//

import UIKit

protocol IndexTitleTableSectionItem {
    var title: String { get }
}

class IndexTitleTableDataSourceDelegate: TableDataSourceDelegate {
    
    func sectionIndexTitles(for tableView: UITableView) -> [String]? {
        let sections = self.sections?.compactMap { $0 as? IndexTitleTableSectionItem }
        if sections?.count != self.sections?.count { return nil }
        return sections?.map { $0.title }
    }
    
    func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int {
        let sections = self.sections?.map { $0 as? IndexTitleTableSectionItem }
        let index = sections?.firstIndex { title == $0?.title }
        return index ?? 0
    }
    
}
