<repo_name>KoryaginSergey/MVVM<file_sep>/MVVM/Source/ViewModel.xctemplate/___FILEBASENAME___ViewModel.swift
//
//  ___FILENAME___
//  ___PROJECTNAME___
//
//  Created by ___FULLUSERNAME___ on ___DATE___.
//  Copyright © ___YEAR___ ___ORGANIZATIONNAME___ All rights reserved.
//

import Foundation
import Combine

protocol ___VARIABLE_sceneName___ViewModelProtocol: AnyObject {
    var viewState: AnyPublisher<___VARIABLE_sceneName___.Models.ViewState, Never> { get }
    var viewAction: AnyPublisher<___VARIABLE_sceneName___.Models.ViewAction, Never> { get }
    var route: AnyPublisher<___VARIABLE_sceneName___.Models.ViewRoute, Never> { get }

    func process(input: ___VARIABLE_sceneName___.Models.ViewModelInput)
}

final class ___VARIABLE_sceneName___ViewModel {
    
    // MARK: - Properties
//    @Injected(container: .shared)
//    private var hapticService: HapticFeedbackServiceProtocol
    
    private let viewStateSubj = CurrentValueSubject<___VARIABLE_sceneName___.Models.ViewState, Never>(.idle)
    private let viewActionSubj = PassthroughSubject<___VARIABLE_sceneName___.Models.ViewAction, Never>()
    private let routeSubj = PassthroughSubject<___VARIABLE_sceneName___.Models.ViewRoute, Never>()
    
    private var subscriptions = Set<AnyCancellable>()
}

// MARK: - ___VARIABLE_sceneName___ViewModelProtocol
extension ___VARIABLE_sceneName___ViewModel: ___VARIABLE_sceneName___ViewModelProtocol {

    var viewState: AnyPublisher<___VARIABLE_sceneName___.Models.ViewState, Never> { viewStateSubj.eraseToAnyPublisher() }
    var viewAction: AnyPublisher<___VARIABLE_sceneName___.Models.ViewAction, Never> { viewActionSubj.eraseToAnyPublisher() }
    var route: AnyPublisher<___VARIABLE_sceneName___.Models.ViewRoute, Never> { routeSubj.eraseToAnyPublisher() }
    
    func process(input: ___VARIABLE_sceneName___.Models.ViewModelInput) {
//        input.onLoad.sink { [weak self] _ in
//            self?.fetch()
//        }.store(in: &subscriptions)
    }
}

// MARK: - Private
private extension ___VARIABLE_sceneName___ViewModel {
//    func fetch() {
//        viewStateSubj.send(.loading)
//        service.fetch.sink { [weak self] result in
//            guard let self = self else { return }
//            switch result {
//            case .success([]):
//                self.viewStateSubj.send(.empty)
//            case let .success(items):
//                self.viewStateSubj.send(.loaded(items)
//            case let .failure(error):
//                self.viewStateSubj.send(.failure(error)
//            }
//        }.store(in: &subscriptions)
//    }
}
<file_sep>/MVVM/Source/Scene.xctemplate/MVVM TableViewController/___FILEBASENAME___DataSource.swift
../../Table DataSource.xctemplate/___FILEBASENAME___DataSource.swift<file_sep>/MVVM/Source/Scene.xctemplate/MVVM CollectionViewController/___FILEBASENAME___DataSource.swift
../../Collection DataSource.xctemplate/___FILEBASENAME___DataSource.swift<file_sep>/MVVM/README.md
To install the MVVMR Xcode templates, run:

> make install_templates

To uninstall the MVVMR Xcode templates, run:

> make uninstall_templates
<file_sep>/MVVM/Source/Collection DataSource.xctemplate/___FILEBASENAME___DataSource.swift
//
//  ___FILENAME___
//  ___PROJECTNAME___
//
//  Created by ___FULLUSERNAME___ on ___DATE___.
//  Copyright © ___YEAR___ ___ORGANIZATIONNAME___ All rights reserved.
//

import UIKit

final class ___VARIABLE_sceneName___DataSource {

    // TODO: remove typealias or DataSource class from this file
    fileprivate typealias DataSource = UICollectionViewDiffableDataSource<___VARIABLE_sceneName___.Models.Section, ___VARIABLE_sceneName___.Models.Item>
    private typealias Snapshot = NSDiffableDataSourceSnapshot<___VARIABLE_sceneName___.Models.Section, ___VARIABLE_sceneName___.Models.Item>

    // MARK: - Properties
    private let collectionView: UICollectionView
    private lazy var dataSource = makeDataSource()

