From 48f8f5d5c29634ce8f802b37434ae43c4fc84e31 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 11 Feb 2026 17:44:44 +0100 Subject: [PATCH 01/11] Defined the Repository protocol in the app target. --- App/Sources/Protocols/Repository.swift | 28 ++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 App/Sources/Protocols/Repository.swift diff --git a/App/Sources/Protocols/Repository.swift b/App/Sources/Protocols/Repository.swift new file mode 100644 index 0000000..c1b97b3 --- /dev/null +++ b/App/Sources/Protocols/Repository.swift @@ -0,0 +1,28 @@ +import SwiftData + +/// A protocol defining the interface for a data repository. +/// +/// This protocol abstracts the underlying data persistence mechanism, allowing for +/// dependency injection and easier testing. Implementations handle the storage and +/// retrieval of model objects. +protocol Repository { + + /// Deletes a model object from the repository. + /// + /// This method removes the specified object from persistent storage. The deletion + /// is typically performed immediately, though the actual persistence may be deferred + /// until a save operation occurs depending on the implementation. + /// + /// - Parameter model: The model object to delete from the repository. + func delete(_ model: some PersistentModel) + + /// Inserts a new model object into the repository. + /// + /// This method adds a new object to persistent storage. The object is typically + /// tracked immediately, though the actual persistence may be deferred until a + /// save operation occurs depending on the implementation. + /// + /// - Parameter model: The model object to insert into the repository. + func insert(_ model: some PersistentModel) + +} -- 2.54.0 From fb8815dafc28d06bd17daa81b78ef53f3d9833e6 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 11 Feb 2026 17:46:34 +0100 Subject: [PATCH 02/11] Conformed the ModelContext from SwiftData to the Repository protocol in the app target. --- App/Sources/Extensions/ModelContext+Conformances.swift | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 App/Sources/Extensions/ModelContext+Conformances.swift diff --git a/App/Sources/Extensions/ModelContext+Conformances.swift b/App/Sources/Extensions/ModelContext+Conformances.swift new file mode 100644 index 0000000..b13caec --- /dev/null +++ b/App/Sources/Extensions/ModelContext+Conformances.swift @@ -0,0 +1,8 @@ +import SwiftData + +/// Extends ModelContext to conform to the Repository protocol. +/// +/// This extension allows ModelContext from SwiftData to be used as a repository, +/// enabling dependency injection and facilitating unit testing by allowing mock +/// repositories to be substituted during tests. +extension ModelContext: Repository {} -- 2.54.0 From 85bd3f6a38ab6020fc413fdff32643288d168415 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 11 Feb 2026 17:47:40 +0100 Subject: [PATCH 03/11] Updated the "save(to: )" function of the NewTaskViewModel view model in the app target to use the Repository protocol instead. --- App/Sources/View Models/NewTaskViewModel.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/App/Sources/View Models/NewTaskViewModel.swift b/App/Sources/View Models/NewTaskViewModel.swift index 732c753..9b3fbef 100644 --- a/App/Sources/View Models/NewTaskViewModel.swift +++ b/App/Sources/View Models/NewTaskViewModel.swift @@ -57,14 +57,14 @@ extension NewTaskView { // MARK: Functions - /// Saves a new todo task to the SwiftData model context. + /// Saves a new todo task to the repository. /// - /// This function creates a new `Todo` instance with the current property values and inserts it into the provided model context. + /// This function creates a new `Todo` instance with the current property values and inserts it into the provided repository. /// The title and note are trimmed of leading and trailing whitespace before saving. /// The `note` property value is only included if it's not empty after trimming. /// - /// - Parameter modelContext: The SwiftData model context to insert a new todo task. - func save(to modelContext: ModelContext) { + /// - Parameter repository: The repository to insert a new todo task. + func save(to repository: Repository) { let trimmedTitle = title.trimmingCharacters(in: .whitespacesAndNewlines) let trimmedNote = note.trimmingCharacters(in: .whitespacesAndNewlines) @@ -74,7 +74,7 @@ extension NewTaskView { due: due ) - modelContext.insert(todo) + repository.insert(todo) } } -- 2.54.0 From 61adf5387fa5e4eaf4bc47835a0571dd2fd7ed37 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 11 Feb 2026 17:49:19 +0100 Subject: [PATCH 04/11] Updated the "delete(_: at: from: )" function of the TaskListViewModel view model in the app target to use the Repository protocol instead. --- App/Sources/View Models/TaskListViewModel.swift | 12 ++++++------ App/Sources/Views/TaskListView.swift | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/App/Sources/View Models/TaskListViewModel.swift b/App/Sources/View Models/TaskListViewModel.swift index 3eec5fc..9782bdf 100644 --- a/App/Sources/View Models/TaskListViewModel.swift +++ b/App/Sources/View Models/TaskListViewModel.swift @@ -87,21 +87,21 @@ extension TaskListView { /// Deletes tasks at the specified indices. /// - /// This method removes tasks from the model context after a swipe-to-delete action. - /// The deletion is performed through SwiftData's model context, which handles + /// This method removes tasks from the repository after a swipe-to-delete action. + /// The deletion is performed through the repository, which handles /// persistence and triggers automatic UI updates. /// /// - Parameters: /// - tasks: The current array of tasks. /// - offsets: The index set of tasks to delete. - /// - modelContext: The SwiftData model context to perform the deletion. - func deleteTasks( + /// - repository: The repository to perform the deletion. + func delete( _ tasks: [Todo], at offsets: IndexSet, - from modelContext: ModelContext + from repository: Repository ) { for index in offsets { - modelContext.delete(tasks[index]) + repository.delete(tasks[index]) } } diff --git a/App/Sources/Views/TaskListView.swift b/App/Sources/Views/TaskListView.swift index 955ec32..dacf3e2 100644 --- a/App/Sources/Views/TaskListView.swift +++ b/App/Sources/Views/TaskListView.swift @@ -45,7 +45,7 @@ struct TaskListView: View { group: viewModel.group, showCompleted: viewModel.showCompleted ) { tasks, offsets in - viewModel.deleteTasks( + viewModel.delete( tasks, at: offsets, from: modelContext -- 2.54.0 From 8772635ad97b464c0fcf606ec8f3b3bc0311f2ea Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 11 Feb 2026 18:04:36 +0100 Subject: [PATCH 05/11] Renamed the Repository protocol in the app target as TodoRepository. --- .../ModelContext+Conformances.swift | 10 +++++-- App/Sources/Protocols/Repository.swift | 28 ------------------- App/Sources/Protocols/TodoRepository.swift | 28 +++++++++++++++++++ .../View Models/NewTaskViewModel.swift | 2 +- .../View Models/TaskListViewModel.swift | 2 +- 5 files changed, 37 insertions(+), 33 deletions(-) delete mode 100644 App/Sources/Protocols/Repository.swift create mode 100644 App/Sources/Protocols/TodoRepository.swift diff --git a/App/Sources/Extensions/ModelContext+Conformances.swift b/App/Sources/Extensions/ModelContext+Conformances.swift index b13caec..593e454 100644 --- a/App/Sources/Extensions/ModelContext+Conformances.swift +++ b/App/Sources/Extensions/ModelContext+Conformances.swift @@ -1,8 +1,12 @@ import SwiftData -/// Extends ModelContext to conform to the Repository protocol. +/// Extends ModelContext to conform to the TodoRepository protocol. /// -/// This extension allows ModelContext from SwiftData to be used as a repository, +/// This extension allows ModelContext from SwiftData to be used as a todo repository, /// enabling dependency injection and facilitating unit testing by allowing mock /// repositories to be substituted during tests. -extension ModelContext: Repository {} +/// +/// The ModelContext methods `delete(_:)` and `insert(_:)` already match the +/// TodoRepository protocol requirements, so this extension simply declares conformance +/// without requiring additional implementation. +extension ModelContext: TodoRepository {} diff --git a/App/Sources/Protocols/Repository.swift b/App/Sources/Protocols/Repository.swift deleted file mode 100644 index c1b97b3..0000000 --- a/App/Sources/Protocols/Repository.swift +++ /dev/null @@ -1,28 +0,0 @@ -import SwiftData - -/// A protocol defining the interface for a data repository. -/// -/// This protocol abstracts the underlying data persistence mechanism, allowing for -/// dependency injection and easier testing. Implementations handle the storage and -/// retrieval of model objects. -protocol Repository { - - /// Deletes a model object from the repository. - /// - /// This method removes the specified object from persistent storage. The deletion - /// is typically performed immediately, though the actual persistence may be deferred - /// until a save operation occurs depending on the implementation. - /// - /// - Parameter model: The model object to delete from the repository. - func delete(_ model: some PersistentModel) - - /// Inserts a new model object into the repository. - /// - /// This method adds a new object to persistent storage. The object is typically - /// tracked immediately, though the actual persistence may be deferred until a - /// save operation occurs depending on the implementation. - /// - /// - Parameter model: The model object to insert into the repository. - func insert(_ model: some PersistentModel) - -} diff --git a/App/Sources/Protocols/TodoRepository.swift b/App/Sources/Protocols/TodoRepository.swift new file mode 100644 index 0000000..4cd9a48 --- /dev/null +++ b/App/Sources/Protocols/TodoRepository.swift @@ -0,0 +1,28 @@ +import SwiftData + +/// A protocol defining the interface for a todo repository. +/// +/// This protocol abstracts the underlying data persistence mechanism for todo tasks, +/// allowing for dependency injection and easier testing. Implementations handle the +/// storage and retrieval of Todo objects. +protocol TodoRepository { + + /// Deletes a todo task from the repository. + /// + /// This method removes the specified todo from persistent storage. The deletion + /// is typically performed immediately, though the actual persistence may be deferred + /// until a save operation occurs depending on the implementation. + /// + /// - Parameter todo: The todo task to delete from the repository. + func delete(_ todo: Todo) + + /// Inserts a new todo task into the repository. + /// + /// This method adds a new todo to persistent storage. The todo is typically + /// tracked immediately, though the actual persistence may be deferred until a + /// save operation occurs depending on the implementation. + /// + /// - Parameter todo: The todo task to insert into the repository. + func insert(_ todo: Todo) + +} diff --git a/App/Sources/View Models/NewTaskViewModel.swift b/App/Sources/View Models/NewTaskViewModel.swift index 9b3fbef..9f03a0f 100644 --- a/App/Sources/View Models/NewTaskViewModel.swift +++ b/App/Sources/View Models/NewTaskViewModel.swift @@ -64,7 +64,7 @@ extension NewTaskView { /// The `note` property value is only included if it's not empty after trimming. /// /// - Parameter repository: The repository to insert a new todo task. - func save(to repository: Repository) { + func save(to repository: TodoRepository) { let trimmedTitle = title.trimmingCharacters(in: .whitespacesAndNewlines) let trimmedNote = note.trimmingCharacters(in: .whitespacesAndNewlines) diff --git a/App/Sources/View Models/TaskListViewModel.swift b/App/Sources/View Models/TaskListViewModel.swift index 9782bdf..b4aae30 100644 --- a/App/Sources/View Models/TaskListViewModel.swift +++ b/App/Sources/View Models/TaskListViewModel.swift @@ -98,7 +98,7 @@ extension TaskListView { func delete( _ tasks: [Todo], at offsets: IndexSet, - from repository: Repository + from repository: TodoRepository ) { for index in offsets { repository.delete(tasks[index]) -- 2.54.0 From 9c8a7bd465321847661176716b99129710e5d1d4 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 11 Feb 2026 18:29:29 +0100 Subject: [PATCH 06/11] Implemented the MockTodoRepository repository in the unit tests target. --- UnitTests/Mocks/MockTodoRepository.swift | 49 ++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 UnitTests/Mocks/MockTodoRepository.swift diff --git a/UnitTests/Mocks/MockTodoRepository.swift b/UnitTests/Mocks/MockTodoRepository.swift new file mode 100644 index 0000000..c7e1780 --- /dev/null +++ b/UnitTests/Mocks/MockTodoRepository.swift @@ -0,0 +1,49 @@ +@testable import class Todos.Todo +@testable import protocol Todos.TodoRepository + +/// A mock implementation of the TodoRepository protocol for unit testing. +/// +/// This mock repository provides a simple in-memory storage mechanism for testing +/// without requiring a full SwiftData ModelContext setup. It tracks inserted and +/// deleted todos, allowing tests to verify repository operations. +final class MockTodoRepository { + + // MARK: Properties + + /// Array of todo tasks managed by this mock repository. + /// + /// This property tracks all todos in the repository, maintaining state across + /// insert and delete operations. It serves as the in-memory storage for the mock, + /// allowing tests to verify repository behavior without requiring SwiftData. + private(set) var todos: [Todo] + + // MARK: Initializers + + /// Creates a new mock repository with an optional initial set of todo tasks. + /// + /// This initializer allows tests to set up a repository with pre-existing todos + /// or start with an empty repository. The default parameter value provides + /// convenient initialization for most test cases. + /// + /// - Parameter todos: An array of todo tasks to initialize the repository with. Defaults to an empty array. + init(todos: [Todo] = []) { + self.todos = todos + } + +} + +// MARK: - TodoRepository + +extension MockTodoRepository: TodoRepository { + + // MARK: Functions + + func delete(_ todo: Todo) { + todos.removeAll { $0.id == todo.id } + } + + func insert(_ todo: Todo) { + todos.append(todo) + } + +} -- 2.54.0 From 12eec3059ce61047b6a331b3ad0f7ef511483aba Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 11 Feb 2026 18:34:21 +0100 Subject: [PATCH 07/11] Updated the cases of the NewTaskViewModelTests tests in the unit tests target to use the MockTodoRepository class instead. --- .../View Models/NewTaskViewModelTests.swift | 67 +++++-------------- 1 file changed, 16 insertions(+), 51 deletions(-) diff --git a/UnitTests/Tests/View Models/NewTaskViewModelTests.swift b/UnitTests/Tests/View Models/NewTaskViewModelTests.swift index 0f32407..b438c16 100644 --- a/UnitTests/Tests/View Models/NewTaskViewModelTests.swift +++ b/UnitTests/Tests/View Models/NewTaskViewModelTests.swift @@ -1,5 +1,4 @@ import Foundation -import SwiftData import Testing @testable import Todos @@ -121,11 +120,9 @@ struct NewTaskViewModelTests { // MARK: Functions tests @Test("save creates todo with all properties") - @MainActor func saveWithAllProperties() throws { // GIVEN - let container = try modelContainer - let context = container.mainContext + let repository = MockTodoRepository() let viewModel = NewTaskView.ViewModel() // WHEN @@ -133,12 +130,10 @@ struct NewTaskViewModelTests { viewModel.note = "Finish by end of week" viewModel.due = Date(timeIntervalSince1970: 1000000000) - viewModel.save(to: context) + viewModel.save(to: repository) // THEN - let descriptor = FetchDescriptor() - let todos = try context.fetch(descriptor) - let todo = try #require(todos.first) + let todo = try #require(repository.todos.first) #expect(todo.title == viewModel.title) #expect(todo.note == viewModel.note) @@ -147,11 +142,9 @@ struct NewTaskViewModelTests { } @Test("save creates todo without note when note is empty") - @MainActor func saveWithEmptyNote() throws { // GIVEN - let container = try modelContainer - let context = container.mainContext + let repository = MockTodoRepository() let viewModel = NewTaskView.ViewModel() // WHEN @@ -159,12 +152,10 @@ struct NewTaskViewModelTests { viewModel.note = "" viewModel.due = Date() - viewModel.save(to: context) + viewModel.save(to: repository) // THEN - let descriptor = FetchDescriptor() - let todos = try context.fetch(descriptor) - let todo = try #require(todos.first) + let todo = try #require(repository.todos.first) #expect(todo.title == viewModel.title) #expect(todo.note == nil) @@ -173,11 +164,9 @@ struct NewTaskViewModelTests { } @Test("save creates todo with note containing only whitespace as nil") - @MainActor func saveWithWhitespaceNote() throws { // GIVEN - let container = try modelContainer - let context = container.mainContext + let repository = MockTodoRepository() let viewModel = NewTaskView.ViewModel() // WHEN @@ -185,12 +174,10 @@ struct NewTaskViewModelTests { viewModel.note = " " viewModel.due = Date() - viewModel.save(to: context) + viewModel.save(to: repository) // THEN - let descriptor = FetchDescriptor() - let todos = try context.fetch(descriptor) - let todo = try #require(todos.first) + let todo = try #require(repository.todos.first) #expect(todo.title == viewModel.title) #expect(todo.note == nil) @@ -199,46 +186,24 @@ struct NewTaskViewModelTests { } @Test("save inserts multiple todos independently") - @MainActor - func saveMultipleTodos() throws { + func saveMultipleTodos() { // GIVEN - let container = try modelContainer - let context = container.mainContext + let repository = MockTodoRepository() let viewModelFirst = NewTaskView.ViewModel() let viewModelSecond = NewTaskView.ViewModel() // WHEN viewModelFirst.title = "First task" - viewModelFirst.save(to: context) + viewModelFirst.save(to: repository) viewModelSecond.title = "Second task" viewModelSecond.note = "With a note" - viewModelSecond.save(to: context) + viewModelSecond.save(to: repository) // THEN - let descriptor = FetchDescriptor() - let todos = try context.fetch(descriptor) - - #expect(todos.count == 2) - #expect(todos.contains { $0.title == "First task" }) - #expect(todos.contains { $0.title == "Second task" }) + #expect(repository.todos.count == 2) + #expect(repository.todos.contains { $0.title == "First task" }) + #expect(repository.todos.contains { $0.title == "Second task" }) } } - -// MARK: - Helpers - -private extension NewTaskViewModelTests { - - // MARK: Computed - - var modelContainer: ModelContainer { - get throws { - try ModelContainer( - for: Todo.self, - configurations: .init(isStoredInMemoryOnly: true) - ) - } - } - -} -- 2.54.0 From 3dab65ce0ad2899d281d1b343976eeeebd85881e Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 11 Feb 2026 18:38:07 +0100 Subject: [PATCH 08/11] Updated the cases of the TaskListViewModelTests tests in the unit tests target to use the MockTodoRepository class instead. --- .../View Models/TaskListViewModelTests.swift | 70 +++++++++++-------- 1 file changed, 42 insertions(+), 28 deletions(-) diff --git a/UnitTests/Tests/View Models/TaskListViewModelTests.swift b/UnitTests/Tests/View Models/TaskListViewModelTests.swift index 619409b..ba2016b 100644 --- a/UnitTests/Tests/View Models/TaskListViewModelTests.swift +++ b/UnitTests/Tests/View Models/TaskListViewModelTests.swift @@ -1,5 +1,3 @@ -import Foundation -import SwiftData import SwiftUI import Testing @@ -91,43 +89,59 @@ struct TaskListViewModelTests { // MARK: Method tests - @Test("deleteTasks removes a single task from model context") - func deleteTasksSingle() throws { + @Test("delete removes a single todo task from repository") + func deleteSingleTodo() { // GIVEN - let container = try ModelContainer( - for: Todo.self, - configurations: .init(isStoredInMemoryOnly: true) - ) - let context = ModelContext(container) - - let viewModel = TaskListView.ViewModel(group: .today) - let todosToInsert: [Todo] = [ + let todos: [Todo] = [ .init(title: "Task 1", due: .now, order: 0), .init(title: "Task 2", due: .now, order: 1), .init(title: "Task 3", due: .now, order: 2), ] - let countInitial = todosToInsert.count - - for todo in todosToInsert { - context.insert(todo) - } + let repository = MockTodoRepository(todos: todos) + let viewModel = TaskListView.ViewModel(group: .today) // WHEN - viewModel.deleteTasks( - todosToInsert, + viewModel.delete( + todos, at: IndexSet(integer: 1), - from: context + from: repository ) // THEN - let descriptor = FetchDescriptor() - let todos = try context.fetch(descriptor) + #expect(repository.todos.count == 2) + #expect(repository.todos.contains { $0.title == "Task 1" }) + #expect(repository.todos.contains { $0.title == "Task 3" }) + #expect(!repository.todos.contains { $0.title == "Task 2" }) + } - #expect(countInitial == 3) - #expect(todos.count == 2) - #expect(todos.contains(where: { $0.title == "Task 1" })) - #expect(todos.contains(where: { $0.title == "Task 3" })) - #expect(!todos.contains(where: { $0.title == "Task 2" })) + @Test("delete removes multiple todo tasks from repository") + func deleteMultipleTodos() { + // GIVEN + let todos: [Todo] = [ + .init(title: "Task 1", due: .now, order: 0), + .init(title: "Task 2", due: .now, order: 1), + .init(title: "Task 3", due: .now, order: 2), + .init(title: "Task 4", due: .now, order: 3), + .init(title: "Task 5", due: .now, order: 4), + ] + + let repository = MockTodoRepository(todos: todos) + let viewModel = TaskListView.ViewModel(group: .today) + + // WHEN + viewModel.delete( + todos, + at: IndexSet([1, 3]), + from: repository + ) + + // THEN + #expect(repository.todos.count == 3) + #expect(repository.todos.contains { $0.title == "Task 1" }) + #expect(repository.todos.contains { $0.title == "Task 3" }) + #expect(repository.todos.contains { $0.title == "Task 5" }) + #expect(!repository.todos.contains { $0.title == "Task 2" }) + #expect(!repository.todos.contains { $0.title == "Task 4" }) } @Test("reorderTasks updates order property when moving task forward") @@ -180,7 +194,7 @@ struct TaskListViewModelTests { #expect(tasks[3].order == 1) } - @Test("toggleCompletedVisbility toggles the showCompleted property") + @Test("toggleCompletedVisibility toggles the showCompleted property") func toggleCompletedVisibility() { // GIVEN let viewModel = TaskListView.ViewModel(group: .today) -- 2.54.0 From 097b0fc23deef8cb4b4e6175b1e3391b0b2a61f6 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 11 Feb 2026 18:38:54 +0100 Subject: [PATCH 09/11] Removed unused imports from the TodoGroupTests, ContextViewModelTests and TaskGroupListViewModelTests tests in the unit tests target. --- UnitTests/Tests/Enumerations/TodoGroupTests.swift | 1 - UnitTests/Tests/View Models/ContentViewModelTests.swift | 2 -- UnitTests/Tests/View Models/TaskGroupListViewModelTests.swift | 1 - 3 files changed, 4 deletions(-) diff --git a/UnitTests/Tests/Enumerations/TodoGroupTests.swift b/UnitTests/Tests/Enumerations/TodoGroupTests.swift index b39933d..ea134a3 100644 --- a/UnitTests/Tests/Enumerations/TodoGroupTests.swift +++ b/UnitTests/Tests/Enumerations/TodoGroupTests.swift @@ -1,4 +1,3 @@ -import Foundation import SwiftUI import Testing diff --git a/UnitTests/Tests/View Models/ContentViewModelTests.swift b/UnitTests/Tests/View Models/ContentViewModelTests.swift index adb8e25..2a8e0eb 100644 --- a/UnitTests/Tests/View Models/ContentViewModelTests.swift +++ b/UnitTests/Tests/View Models/ContentViewModelTests.swift @@ -1,5 +1,3 @@ -import Foundation -import SwiftData import Testing @testable import Todos diff --git a/UnitTests/Tests/View Models/TaskGroupListViewModelTests.swift b/UnitTests/Tests/View Models/TaskGroupListViewModelTests.swift index 4e5068c..2069adf 100644 --- a/UnitTests/Tests/View Models/TaskGroupListViewModelTests.swift +++ b/UnitTests/Tests/View Models/TaskGroupListViewModelTests.swift @@ -1,5 +1,4 @@ import Foundation -import SwiftData import Testing @testable import Todos -- 2.54.0 From e9e091c4d52f491640553424c8eeadb8bd8269df Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 11 Feb 2026 18:56:54 +0100 Subject: [PATCH 10/11] Updated the project to use Swift 6 and strict concurrency. --- Todos.xcodeproj/project.pbxproj | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Todos.xcodeproj/project.pbxproj b/Todos.xcodeproj/project.pbxproj index 7747608..591864f 100644 --- a/Todos.xcodeproj/project.pbxproj +++ b/Todos.xcodeproj/project.pbxproj @@ -410,8 +410,9 @@ SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2,7"; XROS_DEPLOYMENT_TARGET = 26.0; }; @@ -456,8 +457,9 @@ SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2,7"; XROS_DEPLOYMENT_TARGET = 26.0; }; -- 2.54.0 From 546985bd9868853306c96761368e431e2507b1ef Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 11 Feb 2026 19:15:56 +0100 Subject: [PATCH 11/11] Fixed issues encountered in the unit tests target after the project transitioned to Swift 6 and strict concurrency. --- .../Tests/Enumerations/TodoGroupTests.swift | 16 ++++++++++------ .../TodoFetchDescriptor+InitsTests.swift | 10 ++++------ .../View Models/ContentViewModelTests.swift | 1 + .../View Models/NewTaskViewModelTests.swift | 1 + .../TaskGroupListViewModelTests.swift | 5 +++-- .../View Models/TaskListViewModelTests.swift | 9 +++++---- 6 files changed, 24 insertions(+), 18 deletions(-) diff --git a/UnitTests/Tests/Enumerations/TodoGroupTests.swift b/UnitTests/Tests/Enumerations/TodoGroupTests.swift index ea134a3..d264d4f 100644 --- a/UnitTests/Tests/Enumerations/TodoGroupTests.swift +++ b/UnitTests/Tests/Enumerations/TodoGroupTests.swift @@ -3,6 +3,7 @@ import Testing @testable import Todos +@MainActor @Suite("TodoGroup enumeration tests") struct TodoGroupTests { @@ -11,7 +12,7 @@ struct TodoGroupTests { @Test( "canShowCompleted returns correct values", arguments: zip( - TodoGroup.allCases, + [TodoGroup.today, .upcoming, .overdue, .completed], [true, true, true, false] ) ) @@ -30,7 +31,7 @@ struct TodoGroupTests { @Test( "canReorder returns correct values", arguments: zip( - TodoGroup.allCases, + [TodoGroup.today, .upcoming, .overdue, .completed], [true, true, true, false] ) ) @@ -49,7 +50,7 @@ struct TodoGroupTests { @Test( "group color match expected values", arguments: zip( - TodoGroup.allCases, + [TodoGroup.today, .upcoming, .overdue, .completed], [Color.blue, .green, .red, .gray] ) ) @@ -68,7 +69,7 @@ struct TodoGroupTests { @Test( "group icon names match expected values", arguments: zip( - TodoGroup.allCases, + [TodoGroup.today, .upcoming, .overdue, .completed], ["calendar.badge.clock", "calendar", "calendar.badge.exclamationmark", "checkmark.circle.fill"] ) ) @@ -87,7 +88,7 @@ struct TodoGroupTests { @Test( "group titles match expected values", arguments: zip( - TodoGroup.allCases, + [TodoGroup.today, .upcoming, .overdue, .completed], ["today", "upcoming", "overdue", "completed"] ) ) @@ -123,7 +124,10 @@ struct TodoGroupTests { // MARK: Identifiable Tests - @Test("id returns an identity of a case", arguments: TodoGroup.allCases) + @Test( + "id returns an identity of a case", + arguments: [TodoGroup.today, .upcoming, .overdue, .completed] + ) func id(for group: TodoGroup) { // GIVEN // WHEN diff --git a/UnitTests/Tests/Extensions/TodoFetchDescriptor+InitsTests.swift b/UnitTests/Tests/Extensions/TodoFetchDescriptor+InitsTests.swift index 6703150..b62748f 100644 --- a/UnitTests/Tests/Extensions/TodoFetchDescriptor+InitsTests.swift +++ b/UnitTests/Tests/Extensions/TodoFetchDescriptor+InitsTests.swift @@ -4,6 +4,7 @@ import Testing @testable import Todos +@MainActor @Suite("TodoFetchDescriptor+Inits extension tests") struct TodoFetchDescriptor_InitsTests { @@ -11,9 +12,8 @@ struct TodoFetchDescriptor_InitsTests { @Test( "initialization when empty tasks", - arguments: TodoGroup.allCases + arguments: [TodoGroup.today, .upcoming, .overdue, .completed] ) - @MainActor func initializationWhenEmptyTasks(for group: TodoGroup) throws { // GIVEN let container = try modelContainer @@ -32,9 +32,8 @@ struct TodoFetchDescriptor_InitsTests { @Test( "initialization with show completed disabled", - arguments: TodoGroup.allCases + arguments: [TodoGroup.today, .upcoming, .overdue, .completed] ) - @MainActor func initializationWithNoShowCompleted(for group: TodoGroup) throws { // GIVEN let container = try modelContainer @@ -70,9 +69,8 @@ struct TodoFetchDescriptor_InitsTests { @Test( "initialization with show completed enabled", - arguments: TodoGroup.allCases + arguments: [TodoGroup.today, .upcoming, .overdue, .completed] ) - @MainActor func initializationWithShowCompleted(for group: TodoGroup) throws { // GIVEN let container = try modelContainer diff --git a/UnitTests/Tests/View Models/ContentViewModelTests.swift b/UnitTests/Tests/View Models/ContentViewModelTests.swift index 2a8e0eb..677520a 100644 --- a/UnitTests/Tests/View Models/ContentViewModelTests.swift +++ b/UnitTests/Tests/View Models/ContentViewModelTests.swift @@ -2,6 +2,7 @@ import Testing @testable import Todos +@MainActor @Suite("ContentView view model tests") struct ContentViewModelTests { diff --git a/UnitTests/Tests/View Models/NewTaskViewModelTests.swift b/UnitTests/Tests/View Models/NewTaskViewModelTests.swift index b438c16..40cdf44 100644 --- a/UnitTests/Tests/View Models/NewTaskViewModelTests.swift +++ b/UnitTests/Tests/View Models/NewTaskViewModelTests.swift @@ -3,6 +3,7 @@ import Testing @testable import Todos +@MainActor @Suite("NewTaskView view model tests") struct NewTaskViewModelTests { diff --git a/UnitTests/Tests/View Models/TaskGroupListViewModelTests.swift b/UnitTests/Tests/View Models/TaskGroupListViewModelTests.swift index 2069adf..0def748 100644 --- a/UnitTests/Tests/View Models/TaskGroupListViewModelTests.swift +++ b/UnitTests/Tests/View Models/TaskGroupListViewModelTests.swift @@ -3,6 +3,7 @@ import Testing @testable import Todos +@MainActor @Suite("TaskGroupListView view model tests") struct TaskGroupListViewModelTests { @@ -32,7 +33,7 @@ struct TaskGroupListViewModelTests { @Test( "count returns zero when empty data", - arguments: TodoGroup.allCases + arguments: [TodoGroup.today, .upcoming, .overdue, .completed] ) func countWhenEmptyData(for group: TodoGroup) throws { // GIVEN @@ -47,7 +48,7 @@ struct TaskGroupListViewModelTests { @Test( "count returns correct count when filled data", - arguments: TodoGroup.allCases + arguments: [TodoGroup.today, .upcoming, .overdue, .completed] ) func countWhenFilledData(for group: TodoGroup) throws { // GIVEN diff --git a/UnitTests/Tests/View Models/TaskListViewModelTests.swift b/UnitTests/Tests/View Models/TaskListViewModelTests.swift index ba2016b..9409e46 100644 --- a/UnitTests/Tests/View Models/TaskListViewModelTests.swift +++ b/UnitTests/Tests/View Models/TaskListViewModelTests.swift @@ -3,6 +3,7 @@ import Testing @testable import Todos +@MainActor @Suite("TaskList view model tests") struct TaskListViewModelTests { @@ -10,7 +11,7 @@ struct TaskListViewModelTests { @Test( "properties are set correctly during initialization", - arguments: TodoGroup.allCases + arguments: [TodoGroup.today, .upcoming, .overdue, .completed] ) func initialization(for group: TodoGroup) { // GIVEN @@ -26,7 +27,7 @@ struct TaskListViewModelTests { @Test( "buttonCompleted returns correct values when showCompleted is true", - arguments: TodoGroup.allCases, [true, false] + arguments: [TodoGroup.today, .upcoming, .overdue, .completed], [true, false] ) func buttonCompleted( for group: TodoGroup, @@ -54,7 +55,7 @@ struct TaskListViewModelTests { @Test( "canShowCompleted returns correct value for each group", arguments: zip( - TodoGroup.allCases, + [TodoGroup.today, .upcoming, .overdue, .completed], [true, true, true, false] ) ) @@ -74,7 +75,7 @@ struct TaskListViewModelTests { @Test( "navigationTitle returns correct view title for each group", - arguments: TodoGroup.allCases + arguments: [TodoGroup.today, .upcoming, .overdue, .completed] ) func navigationTitle(for group: TodoGroup) { // GIVEN -- 2.54.0