From 829e519d8ea9cfaacdf95a17176f4388cb2d8076 Mon Sep 17 00:00:00 2001 From: Aarif Sumra Date: Tue, 25 May 2021 13:59:37 +0900 Subject: [PATCH] fix: error building and added latest code --- .../{{cookiecutter.domain_model}}.swift | 21 +++-- {{cookiecutter.app_name}}/Domain/Entity.swift | 13 +++ .../Domain/UseCase.swift | 13 +++ .../Network/Endpoints/Endpoints.swift | 26 +++++ ...cookiecutter.domain_model}}sEndpoint.swift | 18 ---- .../Platform/Network/Network.swift | 4 + .../Platform/Protocols/Endpoint.swift | 6 +- .../Platform/Protocols/RepositoryType.swift | 19 +++- ...okiecutter.domain_model}}sRepository.swift | 36 ++++--- .../Repository/RemoteRepository.swift | 67 +++++++++---- .../Repository/RepositoryProvider.swift | 3 +- .../Platform/UseCases/UseCaseProvider.swift | 5 +- ...ecutter.domain_model|lower}}sUseCase.swift | 4 +- {{cookiecutter.app_name}}/Podfile | 4 +- .../project.pbxproj | 60 ++++-------- .../Source/App/App.swift | 4 +- .../Source/App/AppCoordinator.swift | 17 +--- .../Base/Extensions/UIButton+Combinable.swift | 22 ----- .../Extensions/UIControl+Combinable.swift | 85 ----------------- .../Extensions/UITextField+Combinable.swift | 22 ----- ...oller.swift => MainTabBarController.swift} | 4 +- ...ator.swift => MainTabBarCoordinator.swift} | 18 ++-- ...okiecutter.domain_model}}Detail.storyboard | 32 ++++++- ...tter.domain_model}}DetailCoordinator.swift | 35 ++----- ...okiecutter.domain_model}}DetailScene.swift | 16 ++-- ...r.domain_model}}DetailViewController.swift | 36 ++++++- ...cutter.domain_model}}DetailViewModel.swift | 47 ++++++++-- ...{cookiecutter.domain_model}}ListCell.swift | 59 ------------ .../{{cookiecutter.domain_model}}ListCell.xib | 40 -------- ...cookiecutter.domain_model}}List.storyboard | 23 ++--- ...cutter.domain_model}}ListCoordinator.swift | 31 +++--- ...cookiecutter.domain_model}}ListScene.swift | 18 ++-- ...ter.domain_model}}ListViewController.swift | 94 ++++++++++++------- ...iecutter.domain_model}}ListViewModel.swift | 48 +++++----- .../Scenes/Login/LoginCoordinator.swift | 11 +-- .../Source/Scenes/Login/LoginScene.swift | 18 ++-- .../Scenes/Login/LoginViewController.swift | 27 +++++- .../Source/Scenes/Login/LoginViewModel.swift | 14 ++- 38 files changed, 483 insertions(+), 537 deletions(-) create mode 100644 {{cookiecutter.app_name}}/Domain/Entity.swift create mode 100644 {{cookiecutter.app_name}}/Domain/UseCase.swift create mode 100644 {{cookiecutter.app_name}}/Platform/Network/Endpoints/Endpoints.swift delete mode 100644 {{cookiecutter.app_name}}/Platform/Network/Endpoints/{{cookiecutter.domain_model}}sEndpoint.swift delete mode 100644 {{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Base/Extensions/UIButton+Combinable.swift delete mode 100644 {{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Base/Extensions/UIControl+Combinable.swift delete mode 100644 {{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Base/Extensions/UITextField+Combinable.swift rename {{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{HomeTabBarController.swift => MainTabBarController.swift} (88%) rename {{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{HomeTabBarCoordinator.swift => MainTabBarCoordinator.swift} (74%) delete mode 100644 {{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/List/Item/{{cookiecutter.domain_model}}ListCell.swift delete mode 100644 {{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/List/Item/{{cookiecutter.domain_model}}ListCell.xib diff --git a/{{cookiecutter.app_name}}/Domain/Entities/{{cookiecutter.domain_model}}.swift b/{{cookiecutter.app_name}}/Domain/Entities/{{cookiecutter.domain_model}}.swift index 2513be4..41008f8 100644 --- a/{{cookiecutter.app_name}}/Domain/Entities/{{cookiecutter.domain_model}}.swift +++ b/{{cookiecutter.app_name}}/Domain/Entities/{{cookiecutter.domain_model}}.swift @@ -8,20 +8,21 @@ import Foundation +public protocol {{cookiecutter.domain_model}}Entity: Entity { + // Required + var id: String { get } + var title: String { get } + // `Optional`s + var body: String? { get } +} + +// Conformance to codable done here because it can not be done as extension. Proper place would be in the Platform module public struct {{cookiecutter.domain_model}}: Codable { // Required public let id: Int public let title: String - public let posterPath: String? - // Optionals - public private(set) var status: String? - - public init(id: Int, title: String, posterPath: String?, status: String? = nil) { - self.id = id - self.title = title - self.posterPath = posterPath - self.status = status - } + // `Optional`s + public private(set) var body: String? = nil } extension {{cookiecutter.domain_model}}: Hashable { diff --git a/{{cookiecutter.app_name}}/Domain/Entity.swift b/{{cookiecutter.app_name}}/Domain/Entity.swift new file mode 100644 index 0000000..1aab291 --- /dev/null +++ b/{{cookiecutter.app_name}}/Domain/Entity.swift @@ -0,0 +1,13 @@ +// +// Entity.swift +// Domain +// +// Created by {{cookiecutter.lead_dev_name}} on {% now 'local' %}. +// Copyright © {% now 'local', '%Y' %} {{cookiecutter.company_name}} All rights reserved. +// + +import Foundation + +public protocol Entity { + var id: Int { get } +} diff --git a/{{cookiecutter.app_name}}/Domain/UseCase.swift b/{{cookiecutter.app_name}}/Domain/UseCase.swift new file mode 100644 index 0000000..3bb2ec7 --- /dev/null +++ b/{{cookiecutter.app_name}}/Domain/UseCase.swift @@ -0,0 +1,13 @@ +// +// UseCase.swift +// Domain +// +// Created by {{cookiecutter.lead_dev_name}} on {% now 'local' %}. +// Copyright © {% now 'local', '%Y' %} {{cookiecutter.company_name}} All rights reserved. +// + +import Foundation + +public protocol UseCase { + +} diff --git a/{{cookiecutter.app_name}}/Platform/Network/Endpoints/Endpoints.swift b/{{cookiecutter.app_name}}/Platform/Network/Endpoints/Endpoints.swift new file mode 100644 index 0000000..6677e98 --- /dev/null +++ b/{{cookiecutter.app_name}}/Platform/Network/Endpoints/Endpoints.swift @@ -0,0 +1,26 @@ +// +// {{cookiecutter.domain_model}}sEndpoint.swift +// Platform +// +// Created by {{cookiecutter.lead_dev_name}} on {% now 'local' %}. +// Copyright © 2021 {{cookiecutter.company_name}} All rights reserved. +// + +import Domain + +enum Endpoints: Endpoint { + case {{cookiecutter.domain_model|lower}}s + + var relativePath: String { + switch self { + case .{{cookiecutter.domain_model|lower}}s: + return "{{cookiecutter.domain_model|lower}}s" + } + } + + var headers: [String : String] { + return [ + "Content-Type": "application/json" + ] + } +} diff --git a/{{cookiecutter.app_name}}/Platform/Network/Endpoints/{{cookiecutter.domain_model}}sEndpoint.swift b/{{cookiecutter.app_name}}/Platform/Network/Endpoints/{{cookiecutter.domain_model}}sEndpoint.swift deleted file mode 100644 index 1909cfc..0000000 --- a/{{cookiecutter.app_name}}/Platform/Network/Endpoints/{{cookiecutter.domain_model}}sEndpoint.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// {{cookiecutter.domain_model}}sEndpoint.swift -// Platform -// -// Created by {{cookiecutter.lead_dev_name}} on {% now 'local' %}. -// Copyright © 2021 {{cookiecutter.company_name}} All rights reserved. -// - -import Domain - -struct {{cookiecutter.domain_model}}sEndpoint: Endpoint { - typealias Resource = Domain.{{cookiecutter.domain_model}} - static let method: HTTPMethod = .get - static let relativePath = "{{cookiecutter.domain_model|lower}}" - static let headers = [ - "Content-Type": "application/json" - ] -} diff --git a/{{cookiecutter.app_name}}/Platform/Network/Network.swift b/{{cookiecutter.app_name}}/Platform/Network/Network.swift index 440187b..fade6ea 100644 --- a/{{cookiecutter.app_name}}/Platform/Network/Network.swift +++ b/{{cookiecutter.app_name}}/Platform/Network/Network.swift @@ -13,6 +13,10 @@ public class Network: Networking { public init(session: URLSession) { self.session = session } + + static let `default` = { + Network(session: URLSession.shared) + }() public func send(_ request: URLRequest, completion: @escaping (Result) -> Void) { let task = session.dataTask(with: request) { data, response, error in diff --git a/{{cookiecutter.app_name}}/Platform/Protocols/Endpoint.swift b/{{cookiecutter.app_name}}/Platform/Protocols/Endpoint.swift index 6a16838..2f5b55b 100644 --- a/{{cookiecutter.app_name}}/Platform/Protocols/Endpoint.swift +++ b/{{cookiecutter.app_name}}/Platform/Protocols/Endpoint.swift @@ -9,8 +9,6 @@ import Domain protocol Endpoint { - associatedtype Resource: Codable - static var method: HTTPMethod { get } - static var relativePath: String { get } - static var headers: [String: String] { get } + var relativePath: String { get } + var headers: [String: String] { get } } diff --git a/{{cookiecutter.app_name}}/Platform/Protocols/RepositoryType.swift b/{{cookiecutter.app_name}}/Platform/Protocols/RepositoryType.swift index eab1165..341ef73 100644 --- a/{{cookiecutter.app_name}}/Platform/Protocols/RepositoryType.swift +++ b/{{cookiecutter.app_name}}/Platform/Protocols/RepositoryType.swift @@ -9,16 +9,17 @@ import Domain import Combine -public enum RepositoryError: Error { +enum RepositoryError: Error { case queryFailed(Error) case saveFailed case deleteFailed } -public protocol RepositoryType: class { +protocol RepositoryType: class { associatedtype T func queryAll(_ completion: @escaping (Result<[T], Error>) -> Void) - func query(with queryString: String, completion: @escaping (Result<[T], Error>) -> Void) + func query(withId id: Int, completion: @escaping (Result) -> Void) + func query(withQueryItems queryItems: [URLQueryItem], completion: @escaping (Result<[T], Error>) -> Void) func save(entity: T, completion: @escaping (Error?) -> Void) func delete(entity: T, completion: @escaping (Error?) -> Void) } @@ -29,9 +30,17 @@ extension RepositoryType where Self: Combinable { Future(queryAll).eraseToAnyPublisher() } - func queryPublisher(for queryString: String) -> AnyPublisher<[T], Error> { + func queryPublisher(forId id: Int) -> AnyPublisher { Future { promise in - self.query(with: queryString) { result in + self.query(withId: id) { result in + promise(result) + } + }.eraseToAnyPublisher() + } + + func queryPublisher(forQueryItems queryItems: [URLQueryItem]) -> AnyPublisher<[T], Error> { + Future { promise in + self.query(withQueryItems: queryItems) { result in promise(result) } }.eraseToAnyPublisher() diff --git a/{{cookiecutter.app_name}}/Platform/Repository/List/{{cookiecutter.domain_model}}sRepository.swift b/{{cookiecutter.app_name}}/Platform/Repository/List/{{cookiecutter.domain_model}}sRepository.swift index a082da3..dceb218 100644 --- a/{{cookiecutter.app_name}}/Platform/Repository/List/{{cookiecutter.domain_model}}sRepository.swift +++ b/{{cookiecutter.app_name}}/Platform/Repository/List/{{cookiecutter.domain_model}}sRepository.swift @@ -15,33 +15,31 @@ final class {{cookiecutter.domain_model}}sRepository { case notFound } - private let repository: RemoteRepository<{{cookiecutter.domain_model}}sEndpoint> + private let repository: RemoteRepository<{{cookiecutter.domain_model}}> - init(repository: RemoteRepository<{{cookiecutter.domain_model}}sEndpoint>) { + init(repository: RemoteRepository<{{cookiecutter.domain_model}}>) { self.repository = repository } func fetch{{cookiecutter.domain_model}}s() -> AnyPublisher<[{{cookiecutter.domain_model}}], Error> { repository.queryPublisherForAll() } - - func fetch{{cookiecutter.domain_model}}(with id: Int) -> AnyPublisher<{{cookiecutter.domain_model}}, Error> { - repository.queryPublisher(for: "id=\(id)") - .flatMap { items -> Result<{{cookiecutter.domain_model}}, Error>.Publisher in - guard !items.isEmpty else { - return .init(CustomError.notFound) - } - return .init(items[0]) - }.eraseToAnyPublisher() + + func fetch(withId id: Int) -> AnyPublisher<{{cookiecutter.domain_model}}, Error> { + repository.queryPublisher(forId: id) + .eraseToAnyPublisher() } - func search{{cookiecutter.domain_model}}(with name: String) -> AnyPublisher<[{{cookiecutter.domain_model}}], Error> { - repository.queryPublisher(for: "name=\(name)") - .flatMap { items -> Result<[{{cookiecutter.domain_model}}], Error>.Publisher in - guard !items.isEmpty else { - return .init(CustomError.notFound) - } - return .init(items) - }.eraseToAnyPublisher() + func search(with query: String) -> AnyPublisher<[{{cookiecutter.domain_model}}], Error> { + repository.queryPublisher(forQueryItems: [ + URLQueryItem(name: "query", value: query), + // URLQueryItem(name: "page", value: "\(page)") + ]) + .flatMap { items -> Result<[{{cookiecutter.domain_model}}], Error>.Publisher in + guard !items.isEmpty else { + return .init(CustomError.notFound) + } + return .init(items) + }.eraseToAnyPublisher() } } diff --git a/{{cookiecutter.app_name}}/Platform/Repository/RemoteRepository.swift b/{{cookiecutter.app_name}}/Platform/Repository/RemoteRepository.swift index 21f0afd..4b56fa0 100644 --- a/{{cookiecutter.app_name}}/Platform/Repository/RemoteRepository.swift +++ b/{{cookiecutter.app_name}}/Platform/Repository/RemoteRepository.swift @@ -9,11 +9,12 @@ import Domain import Combine -class RemoteRepository: RepositoryType, Combinable { +class RemoteRepository: RepositoryType, Combinable { let network: Networking private let baseURL: URL + private let endpoint: Endpoint private let decoder: JSONDecoder = { let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 @@ -23,22 +24,23 @@ class RemoteRepository: RepositoryType, Combinable { return decoder }() private var endpointURL: URL { - baseURL.appendingPathComponent(E.relativePath) + baseURL.appendingPathComponent(endpoint.relativePath) } - public init(baseURL: URL, network: Networking) { + init(baseURL: URL, network: Networking, endpoint: Endpoint) { self.baseURL = baseURL self.network = network + self.endpoint = endpoint } - func queryAll(_ completion: @escaping (Result<[E.Resource], Error>) -> Void) { + func queryAll(_ completion: @escaping (Result<[T], Error>) -> Void) { let request = URLRequest(url: self.endpointURL) network.send(request) { result in switch result { case .success(let data): do { - let list = try self.decoder.decode(List.self, from: data) - completion(.success(list.results)) + let list = try self.decoder.decode([T].self, from: data) + completion(.success(list)) } catch let error { completion(.failure(error)) // DecodingError } @@ -48,13 +50,30 @@ class RemoteRepository: RepositoryType, Combinable { } } - func query(with queryString: String, completion: @escaping (Result<[E.Resource], Error>) -> Void) { - guard var urlComponents = URLComponents(string: baseURL.absoluteString) else { + func query(withId id: Int, completion: @escaping (Result) -> Void) { + let url = self.endpointURL.appendingPathComponent("\(id)") + let request = URLRequest(url: url) + network.send(request) { result in + switch result { + case .success(let data): + do { + let item = try self.decoder.decode(T.self, from: data) + completion(.success(item)) + } catch let error { + completion(.failure(error)) // DecodingError + } + case .failure(let error): + completion(.failure(error)) // NetworkError + } + } + } + + func query(withQueryItems queryItems: [URLQueryItem], completion: @escaping (Result<[T], Error>) -> Void) { + guard var urlComponents = URLComponents(url: endpointURL, resolvingAgainstBaseURL: false) else { return completion(.failure(URLError(.badURL))) } - urlComponents.path = E.relativePath - urlComponents.query = queryString + urlComponents.queryItems?.append(contentsOf: queryItems) guard let url = urlComponents.url else { return completion(.failure(URLError(.badURL))) @@ -65,7 +84,7 @@ class RemoteRepository: RepositoryType, Combinable { switch result { case .success(let data): do { - let list = try self.decoder.decode(List.self, from: data) + let list = try self.decoder.decode(List.self, from: data) completion(.success(list.results)) } catch let error { completion(.failure(error)) // DecodingError @@ -76,10 +95,10 @@ class RemoteRepository: RepositoryType, Combinable { } } - func save(entity: E.Resource, completion: @escaping (Error?) -> Void) { - let url = baseURL.appendingPathComponent(E.relativePath) + func save(entity: T, completion: @escaping (Error?) -> Void) { + let url = baseURL.appendingPathComponent(endpoint.relativePath) var request = URLRequest(url: url) - request.httpMethod = E.method.rawValue + request.httpMethod = HTTPMethod.post.rawValue // TODO: What about update PATCH? request.httpBody = try? JSONEncoder().encode(entity) self.network.send(request) { result in switch result { @@ -99,8 +118,8 @@ class RemoteRepository: RepositoryType, Combinable { } } - func delete(entity: E.Resource, completion: @escaping (Error?) -> Void) { - let url = baseURL.appendingPathComponent(E.relativePath) + func delete(entity: T, completion: @escaping (Error?) -> Void) { + let url = baseURL.appendingPathComponent(endpoint.relativePath) let request = URLRequest(url: url) self.network.send(request) { result in switch result { @@ -125,3 +144,19 @@ private struct List: Decodable { let totalPages: Int let results: [T] } + +private extension URL { + + func queryItemAdded(name: String, value: String?) -> URL? { + return self.queryItemsAdded([URLQueryItem(name: name, value: value)]) + } + + func queryItemsAdded(_ queryItems: [URLQueryItem]) -> URL? { + guard var components = URLComponents(url: self, resolvingAgainstBaseURL: nil != self.baseURL) else { + return nil + } + components.queryItems = queryItems + (components.queryItems ?? []) + return components.url + } + +} diff --git a/{{cookiecutter.app_name}}/Platform/Repository/RepositoryProvider.swift b/{{cookiecutter.app_name}}/Platform/Repository/RepositoryProvider.swift index f19209c..e1fe5ae 100644 --- a/{{cookiecutter.app_name}}/Platform/Repository/RepositoryProvider.swift +++ b/{{cookiecutter.app_name}}/Platform/Repository/RepositoryProvider.swift @@ -22,7 +22,8 @@ final class RepositoryProvider { {{cookiecutter.domain_model}}sRepository( repository: .init( baseURL: baseURL, - network: network + network: network, + endpoint: Endpoints.{{cookiecutter.domain_model|lower}}s ) ) } diff --git a/{{cookiecutter.app_name}}/Platform/UseCases/UseCaseProvider.swift b/{{cookiecutter.app_name}}/Platform/UseCases/UseCaseProvider.swift index ebf6cc3..d3b84a7 100644 --- a/{{cookiecutter.app_name}}/Platform/UseCases/UseCaseProvider.swift +++ b/{{cookiecutter.app_name}}/Platform/UseCases/UseCaseProvider.swift @@ -17,7 +17,8 @@ public final class UseCaseProvider: Domain.UseCaseProvider { } public func make{{cookiecutter.domain_model}}sUseCase() -> Domain.{{cookiecutter.domain_model}}sUseCase { - let repo = repositoryProvider.make{{cookiecutter.domain_model}}sRepository() - return {{cookiecutter.domain_model}}sUseCase(repository: repo) + {{cookiecutter.domain_model}}sUseCase( + repository: repositoryProvider.makePostsRepository() + ) } } diff --git a/{{cookiecutter.app_name}}/Platform/UseCases/{{cookiecutter.domain_model|lower}}sUseCase.swift b/{{cookiecutter.app_name}}/Platform/UseCases/{{cookiecutter.domain_model|lower}}sUseCase.swift index 3255c42..a9cea4a 100644 --- a/{{cookiecutter.app_name}}/Platform/UseCases/{{cookiecutter.domain_model|lower}}sUseCase.swift +++ b/{{cookiecutter.app_name}}/Platform/UseCases/{{cookiecutter.domain_model|lower}}sUseCase.swift @@ -22,10 +22,10 @@ final class {{cookiecutter.domain_model}}sUseCase: Domain.{{cookiecutter.domain_ } func fetch(with id: Int) -> AnyPublisher<{{cookiecutter.domain_model}}, Error> { - repository.fetch{{cookiecutter.domain_model}}(with: id) + repository.fetch(withId: id) } func search(with name: String) -> AnyPublisher<[{{cookiecutter.domain_model}}], Error> { - repository.search{{cookiecutter.domain_model}}(with: name) + repository.search(with: name) } } diff --git a/{{cookiecutter.app_name}}/Podfile b/{{cookiecutter.app_name}}/Podfile index 07a2536..d784e09 100644 --- a/{{cookiecutter.app_name}}/Podfile +++ b/{{cookiecutter.app_name}}/Podfile @@ -13,7 +13,9 @@ target '{{cookiecutter.app_name}}' do pod 'NStackSDK', '~> 5.0' # External - pod 'Kingfisher', '~> 6.0' + # pod 'Kingfisher', '~> 6.0' + pod 'CombineCocoa' + pod 'CombineExt' post_install do |installer| installer.pods_project.targets.each do |target| diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}.xcodeproj/project.pbxproj b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}.xcodeproj/project.pbxproj index 97b0493..2fd8128 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}.xcodeproj/project.pbxproj +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}.xcodeproj/project.pbxproj @@ -7,11 +7,7 @@ objects = { /* Begin PBXBuildFile section */ - 0806B9E42581C09200A07063 /* HomeTabBarCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0806B9E32581C09200A07063 /* HomeTabBarCoordinator.swift */; }; - 080C1DBA256F7D020036730D /* UIControl+Combinable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080C1DB9256F7D020036730D /* UIControl+Combinable.swift */; }; 080C1DBF256FB7C00036730D /* Combinable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080C1DBE256FB7C00036730D /* Combinable.swift */; }; - 080C1DCA256FB9670036730D /* UITextField+Combinable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080C1DC9256FB9670036730D /* UITextField+Combinable.swift */; }; - 080C1DD5256FBFED0036730D /* UIButton+Combinable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080C1DD4256FBFED0036730D /* UIButton+Combinable.swift */; }; 081267682570C016008CA5AC /* MirrorObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081267672570C016008CA5AC /* MirrorObject.swift */; }; 081F263B258B60BF00288215 /* {{cookiecutter.domain_model}}Detail.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 081F263A258B60BF00288215 /* {{cookiecutter.domain_model}}Detail.storyboard */; }; 081F2667258C939700288215 /* Scene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081F2663258C939700288215 /* Scene.swift */; }; @@ -43,7 +39,6 @@ 083194502583518A0063F84D /* LoginCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0831944F2583518A0063F84D /* LoginCoordinator.swift */; }; 08428B0425A9B9ED00D4AAD9 /* Domain.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 081F26CF2591D5F000288215 /* Domain.framework */; }; 08428B1B25A9C85200D4AAD9 /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08428B1A25A9C85200D4AAD9 /* HTTPMethod.swift */; }; - 08428B8B25AD488400D4AAD9 /* {{cookiecutter.domain_model}}sEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08428B8A25AD488400D4AAD9 /* {{cookiecutter.domain_model}}sEndpoint.swift */; }; 08428B9325AD4AD500D4AAD9 /* UIDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08428B9225AD4AD500D4AAD9 /* UIDevice.swift */; }; 08428B9B25AD4BFA00D4AAD9 /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08428B9A25AD4BFA00D4AAD9 /* Bundle.swift */; }; 08497D37259461BD00462DE1 /* Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08497D36259461BD00462DE1 /* Endpoint.swift */; }; @@ -54,7 +49,6 @@ 088A8B4C256239B300AFF5AD /* App+Style.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088A8B4A256239B300AFF5AD /* App+Style.swift */; }; 089BA9D52593399D0098A58B /* DomainEntityConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089BA9D42593399D0098A58B /* DomainEntityConvertible.swift */; }; 08CD05182575D8EF00FAB7EA /* LoginScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08CD05172575D8EF00FAB7EA /* LoginScene.swift */; }; - 08CD05202575DC1C00FAB7EA /* HomeTabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08CD051F2575DC1C00FAB7EA /* HomeTabBarController.swift */; }; 08CD05252575DC7400FAB7EA /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08CD05242575DC7400FAB7EA /* SearchViewController.swift */; }; 08CD052A2575DC9B00FAB7EA /* {{cookiecutter.domain_model}}ListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08CD05292575DC9B00FAB7EA /* {{cookiecutter.domain_model}}ListViewController.swift */; }; 08CD05372575DCDA00FAB7EA /* {{cookiecutter.domain_model}}ListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08CD05362575DCDA00FAB7EA /* {{cookiecutter.domain_model}}ListViewModel.swift */; }; @@ -63,8 +57,6 @@ 08CD054E2575F33E00FAB7EA /* {{cookiecutter.domain_model}}DetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08CD054D2575F33E00FAB7EA /* {{cookiecutter.domain_model}}DetailViewModel.swift */; }; 08CD055B2575F37400FAB7EA /* {{cookiecutter.domain_model}}DetailScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08CD055A2575F37400FAB7EA /* {{cookiecutter.domain_model}}DetailScene.swift */; }; 08CD05602575F46E00FAB7EA /* {{cookiecutter.domain_model}}List.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 08CD055F2575F46E00FAB7EA /* {{cookiecutter.domain_model}}List.storyboard */; }; - 08CD05692576069900FAB7EA /* {{cookiecutter.domain_model}}ListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08CD05672576069900FAB7EA /* {{cookiecutter.domain_model}}ListCell.swift */; }; - 08CD056A2576069900FAB7EA /* {{cookiecutter.domain_model}}ListCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 08CD05682576069900FAB7EA /* {{cookiecutter.domain_model}}ListCell.xib */; }; 08CD064E2578A34E00FAB7EA /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08CD064D2578A34E00FAB7EA /* AppCoordinator.swift */; }; 08CE2A1C25A7E349004416A8 /* RemoteRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08CE2A1B25A7E349004416A8 /* RemoteRepository.swift */; }; 08CE2A2425A7E411004416A8 /* Combinable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08CE2A2325A7E411004416A8 /* Combinable.swift */; }; @@ -83,6 +75,11 @@ 08D43A2225593061001EFC43 /* Login.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 08D43A1F25593061001EFC43 /* Login.storyboard */; }; 08D43A2325593061001EFC43 /* LoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08D43A2025593061001EFC43 /* LoginViewModel.swift */; }; 1F22DAC811D9A85989C80D12 /* Pods_PlatformTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 56DC9A2FC52ADBB838EA5912 /* Pods_PlatformTests.framework */; }; + 247FC22D265C9CFB00836EB7 /* UseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 247FC22B265C9CFB00836EB7 /* UseCase.swift */; }; + 247FC22E265C9CFB00836EB7 /* Entity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 247FC22C265C9CFB00836EB7 /* Entity.swift */; }; + 247FC256265C9D4B00836EB7 /* Endpoints.swift in Sources */ = {isa = PBXBuildFile; fileRef = 247FC255265C9D4B00836EB7 /* Endpoints.swift */; }; + 247FC25F265C9E2B00836EB7 /* MainTabBarCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 247FC25D265C9E2B00836EB7 /* MainTabBarCoordinator.swift */; }; + 247FC260265C9E2B00836EB7 /* MainTabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 247FC25E265C9E2B00836EB7 /* MainTabBarController.swift */; }; 8CAA51642567A3BB000E166C /* UIScrollView+KeyboardContentInsettable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CAA51632567A3BB000E166C /* UIScrollView+KeyboardContentInsettable.swift */; }; 8CF927BF2566C1B800404F59 /* Localizations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CF927BD2566C1B800404F59 /* Localizations.swift */; }; 8CF927C02566C1B800404F59 /* SKLocalizations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CF927BE2566C1B800404F59 /* SKLocalizations.swift */; }; @@ -176,11 +173,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 0806B9E32581C09200A07063 /* HomeTabBarCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTabBarCoordinator.swift; sourceTree = ""; }; - 080C1DB9256F7D020036730D /* UIControl+Combinable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIControl+Combinable.swift"; sourceTree = ""; }; 080C1DBE256FB7C00036730D /* Combinable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Combinable.swift; sourceTree = ""; }; - 080C1DC9256FB9670036730D /* UITextField+Combinable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextField+Combinable.swift"; sourceTree = ""; }; - 080C1DD4256FBFED0036730D /* UIButton+Combinable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIButton+Combinable.swift"; sourceTree = ""; }; 081267672570C016008CA5AC /* MirrorObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MirrorObject.swift; sourceTree = ""; }; 081F263A258B60BF00288215 /* {{cookiecutter.domain_model}}Detail.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = {{cookiecutter.domain_model}}Detail.storyboard; sourceTree = ""; }; 081F2663258C939700288215 /* Scene.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Scene.swift; sourceTree = ""; }; @@ -213,7 +206,6 @@ 081F27852591F68F00288215 /* Network.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Network.swift; sourceTree = ""; }; 0831944F2583518A0063F84D /* LoginCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginCoordinator.swift; sourceTree = ""; }; 08428B1A25A9C85200D4AAD9 /* HTTPMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPMethod.swift; sourceTree = ""; }; - 08428B8A25AD488400D4AAD9 /* {{cookiecutter.domain_model}}sEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = {{cookiecutter.domain_model}}sEndpoint.swift; sourceTree = ""; }; 08428B9225AD4AD500D4AAD9 /* UIDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIDevice.swift; sourceTree = ""; }; 08428B9A25AD4BFA00D4AAD9 /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = ""; }; 08497D36259461BD00462DE1 /* Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Endpoint.swift; sourceTree = ""; }; @@ -234,7 +226,6 @@ 088A8B4A256239B300AFF5AD /* App+Style.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "App+Style.swift"; sourceTree = ""; }; 089BA9D42593399D0098A58B /* DomainEntityConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainEntityConvertible.swift; sourceTree = ""; }; 08CD05172575D8EF00FAB7EA /* LoginScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScene.swift; sourceTree = ""; }; - 08CD051F2575DC1C00FAB7EA /* HomeTabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTabBarController.swift; sourceTree = ""; }; 08CD05242575DC7400FAB7EA /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = ""; }; 08CD05292575DC9B00FAB7EA /* {{cookiecutter.domain_model}}ListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = {{cookiecutter.domain_model}}ListViewController.swift; sourceTree = ""; }; 08CD05362575DCDA00FAB7EA /* {{cookiecutter.domain_model}}ListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = {{cookiecutter.domain_model}}ListViewModel.swift; sourceTree = ""; }; @@ -243,8 +234,6 @@ 08CD054D2575F33E00FAB7EA /* {{cookiecutter.domain_model}}DetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = {{cookiecutter.domain_model}}DetailViewModel.swift; sourceTree = ""; }; 08CD055A2575F37400FAB7EA /* {{cookiecutter.domain_model}}DetailScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = {{cookiecutter.domain_model}}DetailScene.swift; sourceTree = ""; }; 08CD055F2575F46E00FAB7EA /* {{cookiecutter.domain_model}}List.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = {{cookiecutter.domain_model}}List.storyboard; sourceTree = ""; }; - 08CD05672576069900FAB7EA /* {{cookiecutter.domain_model}}ListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = {{cookiecutter.domain_model}}ListCell.swift; sourceTree = ""; }; - 08CD05682576069900FAB7EA /* {{cookiecutter.domain_model}}ListCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = {{cookiecutter.domain_model}}ListCell.xib; sourceTree = ""; }; 08CD064D2578A34E00FAB7EA /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; }; 08CE2A1B25A7E349004416A8 /* RemoteRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteRepository.swift; sourceTree = ""; }; 08CE2A2325A7E411004416A8 /* Combinable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Combinable.swift; sourceTree = ""; }; @@ -269,6 +258,11 @@ 08D43A1F25593061001EFC43 /* Login.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Login.storyboard; sourceTree = ""; }; 08D43A2025593061001EFC43 /* LoginViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginViewModel.swift; sourceTree = ""; }; 099F81A5F0B262F57078D2CB /* Pods-Domain.staging.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Domain.staging.xcconfig"; path = "Target Support Files/Pods-Domain/Pods-Domain.staging.xcconfig"; sourceTree = ""; }; + 247FC22B265C9CFB00836EB7 /* UseCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UseCase.swift; sourceTree = ""; }; + 247FC22C265C9CFB00836EB7 /* Entity.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Entity.swift; sourceTree = ""; }; + 247FC255265C9D4B00836EB7 /* Endpoints.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Endpoints.swift; sourceTree = ""; }; + 247FC25D265C9E2B00836EB7 /* MainTabBarCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainTabBarCoordinator.swift; sourceTree = ""; }; + 247FC25E265C9E2B00836EB7 /* MainTabBarController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainTabBarController.swift; sourceTree = ""; }; 289BB38960CDE0C8AF1BB70C /* Pods-Platform.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Platform.release.xcconfig"; path = "Target Support Files/Pods-Platform/Pods-Platform.release.xcconfig"; sourceTree = ""; }; 56DC9A2FC52ADBB838EA5912 /* Pods_PlatformTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_PlatformTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 76682FEB23A05B94C2BFC06A /* Pods_{{cookiecutter.app_name}}.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_{{cookiecutter.app_name}}.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -404,6 +398,8 @@ isa = PBXGroup; children = ( 081F26D12591D5F000288215 /* Domain.h */, + 247FC22C265C9CFB00836EB7 /* Entity.swift */, + 247FC22B265C9CFB00836EB7 /* UseCase.swift */, 081F27452591EA5400288215 /* Entities */, 081F27572591ED2100288215 /* UseCases */, 081F26D22591D5F000288215 /* Info.plist */, @@ -472,7 +468,7 @@ 08428BA225AD4DC900D4AAD9 /* Endpoints */ = { isa = PBXGroup; children = ( - 08428B8A25AD488400D4AAD9 /* {{cookiecutter.domain_model}}sEndpoint.swift */, + 247FC255265C9D4B00836EB7 /* Endpoints.swift */, ); path = Endpoints; sourceTree = ""; @@ -571,7 +567,6 @@ 08D3090225877B8B00390276 /* {{cookiecutter.domain_model}}ListCoordinator.swift */, 08CD05292575DC9B00FAB7EA /* {{cookiecutter.domain_model}}ListViewController.swift */, 08CD05362575DCDA00FAB7EA /* {{cookiecutter.domain_model}}ListViewModel.swift */, - 08CD056E257606A600FAB7EA /* Item */, ); path = List; sourceTree = ""; @@ -588,15 +583,6 @@ path = Detail; sourceTree = ""; }; - 08CD056E257606A600FAB7EA /* Item */ = { - isa = PBXGroup; - children = ( - 08CD05672576069900FAB7EA /* {{cookiecutter.domain_model}}ListCell.swift */, - 08CD05682576069900FAB7EA /* {{cookiecutter.domain_model}}ListCell.xib */, - ); - path = Item; - sourceTree = ""; - }; 08CE2A4925A823A9004416A8 /* Protocols */ = { isa = PBXGroup; children = ( @@ -730,8 +716,8 @@ 08D43A062559256C001EFC43 /* Home */ = { isa = PBXGroup; children = ( - 08CD051F2575DC1C00FAB7EA /* HomeTabBarController.swift */, - 0806B9E32581C09200A07063 /* HomeTabBarCoordinator.swift */, + 247FC25E265C9E2B00836EB7 /* MainTabBarController.swift */, + 247FC25D265C9E2B00836EB7 /* MainTabBarCoordinator.swift */, 08CD052E2575DCAD00FAB7EA /* Search */, 08CD052F2575DCB700FAB7EA /* {{cookiecutter.domain_model}}s */, ); @@ -753,9 +739,6 @@ 8CAA51622567A38B000E166C /* Extensions */ = { isa = PBXGroup; children = ( - 080C1DB9256F7D020036730D /* UIControl+Combinable.swift */, - 080C1DC9256FB9670036730D /* UITextField+Combinable.swift */, - 080C1DD4256FBFED0036730D /* UIButton+Combinable.swift */, 8CAA51632567A3BB000E166C /* UIScrollView+KeyboardContentInsettable.swift */, 08428B9225AD4AD500D4AAD9 /* UIDevice.swift */, 08428B9A25AD4BFA00D4AAD9 /* Bundle.swift */, @@ -1067,7 +1050,6 @@ 8CF927C62566C1E400404F59 /* Localizations_en-EN.json in Resources */, 8CF927C72566C1E400404F59 /* Localizations_es-ES.json in Resources */, 088A8A5F255E6FE800AFF5AD /* NStack.plist in Resources */, - 08CD056A2576069900FAB7EA /* {{cookiecutter.domain_model}}ListCell.xib in Resources */, 08D439C72558F2F5001EFC43 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1296,6 +1278,8 @@ buildActionMask = 2147483647; files = ( 081F27502591EBB000288215 /* {{cookiecutter.domain_model}}sUseCase.swift in Sources */, + 247FC22E265C9CFB00836EB7 /* Entity.swift in Sources */, + 247FC22D265C9CFB00836EB7 /* UseCase.swift in Sources */, 081F275F2591ED5500288215 /* UseCaseProvider.swift in Sources */, 081F27322591E5CC00288215 /* {{cookiecutter.domain_model}}.swift in Sources */, ); @@ -1323,8 +1307,8 @@ 08CE2A3A25A8140F004416A8 /* RepositoryProvider.swift in Sources */, 08CE2A6825A8254B004416A8 /* {{cookiecutter.domain_model}}.swift in Sources */, 08CE2A4225A8149E004416A8 /* {{cookiecutter.domain_model}}sRepository.swift in Sources */, + 247FC256265C9D4B00836EB7 /* Endpoints.swift in Sources */, 081F27672591F05D00288215 /* {{cookiecutter.domain_model}}sUseCase.swift in Sources */, - 08428B8B25AD488400D4AAD9 /* {{cookiecutter.domain_model}}sEndpoint.swift in Sources */, 08CE2A1C25A7E349004416A8 /* RemoteRepository.swift in Sources */, 08CE2A2425A7E411004416A8 /* Combinable.swift in Sources */, ); @@ -1346,23 +1330,21 @@ 08CD05372575DCDA00FAB7EA /* {{cookiecutter.domain_model}}ListViewModel.swift in Sources */, 081F2676258C981200288215 /* NavigationCoordinator.swift in Sources */, 08CD064E2578A34E00FAB7EA /* AppCoordinator.swift in Sources */, - 080C1DCA256FB9670036730D /* UITextField+Combinable.swift in Sources */, 081F269C25903FF900288215 /* AnyCoordinator.swift in Sources */, 081F266E258C974B00288215 /* AnyCoordinatable.swift in Sources */, + 247FC25F265C9E2B00836EB7 /* MainTabBarCoordinator.swift in Sources */, 08CD052A2575DC9B00FAB7EA /* {{cookiecutter.domain_model}}ListViewController.swift in Sources */, 083194502583518A0063F84D /* LoginCoordinator.swift in Sources */, 08CD054E2575F33E00FAB7EA /* {{cookiecutter.domain_model}}DetailViewModel.swift in Sources */, - 08CD05202575DC1C00FAB7EA /* HomeTabBarController.swift in Sources */, 08CD05182575D8EF00FAB7EA /* LoginScene.swift in Sources */, 08D3090325877B8B00390276 /* {{cookiecutter.domain_model}}ListCoordinator.swift in Sources */, 08D3090725878C3500390276 /* {{cookiecutter.domain_model}}DetailCoordinator.swift in Sources */, 081F266A258C939700288215 /* ViewModelType.swift in Sources */, 081F2672258C97D100288215 /* Coordinator.swift in Sources */, 088A8B4B256239B300AFF5AD /* App.swift in Sources */, - 08CD05692576069900FAB7EA /* {{cookiecutter.domain_model}}ListCell.swift in Sources */, 081F2698258F175B00288215 /* Coordinatable.swift in Sources */, - 0806B9E42581C09200A07063 /* HomeTabBarCoordinator.swift in Sources */, 8CF927BF2566C1B800404F59 /* Localizations.swift in Sources */, + 247FC260265C9E2B00836EB7 /* MainTabBarController.swift in Sources */, 8CAA51642567A3BB000E166C /* UIScrollView+KeyboardContentInsettable.swift in Sources */, 081F2669258C939700288215 /* CoordinatorType.swift in Sources */, 8CF927C02566C1B800404F59 /* SKLocalizations.swift in Sources */, @@ -1370,7 +1352,6 @@ 081F2667258C939700288215 /* Scene.swift in Sources */, 088A8B4C256239B300AFF5AD /* App+Style.swift in Sources */, 08CD055B2575F37400FAB7EA /* {{cookiecutter.domain_model}}DetailScene.swift in Sources */, - 080C1DBA256F7D020036730D /* UIControl+Combinable.swift in Sources */, 08D43A2325593061001EFC43 /* LoginViewModel.swift in Sources */, 08CD05492575F32400FAB7EA /* {{cookiecutter.domain_model}}DetailViewController.swift in Sources */, 08D439BE2558F2F1001EFC43 /* AppDelegate.swift in Sources */, @@ -1380,7 +1361,6 @@ 08D43A2125593061001EFC43 /* LoginViewController.swift in Sources */, 08D439C02558F2F1001EFC43 /* SceneDelegate.swift in Sources */, 080C1DBF256FB7C00036730D /* Combinable.swift in Sources */, - 080C1DD5256FBFED0036730D /* UIButton+Combinable.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/App/App.swift b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/App/App.swift index 9ef37fd..4a175e9 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/App/App.swift +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/App/App.swift @@ -34,9 +34,9 @@ enum App { static let hostname: String = { switch App.enivornment { case .develop: - return "dev-api.example.com" + return "jsonplaceholder.typicode.com" case .staging: - return "stg-api.example.com" + return "jsonplaceholder.typicode.com" case .production: return "api.example.com" } diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/App/AppCoordinator.swift b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/App/AppCoordinator.swift index 52d57ff..0350928 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/App/AppCoordinator.swift +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/App/AppCoordinator.swift @@ -28,12 +28,13 @@ final class AppCoordinator: CoordinatorType { func start() { App.Style.setup() - let coordinator = HomeTabBarCoordinator() + let coordinator = MainTabBarCoordinator() addChild(coordinator) coordinator.parentCoordinator = self.eraseToAnyCoordinator() coordinator.start() let rootVC = coordinator.viewController - transitionTo(rootVC) + self.window.rootViewController = rootVC + self.window.makeKeyAndVisible() } func addChild(_ coordinator: CoordinatorType) { @@ -45,15 +46,3 @@ final class AppCoordinator: CoordinatorType { childCoordinators.remove(at: index) } } - -private extension AppCoordinator { - - func transitionTo(_ controller: UIViewController) { - UIView.transition(with: self.window, duration: 0.2, options: .transitionCrossDissolve, animations: { - self.window.rootViewController = controller - self.window.makeKeyAndVisible() - }) { _ in - print("window transition completed") - } - } -} diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Base/Extensions/UIButton+Combinable.swift b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Base/Extensions/UIButton+Combinable.swift deleted file mode 100644 index 4f04d51..0000000 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Base/Extensions/UIButton+Combinable.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// UIButton+Combinable.swift -// {{cookiecutter.app_name}} -// -// Created by {{cookiecutter.lead_dev_name}} on {% now 'local' %}. -// Copyright © {% now 'local', '%Y' %} {{cookiecutter.company_name}} All rights reserved. -// - -import UIKit.UIButton -import Combine - -extension Combinable where Self: UIButton { - /// Returns a publisher that emits events when `.touchUpInside` sent on button. - /// - /// - Parameters: - /// - Returns: A publisher that emits events when action taken on button. - func tapPublisher() -> AnyPublisher { - return publisher(for: .touchUpInside) - .map { _ in () } - .eraseToAnyPublisher() - } -} diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Base/Extensions/UIControl+Combinable.swift b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Base/Extensions/UIControl+Combinable.swift deleted file mode 100644 index 3072975..0000000 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Base/Extensions/UIControl+Combinable.swift +++ /dev/null @@ -1,85 +0,0 @@ -// -// UIControl+Combine.swift -// {{cookiecutter.app_name}} -// -// Created by {{cookiecutter.lead_dev_name}} on {% now 'local' %}. -// Copyright © {% now 'local', '%Y' %} {{cookiecutter.company_name}} All rights reserved. -// - -import UIKit.UIControl -import Combine - -extension UIControl: Combinable {} - -extension Combinable where Self: UIControl { - /// Returns a publisher that emits events when action sent on control. - /// - /// - Parameters: - /// - event: The event to publish. - /// - Returns: A publisher that emits events when action taken on control. - func publisher(for event: Event) -> UIControl.EventPublisher { - return UIControl.EventPublisher(control: self, controlEvent: event) - } -} - -extension UIControl { - struct EventPublisher: Publisher { - /// The kind of values published by this publisher. - typealias Output = UIControl - /// The kind of errors this publisher might publish. - /// - /// Using `Never` since this `Publisher` does not publish errors. - typealias Failure = Never - - /// The control this publisher uses as a source. - let control: UIControl - - /// The type of event published by this publisher. - let controlEvent: UIControl.Event - - /// Connecting publisher and subscriber with subscription - func receive(subscriber: S) where S: Subscriber, Self.Failure == S.Failure, Self.Output == S.Input { - let subscription = EventSubscription(control: control, event: controlEvent, subscriber: subscriber) - subscriber.receive(subscription: subscription) - } - } -} - -private extension UIControl.EventPublisher { - - final class EventSubscription: Subscription where S.Input == UIControl, S.Failure == Never { - - private let control: UIControl - private let event: UIControl.Event - private var subscriber: S? - // Controlling back pressure - private var currentDemand: Subscribers.Demand = .none - - init(control: UIControl, event: UIControl.Event, subscriber: S) { - self.control = control - self.event = event - self.subscriber = subscriber - control.addTarget(self, - action: #selector(performAction), - for: event) - } - - func request(_ demand: Subscribers.Demand) { - currentDemand += demand - } - - func cancel() { - subscriber = nil - control.removeTarget(self, - action: #selector(performAction), - for: event) - } - - @objc private func performAction() { - if currentDemand > 0 { - currentDemand += subscriber?.receive(control) ?? .none - currentDemand -= 1 - } - } - } -} diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Base/Extensions/UITextField+Combinable.swift b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Base/Extensions/UITextField+Combinable.swift deleted file mode 100644 index de1eb9f..0000000 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Base/Extensions/UITextField+Combinable.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// UITextField+Combinable.swift -// {{cookiecutter.app_name}} -// -// Created by {{cookiecutter.lead_dev_name}} on {% now 'local' %}. -// Copyright © {% now 'local', '%Y' %} {{cookiecutter.company_name}} All rights reserved. -// - -import UIKit -import Combine - -extension Combinable where Self: UITextField { - /// Returns a publisher that emits events when `.allEditingEvents` sent on UITextField. - /// - /// - Parameters: - /// - Returns: A publisher that emits events when action taken on control. - func textPublisher() -> AnyPublisher { - return publisher(for: .allEditingEvents) - .map { control in (control as? UITextField)?.text } - .eraseToAnyPublisher() - } -} diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/HomeTabBarController.swift b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/MainTabBarController.swift similarity index 88% rename from {{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/HomeTabBarController.swift rename to {{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/MainTabBarController.swift index f8062ea..975aece 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/HomeTabBarController.swift +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/MainTabBarController.swift @@ -1,5 +1,5 @@ // -// HomeTabBarController.swift +// MainTabBarController.swift // {{cookiecutter.app_name}} // // Created by {{cookiecutter.lead_dev_name}} on {% now 'local' %}. @@ -8,7 +8,7 @@ import UIKit -class HomeTabBarController: UITabBarController { +class MainTabBarController: UITabBarController { override func viewDidLoad() { super.viewDidLoad() diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/HomeTabBarCoordinator.swift b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/MainTabBarCoordinator.swift similarity index 74% rename from {{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/HomeTabBarCoordinator.swift rename to {{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/MainTabBarCoordinator.swift index 8aa9ab6..e0dfeea 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/HomeTabBarCoordinator.swift +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/MainTabBarCoordinator.swift @@ -1,5 +1,5 @@ // -// HomeTabBarCoordinator.swift +// MainTabBarCoordinator.swift // {{cookiecutter.app_name}} // // Created by {{cookiecutter.lead_dev_name}} on {% now 'local' %}. @@ -9,14 +9,14 @@ import UIKit import Combine -final class HomeTabBarCoordinator: TabBarCoordinator { +final class MainTabBarCoordinator: TabBarCoordinator { enum Tab { case search - case {{cookiecutter.domain_model}}List + case PostList } override func start() { - [.search, .{{cookiecutter.domain_model}}List].forEach(createTab) + [.search, .PostList].forEach(createTab) let viewControllers = childCoordinators.compactMap { $0.viewController } @@ -30,8 +30,8 @@ final class HomeTabBarCoordinator: TabBarCoordinator { addChild(coordinator) coordinator.start() coordinator.viewController.tabBarItem = tab.tabBarItem - case .{{cookiecutter.domain_model}}List: - let coordinator = {{cookiecutter.domain_model}}ListCoordinator() + case .PostList: + let coordinator = PostListCoordinator() addChild(coordinator) coordinator.start() coordinator.parentCoordinator = self.eraseToAnyCoordinator() @@ -40,14 +40,14 @@ final class HomeTabBarCoordinator: TabBarCoordinator { } } -extension HomeTabBarCoordinator.Tab { +extension MainTabBarCoordinator.Tab { var tabBarItem: UITabBarItem { switch self { case .search: return UITabBarItem(title: "Search", systemName: "magnifyingglass", tag: 0) - case .{{cookiecutter.domain_model}}List: - return UITabBarItem(title: "{{cookiecutter.domain_model|upper}}s", systemName: "rectangle.grid.2x2.fill", tag: 1) + case .PostList: + return UITabBarItem(title: "POSTs", systemName: "rectangle.grid.2x2.fill", tag: 1) } } } diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/Detail/{{cookiecutter.domain_model}}Detail.storyboard b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/Detail/{{cookiecutter.domain_model}}Detail.storyboard index e4f9858..95e45eb 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/Detail/{{cookiecutter.domain_model}}Detail.storyboard +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/Detail/{{cookiecutter.domain_model}}Detail.storyboard @@ -1,9 +1,9 @@ - + - + @@ -16,13 +16,39 @@ + + + + + + + + + + + + + + + + - + diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/Detail/{{cookiecutter.domain_model}}DetailCoordinator.swift b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/Detail/{{cookiecutter.domain_model}}DetailCoordinator.swift index 299dc5c..f8b6870 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/Detail/{{cookiecutter.domain_model}}DetailCoordinator.swift +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/Detail/{{cookiecutter.domain_model}}DetailCoordinator.swift @@ -6,20 +6,19 @@ // Copyright © {% now 'local', '%Y' %} {{cookiecutter.company_name}} All rights reserved. // -import Combine import UIKit.UIViewController -enum {{cookiecutter.domain_model}}DetailRoute { - -} - +// This Coordinator has no `enum Route`. +// It literally means that this is the last screen and no further navigation is possible, +// though you can go back. 😉 class {{cookiecutter.domain_model}}DetailCoordinator: NavigationCoordinator { - - struct SceneDependencies { - let {{cookiecutter.domain_model|lower}}Id: Int - } + // MARK: - Enums and Type aliases + typealias SceneDependencies = {{cookiecutter.domain_model}}DetailScene.Dependencies + + // MARK: - Properties private let sceneDependencies: SceneDependencies - + + // MARK: - Init init(navigationController: UINavigationController, sceneDependencies: SceneDependencies) { self.sceneDependencies = sceneDependencies super.init(viewController: navigationController) @@ -27,25 +26,11 @@ class {{cookiecutter.domain_model}}DetailCoordinator: NavigationCoordinator { override func start() { super.start() - show{{cookiecutter.domain_model}}Detail() - } - - private func show{{cookiecutter.domain_model}}Detail() { let scene = {{cookiecutter.domain_model}}DetailScene( dependencies: .init( - coordinator: self.eraseToAnyCoordinatable(), - viewModel: {{cookiecutter.domain_model}}DetailViewModel( - {{cookiecutter.domain_model|lower}}Id: sceneDependencies.{{cookiecutter.domain_model|lower}}Id - ) + {{cookiecutter.domain_model|lower}}Id: sceneDependencies.{{cookiecutter.domain_model|lower}}Id ) ) self.navigationController.pushViewController(scene.viewController, animated: true) } } - -extension {{cookiecutter.domain_model}}DetailCoordinator: Coordinatable { - - func coordinate(to route: {{cookiecutter.domain_model}}DetailRoute) { - - } -} diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/Detail/{{cookiecutter.domain_model}}DetailScene.swift b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/Detail/{{cookiecutter.domain_model}}DetailScene.swift index 2586f3e..820c9fd 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/Detail/{{cookiecutter.domain_model}}DetailScene.swift +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/Detail/{{cookiecutter.domain_model}}DetailScene.swift @@ -10,19 +10,23 @@ import UIKit.UIViewController import UIKit.UIStoryboard final class {{cookiecutter.domain_model}}DetailScene: Scene { + // MARK: - Properties private let vc: {{cookiecutter.domain_model}}DetailViewController! + + // MARK: - Init init(dependencies: Dependencies) { - let storyboard = UIStoryboard(name: "{{cookiecutter.domain_model}}Detail", bundle: nil) - vc = storyboard.instantiateInitialViewController() as? {{cookiecutter.domain_model}}DetailViewController - vc.viewModel = dependencies.viewModel - vc.viewModel?.coordinator = dependencies.coordinator + vc = {{cookiecutter.domain_model}}DetailViewController.instantiate( + with: {{cookiecutter.domain_model}}DetailViewModel( + {{cookiecutter.domain_model|lower}}Id: dependencies.{{cookiecutter.domain_model|lower}}Id + ) + ) } } +// MARK: - Scene Protocol extension {{cookiecutter.domain_model}}DetailScene { struct Dependencies { - let coordinator: AnyCoordinatable<{{cookiecutter.domain_model}}DetailRoute> - let viewModel: {{cookiecutter.domain_model}}DetailViewModel + let {{cookiecutter.domain_model|lower}}Id: Int } var viewController: UIViewController { diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/Detail/{{cookiecutter.domain_model}}DetailViewController.swift b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/Detail/{{cookiecutter.domain_model}}DetailViewController.swift index 0217154..7b35574 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/Detail/{{cookiecutter.domain_model}}DetailViewController.swift +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/Detail/{{cookiecutter.domain_model}}DetailViewController.swift @@ -7,15 +7,49 @@ // import UIKit +import Combine class {{cookiecutter.domain_model}}DetailViewController: UIViewController { + // MARK: - Outlets + @IBOutlet private weak var titleLabel: UILabel! + @IBOutlet private weak var descriptionLabel: UILabel! - var viewModel: {{cookiecutter.domain_model}}DetailViewModel? + + // MARK: - Properties + private var viewModel: {{cookiecutter.domain_model}}DetailViewModel! + private var cancellables = Set() + + // MARK: - Init + class func instantiate(with viewModel: {{cookiecutter.domain_model}}DetailViewModel) -> {{cookiecutter.domain_model}}DetailViewController { + let name = "{{cookiecutter.domain_model}}Detail" + let storyboard = UIStoryboard(name: name, bundle: nil) + + guard let vc = storyboard.instantiateInitialViewController() as? {{cookiecutter.domain_model}}DetailViewController else { + preconditionFailure("Unable to instantiate a {{cookiecutter.domain_model}}DetailViewController with the name \(name)") + } + + vc.viewModel = viewModel + return vc + } override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. + viewModel?.transform( + .init( + loadData: Just(()).eraseToAnyPublisher() + ) + ).result.receive(on: RunLoop.main) + .sink(receiveValue: { result in + switch result { + case .success(let item): + self.titleLabel.text = item.title + self.descriptionLabel.text = item.body + case .failure(_): + print("No {{cookiecutter.domain_model|lower}}s found") + } + }).store(in: &cancellables) } /* diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/Detail/{{cookiecutter.domain_model}}DetailViewModel.swift b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/Detail/{{cookiecutter.domain_model}}DetailViewModel.swift index b9a3802..a7805c8 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/Detail/{{cookiecutter.domain_model}}DetailViewModel.swift +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/Detail/{{cookiecutter.domain_model}}DetailViewModel.swift @@ -6,14 +6,47 @@ // Copyright © {% now 'local', '%Y' %} {{cookiecutter.company_name}} All rights reserved. // -import Foundation - -class {{cookiecutter.domain_model}}DetailViewModel { - - var coordinator: AnyCoordinatable<{{cookiecutter.domain_model}}DetailRoute>? +import Combine +import Domain +import Platform +class {{cookiecutter.domain_model}}DetailViewModel: ViewModelType { + // MARK: - Input + struct Input { + // Actions + let loadData: AnyPublisher + } + // MARK: - Output + struct Output { + let result: AnyPublisher, Never> + } + + // MARK: - Properties + private let {{cookiecutter.domain_model|lower}}Id: Int + private let useCase: Domain.{{cookiecutter.domain_model}}sUseCase + + // MARK: - Init init({{cookiecutter.domain_model|lower}}Id: Int) { - print(#function) - print({{cookiecutter.domain_model|lower}}Id) + self.{{cookiecutter.domain_model|lower}}Id = {{cookiecutter.domain_model|lower}}Id + self.useCase = Platform.UseCaseProvider( + baseURL: App.Server.baseURL + ).make{{cookiecutter.domain_model}}sUseCase() + } + + // MARK: - I/O Transformer + func transform(_ input: Input) -> Output { + return Output( + result: input.loadData + .flatMap { _ in + self.useCase.fetch(with: self.{{cookiecutter.domain_model|lower}}Id) + .map { + Result<{{cookiecutter.domain_model}},Error>.success($0) + } + .catch { error -> Just> in + return Just(.failure(error)) + } + } + .eraseToAnyPublisher() + ) } } diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/List/Item/{{cookiecutter.domain_model}}ListCell.swift b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/List/Item/{{cookiecutter.domain_model}}ListCell.swift deleted file mode 100644 index 62f7ce5..0000000 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/List/Item/{{cookiecutter.domain_model}}ListCell.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// {{cookiecutter.domain_model}}ListCell.swift -// {{cookiecutter.app_name}} -// -// Created by {{cookiecutter.lead_dev_name}} on {% now 'local' %}. -// Copyright © {% now 'local', '%Y' %} {{cookiecutter.company_name}} All rights reserved. -// - -import UIKit -import Domain -import Kingfisher - -class {{cookiecutter.domain_model}}ListCell: UICollectionViewCell { - - static let identifier = "{{cookiecutter.domain_model}}ListCellIdentifier" - - @IBOutlet private weak var posterImageView: UIImageView! - - // MARK: Life cycle - override func awakeFromNib() { - super.awakeFromNib() - } - - override func prepareForReuse() { - super.prepareForReuse() - clearAll() - } - - func configure(forItem item: {{cookiecutter.domain_model}}) { - if let path = item.posterPath, let posterUrl = URL(string: "https://image.tmdb.org/t/p/w500" + path) { - posterImageView.kf.indicatorType = .activity - posterImageView.kf.setImage( - with: posterUrl, - placeholder: UIImage(named: "No_image_poster"), - options: [ - .loadDiskFileSynchronously, - .cacheOriginalImage - ], - completionHandler: { result in - switch result { - case .success(let value): - print("Task done for: \(value.source.url?.absoluteString ?? "")") - case .failure(let error): - print("Job failed: \(error.localizedDescription)") - self.posterImageView.image = #imageLiteral(resourceName: "No_image_poster") - } - } - ) - } - } -} - -fileprivate extension {{cookiecutter.domain_model}}ListCell { - func clearAll() { - // Cancel downloading of image and then reset - posterImageView.kf.cancelDownloadTask() - posterImageView.image = #imageLiteral(resourceName: "No_image_poster") - } -} diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/List/Item/{{cookiecutter.domain_model}}ListCell.xib b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/List/Item/{{cookiecutter.domain_model}}ListCell.xib deleted file mode 100644 index fb99338..0000000 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/List/Item/{{cookiecutter.domain_model}}ListCell.xib +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/List/{{cookiecutter.domain_model}}List.storyboard b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/List/{{cookiecutter.domain_model}}List.storyboard index 8237ca4..ca69649 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/List/{{cookiecutter.domain_model}}List.storyboard +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/List/{{cookiecutter.domain_model}}List.storyboard @@ -17,29 +17,22 @@ - - + + - - - - - - - - + - - - - + + + + - + diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/List/{{cookiecutter.domain_model}}ListCoordinator.swift b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/List/{{cookiecutter.domain_model}}ListCoordinator.swift index 5e01a4a..ef781d8 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/List/{{cookiecutter.domain_model}}ListCoordinator.swift +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/List/{{cookiecutter.domain_model}}ListCoordinator.swift @@ -6,7 +6,6 @@ // Copyright © {% now 'local', '%Y' %} {{cookiecutter.company_name}} All rights reserved. // -import Combine import UIKit.UIViewController enum {{cookiecutter.domain_model}}ListRoute { @@ -17,20 +16,26 @@ class {{cookiecutter.domain_model}}ListCoordinator: NavigationCoordinator { override func start() { super.start() - show{{cookiecutter.domain_model}}List() - } - - private func show{{cookiecutter.domain_model}}List() { let scene = {{cookiecutter.domain_model}}ListScene( dependencies: .init( - coordinator: self.eraseToAnyCoordinatable(), - viewModel: {{cookiecutter.domain_model}}ListViewModel() + coordinator: self.eraseToAnyCoordinatable() ) ) self.navigationController.setViewControllers([scene.viewController], animated: false) } +} - private func show{{cookiecutter.domain_model}}Detail(id: Int) { +// MARK: - ViewModel → Coordinator Callbacks +extension {{cookiecutter.domain_model}}ListCoordinator: Coordinatable { + + func coordinate(to route: {{cookiecutter.domain_model}}ListRoute) { + switch route { + case .detail(let id): + show{{cookiecutter.domain_model}}ListDetail(id: id) + } + } + + private func show{{cookiecutter.domain_model}}ListDetail(id: Int) { let coordinator = {{cookiecutter.domain_model}}DetailCoordinator( navigationController: navigationController, sceneDependencies: .init( @@ -41,13 +46,3 @@ class {{cookiecutter.domain_model}}ListCoordinator: NavigationCoordinator { coordinator.start() } } - -extension {{cookiecutter.domain_model}}ListCoordinator: Coordinatable { - - func coordinate(to route: {{cookiecutter.domain_model}}ListRoute) { - switch route { - case .detail(let id): - show{{cookiecutter.domain_model}}Detail(id: id) - } - } -} diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/List/{{cookiecutter.domain_model}}ListScene.swift b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/List/{{cookiecutter.domain_model}}ListScene.swift index de32640..799bfc7 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/List/{{cookiecutter.domain_model}}ListScene.swift +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/List/{{cookiecutter.domain_model}}ListScene.swift @@ -5,26 +5,32 @@ // Created by {{cookiecutter.lead_dev_name}} on {% now 'local' %}. // Copyright © {% now 'local', '%Y' %} {{cookiecutter.company_name}} All rights reserved. // + import UIKit.UIViewController import UIKit.UIStoryboard final class {{cookiecutter.domain_model}}ListScene { + // MARK: - Properties private let vc: {{cookiecutter.domain_model}}ListViewController! + + // MARK: - Init init(dependencies: Dependencies) { - let storyboard = UIStoryboard(name: "{{cookiecutter.domain_model}}List", bundle: nil) - vc = storyboard.instantiateInitialViewController() as? {{cookiecutter.domain_model}}ListViewController - vc.viewModel = dependencies.viewModel - vc.viewModel?.coordinator = dependencies.coordinator + vc = {{cookiecutter.domain_model}}ListViewController.instantiate( + with: {{cookiecutter.domain_model}}ListViewModel( + coordinator: dependencies.coordinator + ) + ) } } +// MARK: - Scene Protocol extension {{cookiecutter.domain_model}}ListScene: Scene { struct Dependencies { - let coordinator: AnyCoordinatable<{{cookiecutter.domain_model}}ListCoordinator.Route> - let viewModel: {{cookiecutter.domain_model}}ListViewModel + let coordinator: AnyCoordinatable<{{cookiecutter.domain_model}}ListRoute> } var viewController: UIViewController { return vc } } + diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/List/{{cookiecutter.domain_model}}ListViewController.swift b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/List/{{cookiecutter.domain_model}}ListViewController.swift index 8b14e8b..dbff37e 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/List/{{cookiecutter.domain_model}}ListViewController.swift +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/List/{{cookiecutter.domain_model}}ListViewController.swift @@ -8,72 +8,91 @@ import UIKit import Combine +import CombineCocoa import Domain class {{cookiecutter.domain_model}}ListViewController: UIViewController { - + // MARK: - Enums and Type aliases enum Section { case main } - typealias DataSource = UICollectionViewDiffableDataSource - typealias Delegate = UICollectionViewDelegateFlowLayout + typealias DataSource = UITableViewDiffableDataSource typealias Snapshot = NSDiffableDataSourceSnapshot + + // MARK: - Outlets + @IBOutlet private weak var tableView: UITableView! - var viewModel: {{cookiecutter.domain_model}}ListViewModel? - - @IBOutlet private weak var collectionView: UICollectionView! { - didSet { - let nib = UINib(nibName: "{{cookiecutter.domain_model}}ListCell", bundle: nil) - collectionView.register(nib, forCellWithReuseIdentifier: {{cookiecutter.domain_model}}ListCell.identifier) - collectionView.keyboardDismissMode = .onDrag - } - } + // MARK: - Properties private weak var refreshControl: UIRefreshControl! private weak var noResultsLabel: UILabel! - private var cancellables = Set() private var dataSource: DataSource! + private var snapshot: Snapshot = .init() + + private var viewModel: {{cookiecutter.domain_model}}ListViewModel! + private var cancellables = Set() + private var itemSelectedPublisher: PassthroughSubject = .init() + + // MARK: - Init + class func instantiate(with viewModel: {{cookiecutter.domain_model}}ListViewModel) -> {{cookiecutter.domain_model}}ListViewController { + let name = "{{cookiecutter.domain_model}}List" + let storyboard = UIStoryboard(name: name, bundle: nil) + + guard let vc = storyboard.instantiateInitialViewController() as? {{cookiecutter.domain_model}}ListViewController else { + preconditionFailure("Unable to instantiate a {{cookiecutter.domain_model}}ListViewController with the name \(name)") + } + + vc.viewModel = viewModel + return vc + } + // MARK: - View Lifecycle - override func viewDidLoad() { super.viewDidLoad() - // Do any additional setup after loading the view. + + tableView.delegate = self setupRefreshControl() setupNoResults() setupDataSource() - + let viewModelInput = {{cookiecutter.domain_model}}ListViewModel.Input( - refresh: refreshControl.publisher(for: .valueChanged) + refresh: Publishers.ControlEvent(control: refreshControl, events: .valueChanged) .map { _ in () } .prepend(()) .eraseToAnyPublisher(), - selectedModel: Just(1).eraseToAnyPublisher() + itemSelected: itemSelectedPublisher + .eraseToAnyPublisher(), + loadMore: tableView.reachedBottomPublisher() + .debounce(for: 0.1, scheduler: RunLoop.main) + .eraseToAnyPublisher() ) + let viewModelOutput = viewModel?.transform(viewModelInput) - + viewModelOutput?.results.receive(on: DispatchQueue.main) .catch { [weak self] error -> Just<[{{cookiecutter.domain_model}}]> in self?.noResultsLabel.text = error.localizedDescription return Just([]) }.sink(receiveValue: { values in - self.updateCollection(with: values) + self.updateTable(with: values) }).store(in: &cancellables) } +} - func updateCollection(with items: [{{cookiecutter.domain_model}}]) { - var snapshot = Snapshot() - snapshot.appendSections([.main]) - snapshot.appendItems(items) - dataSource.apply(snapshot) +extension {{cookiecutter.domain_model}}ListViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard let item = dataSource.itemIdentifier(for: indexPath) else { return } + itemSelectedPublisher.send(item.id) } } -extension {{cookiecutter.domain_model}}ListViewController { - +private extension {{cookiecutter.domain_model}}ListViewController { + func setupRefreshControl() { let refreshControl = UIRefreshControl() refreshControl.backgroundColor = .clear refreshControl.tintColor = .lightGray - collectionView.refreshControl = refreshControl + tableView.refreshControl = refreshControl self.refreshControl = refreshControl } @@ -82,21 +101,24 @@ extension {{cookiecutter.domain_model}}ListViewController { label.text = "No {{cookiecutter.domain_model}}s Found!\n Please try different name again..." label.sizeToFit() label.isHidden = true - collectionView.backgroundView = label + tableView.backgroundView = label noResultsLabel = label } func setupDataSource() { + snapshot.appendSections([.main]) dataSource = DataSource( - collectionView: collectionView, - cellProvider: { (collectionView, indexPath, item) -> UICollectionViewCell? in - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: {{cookiecutter.domain_model}}ListCell.identifier, for: indexPath) as? {{cookiecutter.domain_model}}ListCell - cell?.configure(forItem: item) + tableView: tableView, + cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in + let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "cell") // For simplicity + cell.textLabel?.text = item.title + cell.detailTextLabel?.text = item.body return cell }) } -} - -extension {{cookiecutter.domain_model}}ListViewController { - + + func updateTable(with items: [{{cookiecutter.domain_model}}]) { + snapshot.appendItems(items) + dataSource.apply(snapshot) + } } diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/List/{{cookiecutter.domain_model}}ListViewModel.swift b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/List/{{cookiecutter.domain_model}}ListViewModel.swift index 2a43830..3ca1e33 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/List/{{cookiecutter.domain_model}}ListViewModel.swift +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Home/{{cookiecutter.domain_model}}s/List/{{cookiecutter.domain_model}}ListViewModel.swift @@ -11,41 +11,47 @@ import Domain import Platform class {{cookiecutter.domain_model}}ListViewModel: ViewModelType { - + // MARK: - Input struct Input { - // Actions let refresh: AnyPublisher -// let loadMore: AnyPublisher - let selectedModel: AnyPublisher + let itemSelected: AnyPublisher + let loadMore: AnyPublisher } - + // MARK: - Output struct Output { - let results: AnyPublisher<[{{cookiecutter.domain_model}}], Error> + let results: AnyPublisher<[{{cookiecutter.domain_model}}], Never> } - var coordinator: AnyCoordinatable<{{cookiecutter.domain_model}}ListRoute>? + // MARK: - Properties + private let coordinator: AnyCoordinatable<{{cookiecutter.domain_model}}ListCoordinator.Route>? private let useCase: Domain.{{cookiecutter.domain_model}}sUseCase - - init() { + private var cancellables = Set() + + // MARK: - Init + init(coordinator: AnyCoordinatable<{{cookiecutter.domain_model}}ListCoordinator.Route>) { + self.coordinator = coordinator self.useCase = Platform.UseCaseProvider( baseURL: App.Server.baseURL ).make{{cookiecutter.domain_model}}sUseCase() } + // MARK: - I/O Transformer func transform(_ input: Input) -> Output { - Output( - results: input.refresh.setFailureType(to: Error.self) - .flatMap(useCase.fetchAll) + // Item selection handeled locally + input.itemSelected.sink(receiveValue: { [weak self] id in + self?.coordinator?.coordinate(to: .detail(id: id)) + }).store(in: &cancellables) + + return Output( + results: input.refresh + .flatMap { _ in + self.useCase.fetchAll() + .catch { _ -> Just<[{{cookiecutter.domain_model}}]> in + // TODO: Handle Error + return Just([]) + } + } .eraseToAnyPublisher() ) -// return Output( -// results: input.selectedModel -// .handleEvents(receiveOutput: { [weak self] id in -// guard let this = self else { return } -// this.coordinator?.coordinate(to: .detail(id: id)) -// }) -// .flatMap { _ in Just([0]) } -// .eraseToAnyPublisher() -// ) } } diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Login/LoginCoordinator.swift b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Login/LoginCoordinator.swift index 68fe320..ac9e26b 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Login/LoginCoordinator.swift +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Login/LoginCoordinator.swift @@ -7,10 +7,8 @@ // import UIKit -import Combine enum LoginRoute { - case login case signUp case forgotPassword } @@ -19,14 +17,9 @@ class LoginCoordinator: NavigationCoordinator { override func start() { super.start() - showLogin() - } - - private func showLogin() { let scene = LoginScene( dependencies: .init( - coordinator: self.eraseToAnyCoordinatable(), - viewModel: LoginViewModel() + coordinator: self.eraseToAnyCoordinatable() ) ) self.navigationController.setViewControllers([scene.viewController], animated: false) @@ -43,8 +36,6 @@ extension LoginCoordinator: Coordinatable { func coordinate(to route: LoginRoute) { switch route { - case .login: - showLogin() case .signUp: showSignUp() case .forgotPassword: diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Login/LoginScene.swift b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Login/LoginScene.swift index 0e597c0..e418d0a 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Login/LoginScene.swift +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Login/LoginScene.swift @@ -9,20 +9,24 @@ import UIKit.UIViewController import UIKit.UIStoryboard -final class LoginScene: Scene { +final class LoginScene { + // MARK: - Properties private let vc: LoginViewController! + + // MARK: - Init init(dependencies: Dependencies) { - let storyboard = UIStoryboard(name: "Login", bundle: nil) - vc = storyboard.instantiateInitialViewController() as? LoginViewController - vc.viewModel = dependencies.viewModel - vc.viewModel.coordinator = dependencies.coordinator + vc = LoginViewController.instantiate( + with: LoginViewModel( + coordinator: dependencies.coordinator + ) + ) } } -extension LoginScene { +// MARK: - Scene Protocol +extension LoginScene: Scene { struct Dependencies { let coordinator: AnyCoordinatable - let viewModel: LoginViewModel } var viewController: UIViewController { diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Login/LoginViewController.swift b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Login/LoginViewController.swift index 2f55a6d..78d28f1 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Login/LoginViewController.swift +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Login/LoginViewController.swift @@ -8,11 +8,11 @@ import UIKit import Combine +import CombineCocoa class LoginViewController: UIViewController { - var viewModel = LoginViewModel() - + // MARK: - Outlets @IBOutlet private weak var loginHeaderLabel: UILabel! { didSet { loginHeaderLabel.font = UIFont.preferredFont(forTextStyle: .headline) @@ -32,15 +32,32 @@ class LoginViewController: UIViewController { } } @IBOutlet private weak var scrollView: UIScrollView! + + // MARK: - Properties + private var viewModel: LoginViewModel! private var cancellables = Set() + + // MARK: - Init + class func instantiate(with viewModel: LoginViewModel) -> LoginViewController { + let name = "Login" + let storyboard = UIStoryboard(name: name, bundle: nil) + + guard let vc = storyboard.instantiateInitialViewController() as? LoginViewController else { + preconditionFailure("Unable to instantiate a LoginViewController with the name \(name)") + } + vc.viewModel = viewModel + return vc + } + + // MARK: - View Lifecycle - override func viewDidLoad() { super.viewDidLoad() let viewModelInput = LoginViewModel.Input( - username: userNameTextField.textPublisher(), - password: passwordTextField.textPublisher(), - doLogin: loginButton.tapPublisher() + username: userNameTextField.textPublisher, + password: passwordTextField.textPublisher, + doLogin: loginButton.tapPublisher ) let viewModelOutput = viewModel.transform(viewModelInput) diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Login/LoginViewModel.swift b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Login/LoginViewModel.swift index ebd6011..f907754 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Login/LoginViewModel.swift +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Source/Scenes/Login/LoginViewModel.swift @@ -10,7 +10,7 @@ import Combine class LoginViewModel: ViewModelType { - // Input + // MARK: - Input struct Input { // Data let username: AnyPublisher @@ -18,13 +18,19 @@ class LoginViewModel: ViewModelType { // Actions let doLogin: AnyPublisher } - // Output + // MARK: - Output struct Output { let enableLogin: AnyPublisher } - var coordinator: AnyCoordinatable? - + private let coordinator: AnyCoordinatable? + + // MARK: - Init + init(coordinator: AnyCoordinatable) { + self.coordinator = coordinator + } + + // MARK: - I/O Transformer func transform(_ input: Input) -> Output { return Output( enableLogin: Publishers.CombineLatest(input.username, input.password)