    // MARK: - Initializators
    init(collectionView: UICollectionView) {
        self.collectionView = collectionView
        collectionView.collectionViewLayout = makeLayout()
        collectionView.dataSource = dataSource
        makeSupplementaryProvider()
        registerReusable(in: collectionView)
    }

    // MARK: - Interface
    func updateSnapshot(_ items: [___VARIABLE_sceneName___.Models.Item], animated: Bool = true) {
        var snapshot = Snapshot()
        snapshot.appendSections([.main])
        snapshot.appendItems(items)
        dataSource.apply(snapshot, animatingDifferences: animated)
    }
}

// MARK: - Private
private extension ___VARIABLE_sceneName___DataSource {

    func registerReusable(in collectionView: UICollectionView) {
//        collectionView.register(cellType: MyCollectionCell.self)
//        collectionView.register(supplementaryViewType: MySectionView.self, ofKind: MySectionView.reuseIdentifier)
    }
}

// MARK: - DataSource
private extension ___VARIABLE_sceneName___DataSource {

    func makeDataSource() -> DataSource {
        DataSource(collectionView: collectionView) { collectionView, indexPath, item -> UICollectionViewCell? in
            switch item {
            }
        }
    }

    func makeSupplementaryProvider() {
        dataSource.supplementaryViewProvider = { [weak self] collectionView, kind, indexPath in
            return nil
//            guard let self = self else { return  nil }
//
//            let section = self.dataSource.snapshot().sectionIdentifiers[indexPath.section]
//            switch section {
//
//            }
        }
    }
}

// MARK: - Layout
private extension ___VARIABLE_sceneName___DataSource {

    func makeLayout() -> UICollectionViewCompositionalLayout {
        UICollectionViewCompositionalLayout { [weak self] index, _ -> NSCollectionLayoutSection? in
            return nil
        }
    }
}

// MARK: - DataSource class
//private extension ___VARIABLE_sceneName___DataSource {
//
//    final class DataSource: UICollectionViewDiffableDataSource<___VARIABLE_sceneName___.Models.Section, ___VARIABLE_sceneName___.Models.Item>,
//                            SkeletonCollectionViewDataSource {
//
//        func collectionSkeletonView(_ skeletonView: UICollectionView, cellIdentifierForItemAt indexPath: IndexPath) -> ReusableCellIdentifier {
//
//        }
//    }
//}
<file_sep>/MVVM/Source/Assembly.xctemplate/___FILEBASENAME___Assembly.swift
//
//  ___FILENAME___
//  ___PROJECTNAME___
//
//  Created by ___FULLUSERNAME___ on ___DATE___.
//  Copyright © ___YEAR___ ___ORGANIZATIONNAME___ All rights reserved.
//

import UIKit

extension ___VARIABLE_sceneName___ {
    enum Assembly {}
}

extension ___VARIABLE_sceneName___.Assembly {
    static func createModule(with viewModel: ___VARIABLE_sceneName___ViewModelProtocol) -> UIViewController {
        let viewController = ___VARIABLE_sceneName___ViewController.instantiate()
        viewController.setDependencies(viewModel: viewModel)
        return viewController
    }
}
<file_sep>/MVVM/Source/View Controller.xctemplate/UITableViewController/___FILEBASENAME___ViewController.swift
//
//  ___FILENAME___
//  ___PROJECTNAME___
//
//  Created by ___FULLUSERNAME___ on ___DATE___.
//  Copyright © ___YEAR___ ___ORGANIZATIONNAME___ All rights reserved.
//

import UIKit
import Combine

final class ___VARIABLE_sceneName___ViewController: UIViewController, StoryboardBased {

    // MARK: - Properties
    @IBOutlet private weak var tableView: UITableView!
    
    private lazy var dataSource = ___VARIABLE_sceneName___DataSource(tableView: tableView)
    private var viewModel: ___VARIABLE_sceneName___ViewModelProtocol?
    private let onLoad = PassthroughSubject<Void, Never>()
    
    public var subscriptions = Set<AnyCancellable>()
    
    // MARK: - Lifecycle
    override func viewDidLoad() {
        super.viewDidLoad()
        setup()
        bind(to: viewModel)
        onLoad.send(())
    }
}

// MARK: - Internal methods
extension ___VARIABLE_sceneName___ViewController {

    func setDependencies(viewModel: ___VARIABLE_sceneName___ViewModelProtocol) {
        self.viewModel = viewModel
    }
}

// MARK: - Bind
private extension ___VARIABLE_sceneName___ViewController {

