diff --git a/CleanApp/CleanApp.xcodeproj/project.pbxproj b/CleanApp/CleanApp.xcodeproj/project.pbxproj index 1493bcf..a9de195 100644 --- a/CleanApp/CleanApp.xcodeproj/project.pbxproj +++ b/CleanApp/CleanApp.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 4902E3C128F1DB24003AB55D /* DataLayer in Frameworks */ = {isa = PBXBuildFile; productRef = 4902E3C028F1DB24003AB55D /* DataLayer */; }; 4902E3C328F1DB24003AB55D /* DomainLayer in Frameworks */ = {isa = PBXBuildFile; productRef = 4902E3C228F1DB24003AB55D /* DomainLayer */; }; 4902E3C528F1DB24003AB55D /* PresentationLayer in Frameworks */ = {isa = PBXBuildFile; productRef = 4902E3C428F1DB24003AB55D /* PresentationLayer */; }; + 4937EC5328F3358500810F76 /* DIContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4937EC5228F3358500810F76 /* DIContainer.swift */; }; 493CDDD62723258300499858 /* CleanApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 493CDDD52723258300499858 /* CleanApp.swift */; }; 493CDDE72723258400499858 /* iOSTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 493CDDE62723258400499858 /* iOSTests.swift */; }; 493CDDF12723258400499858 /* iOSUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 493CDDF02723258400499858 /* iOSUITests.swift */; }; @@ -37,6 +38,7 @@ 4902E3BE28F1DB07003AB55D /* DataLayer */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = DataLayer; path = Modules/DataLayer; sourceTree = ""; }; 4902E3BF28F1DB0D003AB55D /* PresentationLayer */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = PresentationLayer; path = Modules/PresentationLayer; sourceTree = ""; }; 49204DE3272346EF00891C5E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 4937EC5228F3358500810F76 /* DIContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DIContainer.swift; sourceTree = ""; }; 493CDDD22723258300499858 /* CleanApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CleanApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 493CDDD52723258300499858 /* CleanApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CleanApp.swift; sourceTree = ""; }; 493CDDE22723258400499858 /* iOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = iOSTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -117,8 +119,9 @@ 493CDDD42723258300499858 /* iOS */ = { isa = PBXGroup; children = ( - 493CDDD52723258300499858 /* CleanApp.swift */, 49204DE3272346EF00891C5E /* Assets.xcassets */, + 493CDDD52723258300499858 /* CleanApp.swift */, + 4937EC5228F3358500810F76 /* DIContainer.swift */, ); path = iOS; sourceTree = ""; @@ -208,7 +211,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1300; + LastSwiftUpdateCheck = 1400; LastUpgradeCheck = 1300; TargetAttributes = { 493CDDD12723258300499858 = { @@ -273,6 +276,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4937EC5328F3358500810F76 /* DIContainer.swift in Sources */, 493CDDD62723258300499858 /* CleanApp.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -438,8 +442,8 @@ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -448,9 +452,12 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = io.doenicke.CleanApp; PRODUCT_NAME = CleanApp; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; }; name = Debug; }; @@ -466,8 +473,8 @@ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -476,9 +483,12 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = io.doenicke.CleanApp; PRODUCT_NAME = CleanApp; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; }; name = Release; }; diff --git a/CleanApp/Modules/DataLayer/Sources/Data.swift b/CleanApp/Modules/DataLayer/Sources/Data.swift deleted file mode 100644 index 24945a7..0000000 --- a/CleanApp/Modules/DataLayer/Sources/Data.swift +++ /dev/null @@ -1,8 +0,0 @@ -import DomainLayer - -public struct DataLayer { - public private(set) var text = Domain().text - - public init() { - } -} diff --git a/CleanApp/Modules/DataLayer/Sources/Repositories/LocalTodoRepository.swift b/CleanApp/Modules/DataLayer/Sources/Repositories/LocalTodoRepository.swift index c467cb1..f2e1cae 100644 --- a/CleanApp/Modules/DataLayer/Sources/Repositories/LocalTodoRepository.swift +++ b/CleanApp/Modules/DataLayer/Sources/Repositories/LocalTodoRepository.swift @@ -19,14 +19,11 @@ public class LocalTodoRepository: TodoRepository { self.todos = [] } - public func add(todo: Todo) { - todos.append(todo) - } - public func todos() async -> Result<[Todo], Swift.Error> { .success(todos) } + @discardableResult public func complete(id: Int) async -> Result { if var todo = todos.first(where: { $0.id == id }) { todo.completed = true @@ -35,4 +32,12 @@ public class LocalTodoRepository: TodoRepository { return .failure(Error.notFound) } } + + @discardableResult + public func add(todo: Todo) async -> Result { + var newTodo = todo + newTodo.id = todos.count + 1 + todos.append(newTodo) + return .success(newTodo) + } } diff --git a/CleanApp/Modules/DataLayer/Tests/Repositories/LocalTodoRepositoryTests.swift b/CleanApp/Modules/DataLayer/Tests/Repositories/LocalTodoRepositoryTests.swift index 673030e..7136429 100644 --- a/CleanApp/Modules/DataLayer/Tests/Repositories/LocalTodoRepositoryTests.swift +++ b/CleanApp/Modules/DataLayer/Tests/Repositories/LocalTodoRepositoryTests.swift @@ -14,7 +14,7 @@ final class LocalTodoRepositoryTests: XCTestCase { // Arrange let expected = Todo(id: 1, title: "Mock") let sut = LocalTodoRepository() - sut.add(todo: expected) + await sut.add(todo: expected) // Act let result = try await sut.todos().get() @@ -27,7 +27,7 @@ final class LocalTodoRepositoryTests: XCTestCase { // Arrange let expected = Todo(id: 1, title: "Mock", completed: true) let sut = LocalTodoRepository() - sut.add(todo: expected) + await sut.add(todo: expected) // Act let result = try await sut.complete(id: 1).get() @@ -49,4 +49,17 @@ final class LocalTodoRepositoryTests: XCTestCase { XCTFail() } } + + func testAdd() async throws { + // Arrange + let expected = Todo(id: 1, title: "Mock") + let sut = LocalTodoRepository() + await sut.add(todo: Todo(title: "Mock")) + + // Act + let result = try await sut.todos().get() + + // Assert + XCTAssertEqual(result, [expected]) + } } diff --git a/CleanApp/Modules/DomainLayer/Sources/Domain.swift b/CleanApp/Modules/DomainLayer/Sources/Domain.swift deleted file mode 100644 index 31fedcd..0000000 --- a/CleanApp/Modules/DomainLayer/Sources/Domain.swift +++ /dev/null @@ -1,6 +0,0 @@ -public struct Domain { - public private(set) var text = "Hello, World!" - - public init() { - } -} diff --git a/CleanApp/Modules/DomainLayer/Sources/Protocols/Repositories/TodoRepository.swift b/CleanApp/Modules/DomainLayer/Sources/Protocols/Repositories/TodoRepository.swift index 3175a30..fad0b2d 100644 --- a/CleanApp/Modules/DomainLayer/Sources/Protocols/Repositories/TodoRepository.swift +++ b/CleanApp/Modules/DomainLayer/Sources/Protocols/Repositories/TodoRepository.swift @@ -10,4 +10,5 @@ import Foundation public protocol TodoRepository { func todos() async -> Result<[Todo], Error> func complete(id: Int) async -> Result + func add(todo: Todo) async -> Result } diff --git a/CleanApp/Modules/DomainLayer/Sources/UseCases/AddTodoUseCase.swift b/CleanApp/Modules/DomainLayer/Sources/UseCases/AddTodoUseCase.swift new file mode 100644 index 0000000..e3d6838 --- /dev/null +++ b/CleanApp/Modules/DomainLayer/Sources/UseCases/AddTodoUseCase.swift @@ -0,0 +1,24 @@ +// +// AddTodoUseCase.swift +// DomainLayer +// +// Created by Miguel Dönicke on 09.10.22. +// + +import Foundation + +public protocol AddTodoUseCase { + func execute(todo: Todo) async -> Result +} + +public class DefaultAddTodoUseCase: AddTodoUseCase { + private let repository: TodoRepository + + public init(repository: TodoRepository) { + self.repository = repository + } + + public func execute(todo: Todo) async -> Result { + await repository.add(todo: todo) + } +} diff --git a/CleanApp/Modules/DomainLayer/Sources/UseCases/CompleteTodoUseCase.swift b/CleanApp/Modules/DomainLayer/Sources/UseCases/CompleteTodoUseCase.swift index 341a4f2..6345b43 100644 --- a/CleanApp/Modules/DomainLayer/Sources/UseCases/CompleteTodoUseCase.swift +++ b/CleanApp/Modules/DomainLayer/Sources/UseCases/CompleteTodoUseCase.swift @@ -7,7 +7,11 @@ import Foundation -public class CompleteTodoUseCase { +public protocol CompleteTodoUseCase { + func execute(id: Int) async -> Result +} + +public class DefaultCompleteTodoUseCase: CompleteTodoUseCase { private let repository: TodoRepository public init(repository: TodoRepository) { diff --git a/CleanApp/Modules/DomainLayer/Sources/UseCases/GetAllTodosUseCase.swift b/CleanApp/Modules/DomainLayer/Sources/UseCases/GetAllTodosUseCase.swift index 87164ae..105b10d 100644 --- a/CleanApp/Modules/DomainLayer/Sources/UseCases/GetAllTodosUseCase.swift +++ b/CleanApp/Modules/DomainLayer/Sources/UseCases/GetAllTodosUseCase.swift @@ -7,7 +7,11 @@ import Foundation -public class GetAllTodosUseCase { +public protocol GetAllTodosUseCase { + func execute() async -> Result<[Todo], Error> +} + +public class DefaultGetAllTodosUseCase: GetAllTodosUseCase { private let repository: TodoRepository public init(repository: TodoRepository) { diff --git a/CleanApp/Modules/DomainLayer/Tests/Mocks/Repositories/MockTodayRepository.swift b/CleanApp/Modules/DomainLayer/Tests/Mocks/Repositories/MockTodayRepository.swift index 7e35697..3bc36ae 100644 --- a/CleanApp/Modules/DomainLayer/Tests/Mocks/Repositories/MockTodayRepository.swift +++ b/CleanApp/Modules/DomainLayer/Tests/Mocks/Repositories/MockTodayRepository.swift @@ -11,6 +11,7 @@ import DomainLayer struct MockTodoRepository: TodoRepository { var todosResult: Result<[Todo], Error>! var completeResult: Result! + var addResult: Result! func todos() async -> Result<[Todo], Error> { todosResult @@ -19,4 +20,8 @@ struct MockTodoRepository: TodoRepository { func complete(id: Int) async -> Result { completeResult } + + func add(todo: Todo) async -> Result { + addResult + } } diff --git a/CleanApp/Modules/DomainLayer/Tests/UseCases/DefaultAddTodoUseCaseTests.swift b/CleanApp/Modules/DomainLayer/Tests/UseCases/DefaultAddTodoUseCaseTests.swift new file mode 100644 index 0000000..d840225 --- /dev/null +++ b/CleanApp/Modules/DomainLayer/Tests/UseCases/DefaultAddTodoUseCaseTests.swift @@ -0,0 +1,24 @@ +// +// DefaultAddTodoUseCaseTests.swift +// DomainLayerTests +// +// Created by Miguel Dönicke on 09.10.22. +// + +import DomainLayer +import XCTest + +final class DefaultAddTodoUseCaseTests: XCTestCase { + func testExecute() async throws { + // Arrange + let expected = Todo.mock() + let repository = MockTodoRepository(addResult: .success(expected)) + let sut = DefaultAddTodoUseCase(repository: repository) + + // Act + let result = try await sut.execute(todo: expected).get() + + // Assert + XCTAssertEqual(result, expected) + } +} diff --git a/CleanApp/Modules/DomainLayer/Tests/UseCases/GetTodayUseCaseTests.swift b/CleanApp/Modules/DomainLayer/Tests/UseCases/DefaultCompleteTodoUseCaseTests.swift similarity index 71% rename from CleanApp/Modules/DomainLayer/Tests/UseCases/GetTodayUseCaseTests.swift rename to CleanApp/Modules/DomainLayer/Tests/UseCases/DefaultCompleteTodoUseCaseTests.swift index 49d227c..5aaf838 100644 --- a/CleanApp/Modules/DomainLayer/Tests/UseCases/GetTodayUseCaseTests.swift +++ b/CleanApp/Modules/DomainLayer/Tests/UseCases/DefaultCompleteTodoUseCaseTests.swift @@ -1,5 +1,5 @@ // -// CompleteTodoUseCaseTests.swift +// DefaultCompleteTodoUseCaseTests.swift // DomainLayerTests // // Created by Miguel Dönicke on 08.10.22. @@ -8,12 +8,12 @@ import DomainLayer import XCTest -final class CompleteTodoUseCaseTests: XCTestCase { +final class DefaultCompleteTodoUseCaseTests: XCTestCase { func testExecute() async throws { // Arrange let expected = Todo.mock() let repository = MockTodoRepository(completeResult: .success(expected)) - let sut = CompleteTodoUseCase(repository: repository) + let sut = DefaultCompleteTodoUseCase(repository: repository) // Act let result = try await sut.execute(id: expected.id!).get() diff --git a/CleanApp/Modules/DomainLayer/Tests/UseCases/GetAllTodosUseCaseTests.swift b/CleanApp/Modules/DomainLayer/Tests/UseCases/GetAllTodosUseCaseTests.swift index 6934a26..e90aa5f 100644 --- a/CleanApp/Modules/DomainLayer/Tests/UseCases/GetAllTodosUseCaseTests.swift +++ b/CleanApp/Modules/DomainLayer/Tests/UseCases/GetAllTodosUseCaseTests.swift @@ -1,5 +1,5 @@ // -// GetAllTodosUseCaseTests.swift +// DefaultGetAllTodosUseCaseTests.swift // DomainLayerTests // // Created by Miguel Dönicke on 08.10.22. @@ -8,12 +8,12 @@ import DomainLayer import XCTest -final class GetAllTodosUseCaseTests: XCTestCase { +final class DefaultGetAllTodosUseCaseTests: XCTestCase { func testExecute() async throws { // Arrange let expected = [Todo.mock()] let repository = MockTodoRepository(todosResult: .success(expected)) - let sut = GetAllTodosUseCase(repository: repository) + let sut = DefaultGetAllTodosUseCase(repository: repository) // Act let result = try await sut.execute().get() diff --git a/CleanApp/Modules/PresentationLayer/.swiftpm/xcode/xcshareddata/xcschemes/PresentationLayer.xcscheme b/CleanApp/Modules/PresentationLayer/.swiftpm/xcode/xcshareddata/xcschemes/PresentationLayer.xcscheme new file mode 100644 index 0000000..9e4fd7b --- /dev/null +++ b/CleanApp/Modules/PresentationLayer/.swiftpm/xcode/xcshareddata/xcschemes/PresentationLayer.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CleanApp/Modules/PresentationLayer/Sources/ContentView.swift b/CleanApp/Modules/PresentationLayer/Sources/ContentView.swift deleted file mode 100644 index 30acaf5..0000000 --- a/CleanApp/Modules/PresentationLayer/Sources/ContentView.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// ContentView.swift -// -// -// Created by Miguel Dönicke on 22.10.21. -// - -import DomainLayer -import SwiftUI - -public struct ContentView: View { - public var body: some View { - Text(Domain().text) - .padding() - } - - public init() {} -} - -struct ContentView_Previews: PreviewProvider { - static var previews: some View { - ContentView() - } -} diff --git a/CleanApp/Modules/PresentationLayer/Sources/Preview Assets.xcassets/Contents.json b/CleanApp/Modules/PresentationLayer/Sources/Preview Content/Preview Assets.xcassets/Contents.json similarity index 100% rename from CleanApp/Modules/PresentationLayer/Sources/Preview Assets.xcassets/Contents.json rename to CleanApp/Modules/PresentationLayer/Sources/Preview Content/Preview Assets.xcassets/Contents.json diff --git a/CleanApp/Modules/PresentationLayer/Sources/Preview Content/UseCases/PreviewAddTodoUseCase.swift b/CleanApp/Modules/PresentationLayer/Sources/Preview Content/UseCases/PreviewAddTodoUseCase.swift new file mode 100644 index 0000000..b51b4c2 --- /dev/null +++ b/CleanApp/Modules/PresentationLayer/Sources/Preview Content/UseCases/PreviewAddTodoUseCase.swift @@ -0,0 +1,21 @@ +// +// PreviewAddTodoUseCase.swift +// PresentationLayer +// +// Created by Miguel Dönicke on 09.10.22. +// + +import DomainLayer +import Foundation + +final class PreviewAddTodoUseCase: AddTodoUseCase { + func execute(todo: Todo) async -> Result { + fatalError() + } +} + +extension AddTodoUseCase { + static var preview: PreviewAddTodoUseCase { + .init() + } +} diff --git a/CleanApp/Modules/PresentationLayer/Sources/Preview Content/UseCases/PreviewCompleteTodoUseCase.swift b/CleanApp/Modules/PresentationLayer/Sources/Preview Content/UseCases/PreviewCompleteTodoUseCase.swift new file mode 100644 index 0000000..b36ce82 --- /dev/null +++ b/CleanApp/Modules/PresentationLayer/Sources/Preview Content/UseCases/PreviewCompleteTodoUseCase.swift @@ -0,0 +1,21 @@ +// +// PreviewCompleteTodoUseCase.swift +// PresentationLayer +// +// Created by Miguel Dönicke on 09.10.22. +// + +import DomainLayer +import Foundation + +final class PreviewCompleteTodoUseCase: CompleteTodoUseCase { + func execute(id: Int) async -> Result { + fatalError() + } +} + +extension CompleteTodoUseCase { + static var preview: PreviewCompleteTodoUseCase { + .init() + } +} diff --git a/CleanApp/Modules/PresentationLayer/Sources/Preview Content/UseCases/PreviewGetAllTodosUseCase.swift b/CleanApp/Modules/PresentationLayer/Sources/Preview Content/UseCases/PreviewGetAllTodosUseCase.swift new file mode 100644 index 0000000..faa4885 --- /dev/null +++ b/CleanApp/Modules/PresentationLayer/Sources/Preview Content/UseCases/PreviewGetAllTodosUseCase.swift @@ -0,0 +1,25 @@ +// +// PreviewGetAllTodosUseCase.swift +// PresentationLayer +// +// Created by Miguel Dönicke on 09.10.22. +// + +import DomainLayer +import Foundation + +final class PreviewGetAllTodosUseCase: GetAllTodosUseCase { + func execute() async -> Result<[Todo], Error> { + .success([ + .init(id: 1, title: "Preview #1", completed: false), + .init(id: 2, title: "Preview #2", completed: true), + .init(id: 3, title: "Preview #3", completed: false) + ]) + } +} + +extension GetAllTodosUseCase { + static var preview: GetAllTodosUseCase { + PreviewGetAllTodosUseCase() + } +} diff --git a/CleanApp/Modules/PresentationLayer/Sources/Preview Content/ViewModels/TodoListViewModel+Preview.swift b/CleanApp/Modules/PresentationLayer/Sources/Preview Content/ViewModels/TodoListViewModel+Preview.swift new file mode 100644 index 0000000..8fb309b --- /dev/null +++ b/CleanApp/Modules/PresentationLayer/Sources/Preview Content/ViewModels/TodoListViewModel+Preview.swift @@ -0,0 +1,18 @@ +// +// TodoListViewModel+Preview.swift +// PresentationLayer +// +// Created by Miguel Dönicke on 09.10.22. +// + +import Foundation + +extension TodoListViewModel { + static var preview: TodoListViewModel { + .init( + getAllTodosUseCase: PreviewGetAllTodosUseCase(), + addTodoUseCase: PreviewAddTodoUseCase(), + completeTodoUseCase: PreviewCompleteTodoUseCase() + ) + } +} diff --git a/CleanApp/Modules/PresentationLayer/Sources/ViewModels/TodoListViewModel.swift b/CleanApp/Modules/PresentationLayer/Sources/ViewModels/TodoListViewModel.swift new file mode 100644 index 0000000..3f3c4f8 --- /dev/null +++ b/CleanApp/Modules/PresentationLayer/Sources/ViewModels/TodoListViewModel.swift @@ -0,0 +1,68 @@ +// +// TodoListViewModel.swift +// PresentationLayer +// +// Created by Miguel Dönicke on 09.10.22. +// + +import DomainLayer +import Foundation + +public class TodoListViewModel: ObservableObject { + private let getAllTodosUseCase: GetAllTodosUseCase + private let addTodoUseCase: AddTodoUseCase + private let completeTodoUseCase: CompleteTodoUseCase + + @Published public var newTodoText = "" + @Published public private(set) var todos = [Todo]() + @Published public private(set) var addNewTodoIsDisabled = false + + public init( + getAllTodosUseCase: GetAllTodosUseCase, + addTodoUseCase: AddTodoUseCase, + completeTodoUseCase: CompleteTodoUseCase + ) { + self.getAllTodosUseCase = getAllTodosUseCase + self.addTodoUseCase = addTodoUseCase + self.completeTodoUseCase = completeTodoUseCase + } + + @MainActor + @Sendable + public func loadTodos() async { + _ = await getAllTodosUseCase.execute() + .map { + self.todos = $0 + } + } + + @MainActor + public func addTodo() async { + defer { + addNewTodoIsDisabled = false + } + + addNewTodoIsDisabled = true + + let todo = Todo(title: newTodoText) + let result = await addTodoUseCase.execute(todo: todo) + if case .success(let newTodo) = result { + todos.append(newTodo) + newTodoText = "" + } else { + fatalError("FIX ME :D") + } + } + + @MainActor + public func completeTodo(id: Int) async { + let result = await completeTodoUseCase.execute(id: id) + if case .success(let completedTodo) = result, + let index = todos.firstIndex(where: { $0.id == completedTodo.id }) { + todos.remove(at: index) + todos.insert(completedTodo, at: index) + } else { + fatalError("FIX ME :D") + } + } +} diff --git a/CleanApp/Modules/PresentationLayer/Sources/Views/TodoListView.swift b/CleanApp/Modules/PresentationLayer/Sources/Views/TodoListView.swift new file mode 100644 index 0000000..66d03ff --- /dev/null +++ b/CleanApp/Modules/PresentationLayer/Sources/Views/TodoListView.swift @@ -0,0 +1,56 @@ +// +// TodoListView.swift +// PresentationLayer +// +// Created by Miguel Dönicke on 09.10.22. +// + +import DomainLayer +import SwiftUI + +public struct TodoListView: View { + @ObservedObject + public var viewModel: TodoListViewModel + + public var body: some View { + NavigationView { + VStack { + TextField("Add Todo", text: $viewModel.newTodoText) + .onSubmit { + Task { + await viewModel.addTodo() + } + } + .padding() + .disabled(viewModel.addNewTodoIsDisabled) + + List(viewModel.todos, id: \.id) { todo in + if let id = todo.id { + HStack { + Image(systemName: todo.completed ? "checkmark.circle" : "circle") + Text(todo.title) + } + .onTapGesture { + Task { + await viewModel.completeTodo(id: id) + } + } + } + } + .listStyle(.plain) + } + .task(viewModel.loadTodos) + .navigationTitle("Todos") + } + } + + public init(viewModel: TodoListViewModel) { + self.viewModel = viewModel + } +} + +struct TodoListView_Previews: PreviewProvider { + static var previews: some View { + TodoListView(viewModel: .preview) + } +} diff --git a/CleanApp/Modules/PresentationLayer/Tests/Mocks/Entities/Todo+Mock.swift b/CleanApp/Modules/PresentationLayer/Tests/Mocks/Entities/Todo+Mock.swift new file mode 100644 index 0000000..65a9359 --- /dev/null +++ b/CleanApp/Modules/PresentationLayer/Tests/Mocks/Entities/Todo+Mock.swift @@ -0,0 +1,18 @@ +// +// Todo+Mock.swift +// PresentationLayerTests +// +// Created by Miguel Dönicke on 09.10.22. +// + +import DomainLayer +import Foundation + +private var todos = [Todo]() + +extension Todo { + static func mock(completed: Bool = false) -> Todo { + let id = todos.count + 1 + return .init(id: id, title: "Mock #\(id)", completed: completed) + } +} diff --git a/CleanApp/Modules/PresentationLayer/Tests/Mocks/UseCases/MockAddTodoUseCase.swift b/CleanApp/Modules/PresentationLayer/Tests/Mocks/UseCases/MockAddTodoUseCase.swift new file mode 100644 index 0000000..e361281 --- /dev/null +++ b/CleanApp/Modules/PresentationLayer/Tests/Mocks/UseCases/MockAddTodoUseCase.swift @@ -0,0 +1,23 @@ +// +// MockAddTodoUseCase.swift +// PresentationLayerTests +// +// Created by Miguel Dönicke on 09.10.22. +// + +import DomainLayer +import Foundation + +final class MockAddTodoUseCase: AddTodoUseCase { + private(set) var executeCalled = false + var result: Result + + init(result: Result) { + self.result = result + } + + func execute(todo: Todo) async -> Result { + executeCalled = true + return result + } +} diff --git a/CleanApp/Modules/PresentationLayer/Tests/Mocks/UseCases/MockCompleteTodoUseCase.swift b/CleanApp/Modules/PresentationLayer/Tests/Mocks/UseCases/MockCompleteTodoUseCase.swift new file mode 100644 index 0000000..7636bf6 --- /dev/null +++ b/CleanApp/Modules/PresentationLayer/Tests/Mocks/UseCases/MockCompleteTodoUseCase.swift @@ -0,0 +1,23 @@ +// +// MockCompleteTodoUseCase.swift +// PresentationLayerTests +// +// Created by Miguel Dönicke on 09.10.22. +// + +import DomainLayer +import Foundation + +final class MockCompleteTodoUseCase: CompleteTodoUseCase { + private(set) var executeCalledId: Int? + var result: Result + + init(result: Result) { + self.result = result + } + + func execute(id: Int) async -> Result { + executeCalledId = id + return result + } +} diff --git a/CleanApp/Modules/PresentationLayer/Tests/Mocks/UseCases/MockGetAllTodosUseCase.swift b/CleanApp/Modules/PresentationLayer/Tests/Mocks/UseCases/MockGetAllTodosUseCase.swift new file mode 100644 index 0000000..d1eb49f --- /dev/null +++ b/CleanApp/Modules/PresentationLayer/Tests/Mocks/UseCases/MockGetAllTodosUseCase.swift @@ -0,0 +1,23 @@ +// +// MockGetAllTodosUseCase.swift +// PresentationLayerTests +// +// Created by Miguel Dönicke on 09.10.22. +// + +import DomainLayer +import Foundation + +final class MockGetAllTodosUseCase: GetAllTodosUseCase { + private(set) var executeCalled = false + var result: Result<[Todo], Error> + + init(result: Result<[Todo], Error>) { + self.result = result + } + + func execute() async -> Result<[Todo], Error> { + executeCalled = true + return result + } +} diff --git a/CleanApp/Modules/PresentationLayer/Tests/Mocks/ViewModels/TodoListViewModel+Mock.swift b/CleanApp/Modules/PresentationLayer/Tests/Mocks/ViewModels/TodoListViewModel+Mock.swift new file mode 100644 index 0000000..7584940 --- /dev/null +++ b/CleanApp/Modules/PresentationLayer/Tests/Mocks/ViewModels/TodoListViewModel+Mock.swift @@ -0,0 +1,20 @@ +// +// TodoListViewModel+Mock.swift +// PresentationLayerTests +// +// Created by Miguel Dönicke on 09.10.22. +// + +import DomainLayer +import Foundation +import PresentationLayer + +extension TodoListViewModel { + static var mock: TodoListViewModel { + .init( + getAllTodosUseCase: MockGetAllTodosUseCase(result: .success([Todo.mock()])), + addTodoUseCase: MockAddTodoUseCase(result: .success(Todo.mock())), + completeTodoUseCase: MockCompleteTodoUseCase(result: .success(Todo.mock())) + ) + } +} diff --git a/CleanApp/Modules/PresentationLayer/Tests/PresentationTests.swift b/CleanApp/Modules/PresentationLayer/Tests/PresentationTests.swift deleted file mode 100644 index 7261b43..0000000 --- a/CleanApp/Modules/PresentationLayer/Tests/PresentationTests.swift +++ /dev/null @@ -1,10 +0,0 @@ -import XCTest -@testable import PresentationLayer - -final class PresentationTests: XCTestCase { - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct - // results. - } -} diff --git a/CleanApp/Modules/PresentationLayer/Tests/ViewModels/TodoListViewModelTests.swift b/CleanApp/Modules/PresentationLayer/Tests/ViewModels/TodoListViewModelTests.swift new file mode 100644 index 0000000..6530be9 --- /dev/null +++ b/CleanApp/Modules/PresentationLayer/Tests/ViewModels/TodoListViewModelTests.swift @@ -0,0 +1,82 @@ +// +// TodoListViewModelTests.swift +// PresentationLayerTests +// +// Created by Miguel Dönicke on 09.10.22. +// + +import DomainLayer +import PresentationLayer +import XCTest + +final class TodoListViewModelTests: XCTestCase { + func testOnAppear() async throws { + // Arrange + let expected = [ + Todo.mock(completed: false), + Todo.mock(completed: true), + Todo.mock(completed: false) + ] + let getAllTodosUseCase = MockGetAllTodosUseCase(result: .success(expected)) + let addTodoUseCase = MockAddTodoUseCase(result: .success(expected.last!)) + let completeTodoUseCase = MockCompleteTodoUseCase(result: .success(expected.last!)) + + let sut = TodoListViewModel( + getAllTodosUseCase: getAllTodosUseCase, + addTodoUseCase: addTodoUseCase, + completeTodoUseCase: completeTodoUseCase + ) + + // Act + await sut.loadTodos() + + // Assert + XCTAssert(getAllTodosUseCase.executeCalled) + XCTAssertEqual(sut.todos, expected) + } + + func testAddTodo() async throws { + // Arrange + let expected = Todo.mock(completed: false) + let getAllTodosUseCase = MockGetAllTodosUseCase(result: .success([])) + let addTodoUseCase = MockAddTodoUseCase(result: .success(expected)) + let completeTodoUseCase = MockCompleteTodoUseCase(result: .success(expected)) + + let sut = TodoListViewModel( + getAllTodosUseCase: getAllTodosUseCase, + addTodoUseCase: addTodoUseCase, + completeTodoUseCase: completeTodoUseCase + ) + sut.newTodoText = expected.title + + // Act + await sut.addTodo() + + // Assert + XCTAssert(addTodoUseCase.executeCalled) + XCTAssertEqual(sut.todos, [expected]) + XCTAssertEqual(sut.newTodoText, "") + } + + func testCompleteTodo() async throws { + // Arrange + let expected = Todo.mock(completed: true) + let getAllTodosUseCase = MockGetAllTodosUseCase(result: .success([Todo(id: expected.id, title: expected.title)])) + let addTodoUseCase = MockAddTodoUseCase(result: .success(expected)) + let completeTodoUseCase = MockCompleteTodoUseCase(result: .success(expected)) + + let sut = TodoListViewModel( + getAllTodosUseCase: getAllTodosUseCase, + addTodoUseCase: addTodoUseCase, + completeTodoUseCase: completeTodoUseCase + ) + + // Act + await sut.loadTodos() + await sut.completeTodo(id: expected.id!) + + // Assert + XCTAssertEqual(completeTodoUseCase.executeCalledId, expected.id) + XCTAssertEqual(sut.todos, [expected]) + } +} diff --git a/CleanApp/Modules/PresentationLayer/Tests/Views/TodoListViewTests.swift b/CleanApp/Modules/PresentationLayer/Tests/Views/TodoListViewTests.swift new file mode 100644 index 0000000..8501061 --- /dev/null +++ b/CleanApp/Modules/PresentationLayer/Tests/Views/TodoListViewTests.swift @@ -0,0 +1,15 @@ +// +// TodoListViewTests.swift +// PresentationLayerTests +// +// Created by Miguel Dönicke on 09.10.22. +// + +import XCTest +import PresentationLayer + +final class TodoListViewTests: XCTestCase { + func testTodoListView() throws { + _ = TodoListView(viewModel: .mock) + } +} diff --git a/CleanApp/iOS/CleanApp.swift b/CleanApp/iOS/CleanApp.swift index 62d86eb..67ae87d 100644 --- a/CleanApp/iOS/CleanApp.swift +++ b/CleanApp/iOS/CleanApp.swift @@ -10,9 +10,11 @@ import SwiftUI @main struct CleanApp: App { + let diContainer = DIContainer() + var body: some Scene { WindowGroup { - ContentView() + TodoListView(viewModel: diContainer.makeTodoListViewModel()) } } } diff --git a/CleanApp/iOS/DIContainer.swift b/CleanApp/iOS/DIContainer.swift new file mode 100644 index 0000000..eb65dea --- /dev/null +++ b/CleanApp/iOS/DIContainer.swift @@ -0,0 +1,26 @@ +// +// DIContainer.swift +// iOS +// +// Created by Miguel Dönicke on 09.10.22. +// + +import DataLayer +import DomainLayer +import Foundation +import PresentationLayer + +final class DIContainer { + lazy var todoRepository = LocalTodoRepository() + lazy var getAllTodosUseCase = DefaultGetAllTodosUseCase(repository: todoRepository) + lazy var addTodoUseCase = DefaultAddTodoUseCase(repository: todoRepository) + lazy var completeTodoUseCase = DefaultCompleteTodoUseCase(repository: todoRepository) + + func makeTodoListViewModel() -> TodoListViewModel { + TodoListViewModel( + getAllTodosUseCase: getAllTodosUseCase, + addTodoUseCase: addTodoUseCase, + completeTodoUseCase: completeTodoUseCase + ) + } +}