    func bind(to viewModel: ___VARIABLE_sceneName___ViewModelProtocol?) {
        subscriptions.forEach { $0.cancel() }
        subscriptions.removeAll()
        
        let input = ___VARIABLE_sceneName___.Models.ViewModelInput(onLoad: onLoad.eraseToAnyPublisher())
        viewModel?.process(input: input)
        
        viewModel?.viewState
            .removeDuplicates()
            .receive(on: DispatchQueue.main)
            .sink(receiveValue: { [weak self] state in
                self?.render(state)
            }).store(in: &subscriptions)

        viewModel?.route
            .receive(on: DispatchQueue.main)
            .sink(receiveValue: { [weak self] route in
                self?.handleRoute(route)
            }).store(in: &subscriptions)

        viewModel?.viewAction
            .receive(on: DispatchQueue.main)
            .sink(receiveValue: { [weak self] action in
                self?.handleAction(action)
            }).store(in: &subscriptions)
    }
    
    func render(_ state: ___VARIABLE_sceneName___.Models.ViewState) {
        switch state {
        case .idle:
            updateSnapshot([], animated: false)
        case .loading:
            startLoading()
        case .loaded:
            stopLoading()
        case .empty:
            stopLoading()
        case .failure:
            stopLoading()
        }
    }

    func handleAction(_ action: ___VARIABLE_sceneName___.Models.ViewAction) {
        switch action {
            //show alert
            //scrollToTop
            // ...
        }
    }
    
    func handleRoute(_ route: ___VARIABLE_sceneName___.Models.ViewRoute) {
        switch route {
        }
    }
}

// MARK: - DataSource
private extension ___VARIABLE_sceneName___ViewController {

    func updateSnapshot(_ items: [___VARIABLE_sceneName___.Models.Item], animated: Bool = true) {
        dataSource.updateSnapshot(items, animated: animated)
    }
}

// MARK: - Private
private extension ___VARIABLE_sceneName___ViewController {
    
    func setup() {
    }
    
    func startLoading() {
    }
    
    func stopLoading() {
    }
}
<file_sep>/MVVM/Source/View.xctemplate/UICollectionViewCell/___FILEBASENAME___.swift
//___FILEHEADER___

import UIKit
import Reusable

final class ___FILEBASENAMEASIDENTIFIER___: ___VARIABLE_cocoaTouchSubclass___, NibReusable {

    struct State {
        
    }
    
    // MARK: - Properties
    var state: State? {
        didSet {
            configure()
        }
    }
    
    // MARK: - Lifecycle
    override func awakeFromNib() {
        super.awakeFromNib()
        setupUI()
    }
 
    /*
    override func layoutSubviews() {
        super.layoutSubviews()
    }
    
    override func prepareForReuse() {
        super.prepareForReuse()
    }
     */
}

// MARK: - Internal methods
extension ___FILEBASENAMEASIDENTIFIER___ {
    
}

// MARK: - Private methods
private extension ___FILEBASENAMEASIDENTIFIER___ {
    
    func setupUI() {
        
    }
    
    func configure() {
        
    }
}

/*
// MARK: - ___FILEBASENAMEASIDENTIFIER___.State + Hashable
extension ___FILEBASENAMEASIDENTIFIER___.State: Hashable {
    
}
 */
<file_sep>/MVVM/Source/View.xctemplate/UIViewXIB/___FILEBASENAME___.swift
//___FILEHEADER___

import UIKit
import Reusable

final class ___FILEBASENAMEASIDENTIFIER___: XibView {
    
    struct State {
        
    }
    
    // MARK: - Properties
    var state: State? {
        didSet {
            configure()
        }
    }
    
    // MARK: - Lifecycle
    override func didMoveToSuperview() {
        super.didMoveToSuperview()
        setupUI()
    }
 
    /*
    override func layoutSubviews() {
        super.layoutSubviews()
    }
     */
}

// MARK: - Internal methods
extension ___FILEBASENAMEASIDENTIFIER___ {
    
}

// MARK: - Private methods
private extension ___FILEBASENAMEASIDENTIFIER___ {
    
    func setupUI() {
        
    }
    
    func configure() {
        
    }
}

/*
// MARK: - ___FILEBASENAMEASIDENTIFIER___.State + Hashable
extension ___FILEBASENAMEASIDENTIFIER___.State: Hashable {
    
}
 */
<file_sep>/MVVM/Source/Table DataSource.xctemplate/___FILEBASENAME___DataSource.swift
//
//  ___FILENAME___
//  ___PROJECTNAME___
//
//  Created by ___FULLUSERNAME___ on ___DATE___.
//  Copyright © ___YEAR___ ___ORGANIZATIONNAME___ All rights reserved.
//

import UIKit

final class ___VARIABLE_sceneName___DataSource {

    // TODO: remove typealias or DataSource class from this file
    fileprivate typealias DataSource = UITableViewDiffableDataSource<___VARIABLE_sceneName___.Models.Section, ___VARIABLE_sceneName___.Models.Item>
    private typealias Snapshot = NSDiffableDataSourceSnapshot<___VARIABLE_sceneName___.Models.Section, ___VARIABLE_sceneName___.Models.Item>

    // MARK: - Properties
    private let tableView: UITableView
    private lazy var dataSource = makeDataSource()

    // MARK: - Initializators
    init(tableView: UITableView) {
        self.tableView = tableView
        tableView.dataSource = dataSource
        registerReusable(in: tableView)
    }

    // MARK: - Interface
    func updateSnapshot(_ items: [___VARIABLE_sceneName___.Models.Item], animated: Bool = true) {
        var snapshot = Snapshot()
        snapshot.appendSections([.main])
        snapshot.appendItems(items)
        dataSource.apply(snapshot, animatingDifferences: animated)
    }
}

// MARK: - Private
private extension ___VARIABLE_sceneName___DataSource {

    func registerReusable(in tableView: UITableView) {
//        tableView.register(cellType: MyTableCell.self)
    }
}

// MARK: - DataSource
private extension ___VARIABLE_sceneName___DataSource {

    func makeDataSource() -> DataSource {
        let source = DataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in
            switch item {
            }
        }
        source.defaultRowAnimation = .fade
        return source
    }
}

// MARK: - DataSource class
//private extension ___VARIABLE_sceneName___DataSource {
//
//    final class DataSource: UITableViewDiffableDataSource<___VARIABLE_sceneName___.Models.Section, ___VARIABLE_sceneName___.Models.Item>,
//                            SkeletonTableViewDataSource {
//
//        func collectionSkeletonView(_ skeletonView: UITableView, cellIdentifierForRowAt indexPath: IndexPath) -> ReusableCellIdentifier {
//
//        }
//    }
//}
<file_sep>/MVVM/Source/Scene.xctemplate/MVVM TableViewController/___FILEBASENAME___ViewController.swift
../../View Controller.xctemplate/UITableViewController/___FILEBASENAME___ViewController.swift<file_sep>/MVVM/Source/Scene.xctemplate/MVVM CollectionViewController/___FILEBASENAME___ViewModel.swift
../../ViewModel.xctemplate/___FILEBASENAME___ViewModel.swift<file_sep>/MVVM/Source/Models.xctemplate/___FILEBASENAME___Models.swift
//
//  ___FILENAME___
//  ___PROJECTNAME___
//
//  Created by ___FULLUSERNAME___ on ___DATE___.
//  Copyright © ___YEAR___ ___ORGANIZATIONNAME___ All rights reserved.
//

import Foundation
import Combine

enum ___VARIABLE_sceneName___ {}

extension ___VARIABLE_sceneName___ {
    enum Models {}
}

// MARK: - Models View Input/Output
extension ___VARIABLE_sceneName___.Models {

    // MARK: Input
    struct ViewModelInput {
        let onLoad: AnyPublisher<Void, Never>
    }

    // MARK: Output
    enum ViewState: Equatable {
        case idle
        case loading
        case loaded
        case empty
        case failure
    }

    enum ViewAction {
    }

    enum ViewRoute {
    }
}

// MARK: - Scene Models
//extension ___VARIABLE_sceneName___.Models {
//
//    // MARK: List Models
//    enum Section: Hashable {
//        case main
//    }
//
//    enum Item: Hashable {
//    }
//}
<file_sep>/MVVM/Source/Scene.xctemplate/MVVM CollectionViewController/___FILEBASENAME___Assembly.swift
../../Assembly.xctemplate/___FILEBASENAME___Assembly.swift<file_sep>/MVVM/Makefile
XCODE_USER_TEMPLATES_DIR=~/Library/Developer/Xcode/Templates/File\ Templates

TEMPLATES_SOURCE_DIR=Source
TEMPLATES_DIR_NAME=MVVM+Combine

install_templates:
	mkdir -p $(XCODE_USER_TEMPLATES_DIR)
	rm -fR $(XCODE_USER_TEMPLATES_DIR)/$(TEMPLATES_DIR_NAME)
	cp -LR $(TEMPLATES_SOURCE_DIR)/. $(XCODE_USER_TEMPLATES_DIR)/${TEMPLATES_DIR_NAME}

uninstall_templates:
	rm -fR $(XCODE_USER_TEMPLATES_DIR)/$(TEMPLATES_DIR_NAME)
