diff --git a/Core/Entities/AddPersonParameters.swift b/Core/Entities/AddPersonParameters.swift index 75cdebe..46ef62f 100644 --- a/Core/Entities/AddPersonParameters.swift +++ b/Core/Entities/AddPersonParameters.swift @@ -8,7 +8,7 @@ import Foundation -public struct AddPersonParameters: Equatable { +public struct AddPersonParameters: Equatable, Sendable { public var name: String public var dayOfBirth: Date public var timeOfBirth: Date diff --git a/Core/Entities/Person.swift b/Core/Entities/Person.swift index dbb3c70..f618e17 100644 --- a/Core/Entities/Person.swift +++ b/Core/Entities/Person.swift @@ -8,7 +8,7 @@ import Foundation -public struct Person: Equatable, Hashable { +public struct Person: Equatable, Hashable, Sendable { public var id: UUID public var name: String diff --git a/Core/EntityGateway/Cache/CachePersonsGateway.swift b/Core/EntityGateway/Cache/CachePersonsGateway.swift index b47aa63..017e015 100644 --- a/Core/EntityGateway/Cache/CachePersonsGateway.swift +++ b/Core/EntityGateway/Cache/CachePersonsGateway.swift @@ -18,18 +18,6 @@ public final class CachePersonsGateway: PersonsGateway { self.imageStore = imageStore } - public func add(parameters: AddPersonParameters, completionHandler: @escaping AddPersonEntityGatewayCompletionHandler) { - Task { - do { - await complete(.success(try await add(parameters: parameters)), using: completionHandler) - } catch let error as CoreError { - await complete(.failure(error), using: completionHandler) - } catch { - await complete(.failure(CoreError(error: error)), using: completionHandler) - } - } - } - public func add(parameters: AddPersonParameters) async throws -> Person { try Task.checkCancellation() let savedImages = try await save(newImages(in: parameters)) @@ -42,37 +30,14 @@ public final class CachePersonsGateway: PersonsGateway { } } - public func fetchPersons(completionHandler: @escaping FetchPersonsEntityGatewayCompletionHandler) { - coreDataGateway.fetchPersons(completionHandler: completionHandler) - } - public func fetchPersons() async throws -> [Person] { try await coreDataGateway.fetchPersons() } - public func fetchWidgetPersons(completion: @escaping FetchPersonsEntityGatewayCompletionHandler) { - coreDataGateway.fetchWidgetPersons(completion: completion) - } - public func fetchWidgetPersons() async throws -> [Person] { try await coreDataGateway.fetchWidgetPersons() } - public func edit( - person: Person, with parameters: AddPersonParameters, - completionHandler: @escaping EditPersonEntityGatewayCompletionHandler - ) { - Task { - do { - await complete(.success(try await edit(person: person, with: parameters)), using: completionHandler) - } catch let error as CoreError { - await complete(.failure(error), using: completionHandler) - } catch { - await complete(.failure(CoreError(error: error)), using: completionHandler) - } - } - } - public func edit(person: Person, with parameters: AddPersonParameters) async throws -> Person { try Task.checkCancellation() let savedImages = try await save(newImages(in: parameters)) @@ -87,19 +52,6 @@ public final class CachePersonsGateway: PersonsGateway { } } - public func remove(person: Person, completionHandler: @escaping RemovePersonEntityGatewayCompletionHandler) { - Task { - do { - try await remove(person: person) - await complete(.success(()), using: completionHandler) - } catch let error as CoreError { - await complete(.failure(error), using: completionHandler) - } catch { - await complete(.failure(CoreError(error: error)), using: completionHandler) - } - } - } - public func remove(person: Person) async throws { try Task.checkCancellation() try await coreDataGateway.remove(person: person) @@ -154,10 +106,4 @@ public final class CachePersonsGateway: PersonsGateway { } } } - - private func complete( - _ result: Result, using completion: @escaping (Result) -> Void - ) async { - await MainActor.run { completion(result) } - } } diff --git a/Core/EntityGateway/Cache/ImagesCache.swift b/Core/EntityGateway/Cache/ImagesCache.swift index 1f20987..f6a7b1c 100644 --- a/Core/EntityGateway/Cache/ImagesCache.swift +++ b/Core/EntityGateway/Cache/ImagesCache.swift @@ -14,26 +14,24 @@ public enum ImagesCache { public static let memoryCache = MemoryCache() - public static func loadImageFromDiskOrMemory( - image: PersonImage, completion: @escaping (Result) -> Void - ) { - DispatchQueue.global(qos: .userInitiated).async { - var result = Result.failure(.unknownError) - if let memoryImg = memoryCache.value(forKey: image.cachingKey) { - result = .success(memoryImg) - } else { - let directory = Disk.Directory.sharedContainer(appGroupName: Constants.appGroupId) - do { - let image = try Disk.retrieve(image.cachingKey, from: directory, as: UIImage.self) - result = .success(image) - } catch { - let coreError = CoreError(error: error) - result = .failure(coreError) + public static func loadImageFromDiskOrMemory(image: PersonImage) async throws -> UIImage { + try Task.checkCancellation() + let loadedImage = try await withCheckedThrowingContinuation { continuation in + DispatchQueue.global(qos: .userInitiated).async { + if let memoryImg = memoryCache.value(forKey: image.cachingKey) { + continuation.resume(returning: memoryImg) + } else { + let directory = Disk.Directory.sharedContainer(appGroupName: Constants.appGroupId) + do { + continuation.resume( + returning: try Disk.retrieve(image.cachingKey, from: directory, as: UIImage.self)) + } catch { + continuation.resume(throwing: CoreError(error: error)) + } } } - DispatchQueue.main.async { - completion(result) - } } + try Task.checkCancellation() + return loadedImage } } diff --git a/Core/EntityGateway/LocalPersistance/CoreDataPersonsGateway.swift b/Core/EntityGateway/LocalPersistance/CoreDataPersonsGateway.swift index 66aeac4..6e0fb48 100644 --- a/Core/EntityGateway/LocalPersistance/CoreDataPersonsGateway.swift +++ b/Core/EntityGateway/LocalPersistance/CoreDataPersonsGateway.swift @@ -9,9 +9,6 @@ import CoreData import Foundation -public typealias FetchedPersonsCompletionHandler = (_ persons: Result<[Person], CoreError>) -> Void -public typealias FetchedPersonCompletionHandler = (_ person: Result) -> Void - public class CoreDataPersonsGateway: PersonsGateway { let coreDataStack: CoreDataStack @@ -20,129 +17,114 @@ public class CoreDataPersonsGateway: PersonsGateway { self.coreDataStack = coreDataStack } - public func add(parameters: AddPersonParameters, completionHandler: @escaping AddPersonEntityGatewayCompletionHandler) { - coreDataStack.persistentContainer.performBackgroundTask { context in - var result: Result = .failure(CoreError.coreDataAddFailed) - if let cdPerson = context.addEntity(withType: CoreDataPerson.self) { - cdPerson.populate(with: parameters) + public func add(parameters: AddPersonParameters) async throws -> Person { + try Task.checkCancellation() + return try await withCheckedThrowingContinuation { continuation in + coreDataStack.persistentContainer.performBackgroundTask { context in + guard let person = context.addEntity(withType: CoreDataPerson.self) else { + continuation.resume(throwing: CoreError.coreDataAddFailed) + return + } do { - if parameters.isOnWidget { - cdPerson.accessToWidget = try AccessToWidget.sharedInstance(in: context) - } else { - cdPerson.accessToWidget = nil - } + person.populate(with: parameters) + person.accessToWidget = + parameters.isOnWidget + ? try AccessToWidget.sharedInstance(in: context) : nil try context.save() - result = .success(cdPerson.person) + continuation.resume(returning: person.person) } catch let error as CoreError { - context.delete(cdPerson) - result = .failure(error) + context.delete(person) + continuation.resume(throwing: error) } catch { - context.delete(cdPerson) - result = .failure(CoreError(error: error)) + context.delete(person) + continuation.resume(throwing: CoreError(error: error)) } } - DispatchQueue.main.async { - completionHandler(result) - } } } - public func fetchPersons(completionHandler: @escaping FetchPersonsEntityGatewayCompletionHandler) { - coreDataStack.persistentContainer.performBackgroundTask { _ in - var result: Result<[Person], CoreError> = .failure(.unknownError) - let fetchRequest: NSFetchRequest = CoreDataPerson.fetchRequest() - fetchRequest.sortDescriptors = [NSSortDescriptor(key: "createdDate", ascending: true)] - do { - let persons = try fetchRequest.execute().map({ $0.person }) - result = .success(persons) - } catch { - let coreError = CoreError(error: error) - result = .failure(coreError) - } - DispatchQueue.main.async { - completionHandler(result) + public func fetchPersons() async throws -> [Person] { + try Task.checkCancellation() + let persons = try await withCheckedThrowingContinuation { continuation in + coreDataStack.persistentContainer.performBackgroundTask { context in + let request: NSFetchRequest = CoreDataPerson.fetchRequest() + request.sortDescriptors = [NSSortDescriptor(key: "createdDate", ascending: true)] + do { + continuation.resume(returning: try context.fetch(request).map(\.person)) + } catch { + continuation.resume(throwing: CoreError(error: error)) + } } } + try Task.checkCancellation() + return persons } - public func fetchWidgetPersons(completion: @escaping FetchPersonsEntityGatewayCompletionHandler) { - coreDataStack.persistentContainer.performBackgroundTask { context in - var result: Result<[Person], CoreError> = .failure(.unknownError) - do { - let access = try AccessToWidget.sharedInstance(in: context) - guard let persons = access.widgetPersons else { - throw CoreError.missingValue + public func fetchWidgetPersons() async throws -> [Person] { + try Task.checkCancellation() + let persons = try await withCheckedThrowingContinuation { continuation in + coreDataStack.persistentContainer.performBackgroundTask { context in + do { + guard let persons = try AccessToWidget.sharedInstance(in: context).widgetPersons else { + throw CoreError.missingValue + } + continuation.resume(returning: persons.map(\.person).sorted()) + } catch let error as CoreError { + continuation.resume(throwing: error) + } catch { + continuation.resume(throwing: CoreError(error: error)) } - let cdPersons = Array(persons.map({ $0.person })).sorted() - result = .success(cdPersons) - } catch let error as CoreError { - result = .failure(error) - } catch { - let coreError = CoreError(error: error) - result = .failure(coreError) - } - DispatchQueue.main.async { - completion(result) } } + try Task.checkCancellation() + return persons } - public func edit( - person: Person, with parameters: AddPersonParameters, - completionHandler: @escaping EditPersonEntityGatewayCompletionHandler - ) { - coreDataStack.persistentContainer.performBackgroundTask { context in - var result: Result = .failure(CoreError.unknownError) - do { - let predicate = NSPredicate(format: "%K == %@", #keyPath(CoreDataPerson.id), person.id.uuidString) - let fetchRequest: NSFetchRequest = CoreDataPerson.fetchRequest() - fetchRequest.predicate = predicate - let coreDataPerson = try fetchRequest.execute().first - guard let cdPerson = coreDataPerson else { - throw CoreError.coreDataFetchFailed - } - cdPerson.populate(with: parameters) - if parameters.isOnWidget { - cdPerson.accessToWidget = try AccessToWidget.sharedInstance(in: context) - } else { - cdPerson.accessToWidget = nil + public func edit(person: Person, with parameters: AddPersonParameters) async throws -> Person { + try Task.checkCancellation() + return try await withCheckedThrowingContinuation { continuation in + coreDataStack.persistentContainer.performBackgroundTask { context in + do { + let request: NSFetchRequest = CoreDataPerson.fetchRequest() + request.predicate = NSPredicate( + format: "%K == %@", #keyPath(CoreDataPerson.id), person.id.uuidString) + guard let storedPerson = try context.fetch(request).first else { + throw CoreError.coreDataFetchFailed + } + storedPerson.populate(with: parameters) + storedPerson.accessToWidget = + parameters.isOnWidget + ? try AccessToWidget.sharedInstance(in: context) : nil + try context.save() + continuation.resume(returning: storedPerson.person) + } catch let error as CoreError { + continuation.resume(throwing: error) + } catch { + continuation.resume(throwing: CoreError(error: error)) } - try context.save() - result = .success(cdPerson.person) - } catch let coreError as CoreError { - result = .failure(coreError) - } catch { - let coreError = CoreError(error: error) - result = .failure(coreError) - } - DispatchQueue.main.async { - completionHandler(result) } } } - public func remove(person: Person, completionHandler: @escaping RemovePersonEntityGatewayCompletionHandler) { - coreDataStack.persistentContainer.performBackgroundTask { context in - var result: Result = .failure(.unknownError) - do { - let predicate = NSPredicate(format: "%K == %@", #keyPath(CoreDataPerson.id), person.id.uuidString) - let fetchRequest: NSFetchRequest = CoreDataPerson.fetchRequest() - fetchRequest.predicate = predicate - let coreDataPerson = try fetchRequest.execute().first - guard let cdPerson = coreDataPerson else { - throw CoreError.coreDataFetchFailed + public func remove(person: Person) async throws { + try Task.checkCancellation() + try await withCheckedThrowingContinuation { continuation in + coreDataStack.persistentContainer.performBackgroundTask { context in + do { + let request: NSFetchRequest = CoreDataPerson.fetchRequest() + request.predicate = NSPredicate( + format: "%K == %@", #keyPath(CoreDataPerson.id), person.id.uuidString) + guard let storedPerson = try context.fetch(request).first else { + throw CoreError.coreDataFetchFailed + } + context.delete(storedPerson) + try context.save() + continuation.resume(returning: ()) + } catch let error as CoreError { + continuation.resume(throwing: error) + } catch { + continuation.resume(throwing: CoreError(error: error)) } - context.delete(cdPerson) - try context.save() - result = .success(()) - } catch let error as CoreError { - result = .failure(error) - } catch { - let coreError = CoreError(error: error) - result = .failure(coreError) - } - DispatchQueue.main.async { - completionHandler(result) } } } diff --git a/Core/Gateways/PersonsGateway.swift b/Core/Gateways/PersonsGateway.swift index 8aaca96..ccbed55 100644 --- a/Core/Gateways/PersonsGateway.swift +++ b/Core/Gateways/PersonsGateway.swift @@ -6,66 +6,10 @@ // Copyright © 2019-2026 Danis Ziganshin. // -import Foundation - -public typealias AddPersonEntityGatewayCompletionHandler = (_ result: Result) -> Void -public typealias EditPersonEntityGatewayCompletionHandler = (_ result: Result) -> Void -public typealias FetchPersonsEntityGatewayCompletionHandler = (_ result: Result<[Person], CoreError>) -> Void -public typealias RemovePersonEntityGatewayCompletionHandler = (_ result: Result) -> Void - public protocol PersonsGateway { - func add(parameters: AddPersonParameters, completionHandler: @escaping AddPersonEntityGatewayCompletionHandler) func add(parameters: AddPersonParameters) async throws -> Person - func fetchPersons(completionHandler: @escaping FetchPersonsEntityGatewayCompletionHandler) func fetchPersons() async throws -> [Person] - func edit( - person: Person, with parameters: AddPersonParameters, - completionHandler: @escaping EditPersonEntityGatewayCompletionHandler) func edit(person: Person, with parameters: AddPersonParameters) async throws -> Person - func remove(person: Person, completionHandler: @escaping RemovePersonEntityGatewayCompletionHandler) func remove(person: Person) async throws - func fetchWidgetPersons(completion: @escaping FetchPersonsEntityGatewayCompletionHandler) func fetchWidgetPersons() async throws -> [Person] } - -public extension PersonsGateway { - - func add(parameters: AddPersonParameters) async throws -> Person { - try Task.checkCancellation() - return try await withCheckedThrowingContinuation { continuation in - add(parameters: parameters) { continuation.resume(with: $0) } - } - } - - func fetchPersons() async throws -> [Person] { - try Task.checkCancellation() - let persons = try await withCheckedThrowingContinuation { continuation in - fetchPersons { continuation.resume(with: $0) } - } - try Task.checkCancellation() - return persons - } - - func edit(person: Person, with parameters: AddPersonParameters) async throws -> Person { - try Task.checkCancellation() - return try await withCheckedThrowingContinuation { continuation in - edit(person: person, with: parameters) { continuation.resume(with: $0) } - } - } - - func remove(person: Person) async throws { - try Task.checkCancellation() - try await withCheckedThrowingContinuation { continuation in - remove(person: person) { continuation.resume(with: $0) } - } - } - - func fetchWidgetPersons() async throws -> [Person] { - try Task.checkCancellation() - let persons = try await withCheckedThrowingContinuation { continuation in - fetchWidgetPersons { continuation.resume(with: $0) } - } - try Task.checkCancellation() - return persons - } -} diff --git a/Core/UseCases/AddPersonUseCase.swift b/Core/UseCases/AddPersonUseCase.swift index 99a7f13..d65da47 100644 --- a/Core/UseCases/AddPersonUseCase.swift +++ b/Core/UseCases/AddPersonUseCase.swift @@ -6,12 +6,7 @@ // Copyright © 2019-2026 Danis Ziganshin. // -import Foundation - -public typealias AddPersonUseCaseCompletionHandler = (_ person: Result) -> Void - public protocol AddPersonUseCase { - func add(parameters: AddPersonParameters, completionHandler: @escaping AddPersonUseCaseCompletionHandler) func add(parameters: AddPersonParameters) async throws -> Person } @@ -22,14 +17,7 @@ public final class AddPersonUseCaseImplementation: AddPersonUseCase { self.personsGateway = personsGateway } - public func add(parameters: AddPersonParameters, completionHandler: @escaping (Result) -> Void) { - personsGateway.add(parameters: parameters) { (result) in - completionHandler(result) - } - } - public func add(parameters: AddPersonParameters) async throws -> Person { try await personsGateway.add(parameters: parameters) } - } diff --git a/Core/UseCases/EditPersonUseCase.swift b/Core/UseCases/EditPersonUseCase.swift index 8df0c99..37115b0 100644 --- a/Core/UseCases/EditPersonUseCase.swift +++ b/Core/UseCases/EditPersonUseCase.swift @@ -6,14 +6,7 @@ // Copyright © 2019-2026 Danis Ziganshin. // -import Foundation - -public typealias EditPersonUseCaseCompletionHandler = (_ person: Result) -> Void - public protocol EditPersonUseCase { - func edit( - person: Person, with parameters: AddPersonParameters, - completionHandler: @escaping EditPersonUseCaseCompletionHandler) func edit(person: Person, with parameters: AddPersonParameters) async throws -> Person } @@ -24,17 +17,7 @@ public final class EditPersonUseCaseImplementation: EditPersonUseCase { self.personsGateway = personsGateway } - public func edit( - person: Person, with parameters: AddPersonParameters, - completionHandler: @escaping (Result) -> Void - ) { - personsGateway.edit(person: person, with: parameters) { (result) in - completionHandler(result) - } - } - public func edit(person: Person, with parameters: AddPersonParameters) async throws -> Person { try await personsGateway.edit(person: person, with: parameters) } - } diff --git a/Core/UseCases/FetchPersonsUseCase.swift b/Core/UseCases/FetchPersonsUseCase.swift index 034bdaf..c8fa5a2 100644 --- a/Core/UseCases/FetchPersonsUseCase.swift +++ b/Core/UseCases/FetchPersonsUseCase.swift @@ -6,14 +6,8 @@ // Copyright © 2019-2026 Danis Ziganshin. // -import Foundation - -public typealias FetchPersonsUseCaseCompletionHandler = (_ persons: Result<[Person], CoreError>) -> Void - public protocol FetchPersonsUseCase { - func fetchPersons(completionHandler: @escaping FetchPersonsUseCaseCompletionHandler) func fetchPersons() async throws -> [Person] - func fetchWidgetPersons(completion: @escaping FetchPersonsUseCaseCompletionHandler) func fetchWidgetPersons() async throws -> [Person] } @@ -24,18 +18,6 @@ public final class FetchPersonsUseCaseImplementation: FetchPersonsUseCase { self.personsGateway = personsGateway } - // MARK: - DisplayPersonsUseCase - - public func fetchPersons(completionHandler: @escaping (Result<[Person], CoreError>) -> Void) { - self.personsGateway.fetchPersons { (result) in - completionHandler(result) - } - } - - public func fetchWidgetPersons(completion: @escaping FetchPersonsUseCaseCompletionHandler) { - personsGateway.fetchWidgetPersons(completion: completion) - } - public func fetchPersons() async throws -> [Person] { try await personsGateway.fetchPersons() } diff --git a/Core/UseCases/RemovePersonUseCase.swift b/Core/UseCases/RemovePersonUseCase.swift index d7ca3ea..a94ad89 100644 --- a/Core/UseCases/RemovePersonUseCase.swift +++ b/Core/UseCases/RemovePersonUseCase.swift @@ -6,12 +6,7 @@ // Copyright © 2019-2026 Danis Ziganshin. // -import Foundation - -public typealias RemovePersonUseCaseCompletionHandler = (_ result: Result) -> Void - public protocol RemovePersonUseCase { - func remove(person: Person, completionHandler: @escaping RemovePersonUseCaseCompletionHandler) func remove(person: Person) async throws } @@ -22,10 +17,6 @@ public final class RemovePersonUseCaseImplementation: RemovePersonUseCase { self.personsGateway = personsGateway } - public func remove(person: Person, completionHandler: @escaping RemovePersonUseCaseCompletionHandler) { - personsGateway.remove(person: person, completionHandler: completionHandler) - } - public func remove(person: Person) async throws { try await personsGateway.remove(person: person) } diff --git a/GrowingUp.xcodeproj/project.pbxproj b/GrowingUp.xcodeproj/project.pbxproj index 3970e1a..f88e439 100644 --- a/GrowingUp.xcodeproj/project.pbxproj +++ b/GrowingUp.xcodeproj/project.pbxproj @@ -1127,6 +1127,7 @@ /* Begin PBXShellScriptBuildPhase section */ 09AC10CD22457FB500AC12D5 /* Swift Lint */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -1141,7 +1142,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; + shellScript = "export PATH=\"/opt/homebrew/bin:/usr/local/bin:$PATH\"\nif command -v swiftlint >/dev/null 2>&1; then\n \"$SRCROOT/scripts/lint-swift.sh\"\nelse\n echo \"note: SwiftLint is unavailable; lint is enforced by the dedicated CI job\"\nfi\n"; }; /* End PBXShellScriptBuildPhase section */ diff --git a/GrowingUp/Scenes/EditPerson/Cells/DateTableViewCell/DateTableViewCell.swift b/GrowingUp/Scenes/EditPerson/Cells/DateTableViewCell/DateTableViewCell.swift index 9d39e50..890de15 100644 --- a/GrowingUp/Scenes/EditPerson/Cells/DateTableViewCell/DateTableViewCell.swift +++ b/GrowingUp/Scenes/EditPerson/Cells/DateTableViewCell/DateTableViewCell.swift @@ -14,6 +14,7 @@ protocol DateCellView: AnyObject { func setup(with delegate: DateCellDelegate?, forRow row: Int) } +@MainActor protocol DateCellDelegate: AnyObject { func dateCell(_ cell: DateCellView, didChangeBirthdayTo date: Date) } diff --git a/GrowingUp/Scenes/EditPerson/Cells/ImagePickersTableViewCell/ImagesTableViewCell.swift b/GrowingUp/Scenes/EditPerson/Cells/ImagePickersTableViewCell/ImagesTableViewCell.swift index 52985ba..ed27d20 100644 --- a/GrowingUp/Scenes/EditPerson/Cells/ImagePickersTableViewCell/ImagesTableViewCell.swift +++ b/GrowingUp/Scenes/EditPerson/Cells/ImagePickersTableViewCell/ImagesTableViewCell.swift @@ -33,6 +33,8 @@ final class ImagesTableViewCell: UITableViewCell, ImagesCellView { private let widgetPicRemoveBadge = ImagesTableViewCell.makeRemoveBadge() private let appPicMenuButton = ImagesTableViewCell.makeSourceMenuButton() private let widgetPicMenuButton = ImagesTableViewCell.makeSourceMenuButton() + private var appPicRequestID = UUID() + private var widgetPicRequestID = UUID() override func awakeFromNib() { super.awakeFromNib() @@ -110,6 +112,8 @@ final class ImagesTableViewCell: UITableViewCell, ImagesCellView { // MARK: - ImagesCellView func display(appPic: PersonImage?) { + appPicRequestID = UUID() + let requestID = appPicRequestID appPicRemoveBadge.isHidden = (appPic == nil) guard let appPic = appPic else { appPicButton.drawImage(nil) @@ -118,18 +122,21 @@ final class ImagesTableViewCell: UITableViewCell, ImagesCellView { if let uiImage = appPic.uiImage { appPicButton.drawImage(uiImage) } else { - ImagesCache.loadImageFromDiskOrMemory(image: appPic) { result in - switch result { - case .success(let img): - self.appPicButton.drawImage(img) - case .failure(let error): - Logging.logError(error) + Task { @MainActor [weak self] in + do { + let image = try await ImagesCache.loadImageFromDiskOrMemory(image: appPic) + guard self?.appPicRequestID == requestID else { return } + self?.appPicButton.drawImage(image) + } catch { + Logging.logError(CoreError(error: error)) } } } } func display(widgetPic: PersonImage?) { + widgetPicRequestID = UUID() + let requestID = widgetPicRequestID widgetPicRemoveBadge.isHidden = (widgetPic == nil) guard let widgetPic = widgetPic else { widgetPicButton.drawImage(nil) @@ -138,12 +145,13 @@ final class ImagesTableViewCell: UITableViewCell, ImagesCellView { if let uiImage = widgetPic.uiImage { widgetPicButton.drawImage(uiImage) } else { - ImagesCache.loadImageFromDiskOrMemory(image: widgetPic) { result in - switch result { - case .success(let img): - self.widgetPicButton.drawImage(img) - case .failure(let error): - Logging.logError(error) + Task { @MainActor [weak self] in + do { + let image = try await ImagesCache.loadImageFromDiskOrMemory(image: widgetPic) + guard self?.widgetPicRequestID == requestID else { return } + self?.widgetPicButton.drawImage(image) + } catch { + Logging.logError(CoreError(error: error)) } } } diff --git a/GrowingUp/Scenes/EditPerson/Cells/TextFieldTableViewCell/TextFieldTableViewCell.swift b/GrowingUp/Scenes/EditPerson/Cells/TextFieldTableViewCell/TextFieldTableViewCell.swift index 51e2892..25652f6 100644 --- a/GrowingUp/Scenes/EditPerson/Cells/TextFieldTableViewCell/TextFieldTableViewCell.swift +++ b/GrowingUp/Scenes/EditPerson/Cells/TextFieldTableViewCell/TextFieldTableViewCell.swift @@ -15,6 +15,7 @@ protocol TextFieldCellView: AnyObject { func setup(with presenter: TextFieldCellPresenter, observer: TextFieldObserver?, forRow row: Int) } +@MainActor protocol TextFieldObserver: AnyObject { func textDidChange(forView: TextFieldCellView, text: String) } diff --git a/GrowingUp/Scenes/EditPerson/Cells/ToggleTableViewCell/ToggleTableViewCell.swift b/GrowingUp/Scenes/EditPerson/Cells/ToggleTableViewCell/ToggleTableViewCell.swift index ec31b58..38a8d2d 100644 --- a/GrowingUp/Scenes/EditPerson/Cells/ToggleTableViewCell/ToggleTableViewCell.swift +++ b/GrowingUp/Scenes/EditPerson/Cells/ToggleTableViewCell/ToggleTableViewCell.swift @@ -14,6 +14,7 @@ protocol ToggleCellView { func setup(with delegate: ToggleCellDelegate?, forRow row: Int) } +@MainActor protocol ToggleCellDelegate: AnyObject { func toggle(toggle: ToggleCellView, didChangeStateForRow row: Int, to state: Bool) } diff --git a/GrowingUp/Scenes/EditPerson/Create/AddPersonConfigurator.swift b/GrowingUp/Scenes/EditPerson/Create/AddPersonConfigurator.swift index 50f6918..a8a8bb6 100644 --- a/GrowingUp/Scenes/EditPerson/Create/AddPersonConfigurator.swift +++ b/GrowingUp/Scenes/EditPerson/Create/AddPersonConfigurator.swift @@ -9,6 +9,7 @@ import Core import Foundation +@MainActor class AddPersonConfigurator: EditPersonConfigurator { private weak var editPersonPresenterDelegate: EditPersonPresenterDelegate? diff --git a/GrowingUp/Scenes/EditPerson/Create/AddPersonPresenter.swift b/GrowingUp/Scenes/EditPerson/Create/AddPersonPresenter.swift index 017c212..a127d58 100644 --- a/GrowingUp/Scenes/EditPerson/Create/AddPersonPresenter.swift +++ b/GrowingUp/Scenes/EditPerson/Create/AddPersonPresenter.swift @@ -9,6 +9,7 @@ import Core import Foundation +@MainActor final class AddPersonPresenter: EditPersonPresenter { private weak var view: EditPersonView? @@ -62,16 +63,16 @@ final class AddPersonPresenter: EditPersonPresenter { } private func configureInitialStateForToggle() { - fetchWidgetPersonsUseCase.fetchWidgetPersons { result in - switch result { - case .success(let favs): - let maxReached = favs.count >= 3 - self.toggleCellPresenter.valueFor(row: EPC.addToWidgetRow, didChangeTo: !maxReached) - case .failure(let error): - self.toggleCellPresenter.valueFor(row: EPC.addToWidgetRow, didChangeTo: false) - Logging.logError(error) + Task { @MainActor [weak self] in + guard let self else { return } + do { + let maxReached = try await fetchWidgetPersonsUseCase.fetchWidgetPersons().count >= 3 + toggleCellPresenter.valueFor(row: EPC.addToWidgetRow, didChangeTo: !maxReached) + } catch { + toggleCellPresenter.valueFor(row: EPC.addToWidgetRow, didChangeTo: false) + Logging.logError(CoreError(error: error)) } - self.view?.reloadData() + view?.reloadData() } } @@ -85,13 +86,13 @@ final class AddPersonPresenter: EditPersonPresenter { guard let parameters = params else { return } updateNavigationItemsState(isEnabled: false) - addPersonUseCase.add(parameters: parameters) { result in - self.updateNavigationItemsState(isEnabled: true) - switch result { - case let .success(person): - self.handlePersonAdded(person) - case let .failure(error): - self.handleAddPersonError(error) + Task { @MainActor [weak self] in + guard let self else { return } + defer { updateNavigationItemsState(isEnabled: true) } + do { + handlePersonAdded(try await addPersonUseCase.add(parameters: parameters)) + } catch { + handleAddPersonError(error) } } } diff --git a/GrowingUp/Scenes/EditPerson/Edit/EditPersonConfigurator.swift b/GrowingUp/Scenes/EditPerson/Edit/EditPersonConfigurator.swift index 6eb0322..9104f1b 100644 --- a/GrowingUp/Scenes/EditPerson/Edit/EditPersonConfigurator.swift +++ b/GrowingUp/Scenes/EditPerson/Edit/EditPersonConfigurator.swift @@ -9,10 +9,12 @@ import Core import Foundation +@MainActor protocol EditPersonConfigurator { func configure(editPersonViewController: EditPersonViewController) } +@MainActor class EditPersonConfiguratorImplementation: EditPersonConfigurator { private let person: Person diff --git a/GrowingUp/Scenes/EditPerson/Edit/EditPersonPresenter.swift b/GrowingUp/Scenes/EditPerson/Edit/EditPersonPresenter.swift index fd594f9..869c10f 100644 --- a/GrowingUp/Scenes/EditPerson/Edit/EditPersonPresenter.swift +++ b/GrowingUp/Scenes/EditPerson/Edit/EditPersonPresenter.swift @@ -9,6 +9,7 @@ import Core import Foundation +@MainActor protocol EditPersonPresenter: TextFieldObserver, ToggleCellDelegate, DateCellDelegate { var router: EditPersonViewRouter { get } func viewDidLoad() @@ -28,6 +29,7 @@ protocol EditPersonPresenter: TextFieldObserver, ToggleCellDelegate, DateCellDel func shouldShowRemoveButton() -> Bool } +@MainActor protocol EditPersonPresenterDelegate: AnyObject { func editPersonPresenter(_ presenter: EditPersonPresenter, didAdd person: Person) func editPersonPresenter(_ presenter: EditPersonPresenter, didEdit person: Person) @@ -35,6 +37,7 @@ protocol EditPersonPresenterDelegate: AnyObject { func editPersonPresenterCancel(presenter: EditPersonPresenter) } +@MainActor final class EditPersonPresenterImplementation: EditPersonPresenter { private let person: Person @@ -104,13 +107,13 @@ final class EditPersonPresenterImplementation: EditPersonPresenter { guard let parameters = params else { return } updateNavigationItemsState(isEnabled: false) - editPersonUseCase.edit(person: person, with: parameters) { result in - self.updateNavigationItemsState(isEnabled: true) - switch result { - case let .success(person): - self.handlePersonEdited(person) - case let .failure(error): - self.handleEditPersonError(error) + Task { @MainActor [weak self] in + guard let self else { return } + defer { updateNavigationItemsState(isEnabled: true) } + do { + handlePersonEdited(try await editPersonUseCase.edit(person: person, with: parameters)) + } catch { + handleEditPersonError(error) } } } @@ -121,13 +124,14 @@ final class EditPersonPresenterImplementation: EditPersonPresenter { func removePersonPressed() { updateNavigationItemsState(isEnabled: false) - removePersonUseCase.remove(person: person) { result in - self.updateNavigationItemsState(isEnabled: true) - switch result { - case .success: - self.handlePersonRemoved() - case .failure(let error): - self.handleEditPersonError(error) + Task { @MainActor [weak self] in + guard let self else { return } + defer { updateNavigationItemsState(isEnabled: true) } + do { + try await removePersonUseCase.remove(person: person) + handlePersonRemoved() + } catch { + handleEditPersonError(error) } } } diff --git a/GrowingUp/Scenes/EmptyPerson/EmptyPersonConfigurator.swift b/GrowingUp/Scenes/EmptyPerson/EmptyPersonConfigurator.swift index bf40ac1..6c8b904 100644 --- a/GrowingUp/Scenes/EmptyPerson/EmptyPersonConfigurator.swift +++ b/GrowingUp/Scenes/EmptyPerson/EmptyPersonConfigurator.swift @@ -8,10 +8,12 @@ import Foundation +@MainActor protocol EmptyPersonConfigurator { func configure(emptyPersonController: EmptyPersonViewController) } +@MainActor final class EmptyPersonConfiguratorImplementation: EmptyPersonConfigurator { private let index: Int diff --git a/GrowingUp/Scenes/EmptyPerson/EmptyPersonPresenter.swift b/GrowingUp/Scenes/EmptyPerson/EmptyPersonPresenter.swift index accc2c2..0c39020 100644 --- a/GrowingUp/Scenes/EmptyPerson/EmptyPersonPresenter.swift +++ b/GrowingUp/Scenes/EmptyPerson/EmptyPersonPresenter.swift @@ -8,10 +8,12 @@ import Foundation +@MainActor protocol EmptyPersonPresenter { func addButtonPressed() } +@MainActor final class EmptyPersonPresenterImplementation: EmptyPersonPresenter { private let router: EmptyPersonViewRouter diff --git a/GrowingUp/Scenes/EmptyPerson/EmptyPersonViewRouter.swift b/GrowingUp/Scenes/EmptyPerson/EmptyPersonViewRouter.swift index 9b48d52..370163f 100644 --- a/GrowingUp/Scenes/EmptyPerson/EmptyPersonViewRouter.swift +++ b/GrowingUp/Scenes/EmptyPerson/EmptyPersonViewRouter.swift @@ -8,10 +8,12 @@ import UIKit +@MainActor protocol EmptyPersonViewRouter { func presentAddPerson(addPersonPresenterDelegate: EditPersonPresenterDelegate?) } +@MainActor final class EmptyPersonViewRouterImplementation: EmptyPersonViewRouter { private weak var emptyPersonViewController: EmptyPersonViewController? diff --git a/GrowingUp/Scenes/PersonOverview/PersonOverviewConfigurator.swift b/GrowingUp/Scenes/PersonOverview/PersonOverviewConfigurator.swift index cd5bc8b..f9e8170 100644 --- a/GrowingUp/Scenes/PersonOverview/PersonOverviewConfigurator.swift +++ b/GrowingUp/Scenes/PersonOverview/PersonOverviewConfigurator.swift @@ -9,10 +9,12 @@ import Core import Foundation +@MainActor protocol PersonOverviewConfigurator { func configure(personOverviewController: PersonOverviewViewController) } +@MainActor final class PersonOverviewConfiguratorImplementation: PersonOverviewConfigurator { let index: Int diff --git a/GrowingUp/Scenes/PersonOverview/PersonOverviewPresenter.swift b/GrowingUp/Scenes/PersonOverview/PersonOverviewPresenter.swift index bf730ad..abfea4b 100644 --- a/GrowingUp/Scenes/PersonOverview/PersonOverviewPresenter.swift +++ b/GrowingUp/Scenes/PersonOverview/PersonOverviewPresenter.swift @@ -9,11 +9,13 @@ import Core import Foundation +@MainActor protocol PersonOverviewPresenter { func loadPerson() func showEditPerson() } +@MainActor final class PersonOverviewPresenterImplementation: PersonOverviewPresenter { weak var view: PersonOverviewView? @@ -28,6 +30,10 @@ final class PersonOverviewPresenterImplementation: PersonOverviewPresenter { self.router = router } + deinit { + timer?.invalidate() + } + func loadPerson() { view?.displayPersonName(name: person.name) let personAppPic = PersonImage(id: person.appPicId) @@ -43,8 +49,10 @@ final class PersonOverviewPresenterImplementation: PersonOverviewPresenter { private func scheduleAgeTicker() { timer?.invalidate() showAge() - timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in - self.showAge() + timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in + Task { @MainActor in + self?.showAge() + } } } diff --git a/GrowingUp/Scenes/PersonOverview/PersonOverviewRouter.swift b/GrowingUp/Scenes/PersonOverview/PersonOverviewRouter.swift index c2882af..1cee8ac 100644 --- a/GrowingUp/Scenes/PersonOverview/PersonOverviewRouter.swift +++ b/GrowingUp/Scenes/PersonOverview/PersonOverviewRouter.swift @@ -10,10 +10,12 @@ import Core import Foundation import UIKit +@MainActor protocol PersonOverviewRouter { func showEdit(for person: Person, presenterDelegate: EditPersonPresenterDelegate?) } +@MainActor final class PersonOverviewRouterImplementation: PersonOverviewRouter { private weak var personOverviewViewController: PersonOverviewViewController? diff --git a/GrowingUp/Scenes/PersonOverview/PersonOverviewViewController.swift b/GrowingUp/Scenes/PersonOverview/PersonOverviewViewController.swift index 9f7b196..c4f896c 100644 --- a/GrowingUp/Scenes/PersonOverview/PersonOverviewViewController.swift +++ b/GrowingUp/Scenes/PersonOverview/PersonOverviewViewController.swift @@ -81,12 +81,11 @@ final class PersonOverviewViewController: UIViewController, PersonOverviewView { noPicPlaceholder.isHidden = image != nil darkHoverView.alpha = image != nil ? 0.15 : 0.5 guard let image = image else { return } - ImagesCache.loadImageFromDiskOrMemory(image: image) { result in - switch result { - case .success(let img): - self.appImage.image = img - case .failure(let error): - Logging.logError(error) + Task { @MainActor [weak self] in + do { + self?.appImage.image = try await ImagesCache.loadImageFromDiskOrMemory(image: image) + } catch { + Logging.logError(CoreError(error: error)) } } } diff --git a/GrowingUp/Scenes/PersonsList/PersonsListConfigurator.swift b/GrowingUp/Scenes/PersonsList/PersonsListConfigurator.swift index 7e68c20..8a5f5fa 100644 --- a/GrowingUp/Scenes/PersonsList/PersonsListConfigurator.swift +++ b/GrowingUp/Scenes/PersonsList/PersonsListConfigurator.swift @@ -9,10 +9,12 @@ import Core import Foundation +@MainActor protocol PersonsListConfigurator { func configure(personsListController: PersonsListViewController) } +@MainActor final class PersonsListConfiguratorImplementation: PersonsListConfigurator { func configure(personsListController: PersonsListViewController) { diff --git a/GrowingUp/Scenes/PersonsList/PersonsListPresenter.swift b/GrowingUp/Scenes/PersonsList/PersonsListPresenter.swift index 1ec935f..de61cbf 100644 --- a/GrowingUp/Scenes/PersonsList/PersonsListPresenter.swift +++ b/GrowingUp/Scenes/PersonsList/PersonsListPresenter.swift @@ -12,12 +12,14 @@ import WidgetKit private let maxNumberOfPersons = 20 +@MainActor protocol PersonsListPresenter { func pageViewControllerScreen(atIndex index: Int) -> PageViewControllerViewable? func numberOfPages() -> Int func emptyPageIndex() -> Int } +@MainActor final class PersonsListPresenterImplementation: PersonsListPresenter { private var cachedScreens = [Int: PageViewControllerViewable]() private var persons = [Person]() @@ -37,13 +39,15 @@ final class PersonsListPresenterImplementation: PersonsListPresenter { } private func loadListOfPersons() { - fetchPersonsUseCase.fetchPersons { result in - switch result { - case .success(let value): self.persons = value - case .failure(let error): Logging.logError(error) + Task { @MainActor [weak self] in + guard let self else { return } + do { + persons = try await fetchPersonsUseCase.fetchPersons() + } catch { + Logging.logError(CoreError(error: error)) } - self.cachedScreens.removeAll() - self.view?.updateListOfScreens(defaultPage: self.defaultPage) + cachedScreens.removeAll() + view?.updateListOfScreens(defaultPage: defaultPage) } } diff --git a/GrowingUp/Views/VerticalButton.swift b/GrowingUp/Views/VerticalButton.swift index 3f47a6b..fc539ad 100644 --- a/GrowingUp/Views/VerticalButton.swift +++ b/GrowingUp/Views/VerticalButton.swift @@ -27,35 +27,9 @@ class VerticalButton: UIButton { } private func sharedInit() { - centerVertically() - } - - override var intrinsicContentSize: CGSize { - return verticalAlignedIntrinsicContentSize() - } - - private func centerVertically(padding: CGFloat = 6) { - let imageSize = imageView?.frame.size ?? .zero - let titleSize = titleLabel?.frame.size ?? .zero - - let totalHeight = imageSize.height + titleSize.height + padding - let imageTop = -(totalHeight - imageSize.height) - imageEdgeInsets = UIEdgeInsets(top: imageTop, left: 0, bottom: 0, right: -titleSize.width) - - let titleBottom = -(totalHeight - titleSize.height) - titleEdgeInsets = UIEdgeInsets(top: 0, left: -imageSize.width, bottom: titleBottom, right: 0) - } - - private func verticalAlignedIntrinsicContentSize(padding: CGFloat = 6) -> CGSize { - let maxSize = CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude) - - if let titleSize = titleLabel?.sizeThatFits(maxSize), let imageSize = imageView?.sizeThatFits(maxSize) { - let width = ceil(max(imageSize.width, titleSize.width)) - let height = ceil(imageSize.height + titleSize.height + padding) - - return CGSize(width: width, height: height) - } - - return super.intrinsicContentSize + var configuration = configuration ?? .plain() + configuration.imagePlacement = .top + configuration.imagePadding = 6 + self.configuration = configuration } } diff --git a/GrowingUpTests/Gateways/CachePersonsGatewayTests.swift b/GrowingUpTests/Gateways/CachePersonsGatewayTests.swift index 66cca94..cef8067 100644 --- a/GrowingUpTests/Gateways/CachePersonsGatewayTests.swift +++ b/GrowingUpTests/Gateways/CachePersonsGatewayTests.swift @@ -21,42 +21,33 @@ final class CachePersonsGatewayTests: XCTestCase { sut = CachePersonsGateway(coreDataGateway: coreDataGatewaySpy, imageStore: imageStoreSpy) } - func testAddSavesImagesBeforeCoreData() { + func testAddSavesImagesBeforeCoreData() async throws { let parameters = parameters(appImage: PersonImage(uiImage: UIImage()), widgetImage: PersonImage(uiImage: UIImage())) let expectedPerson = Person.createPerson() coreDataGatewaySpy.addPersonResultToBeReturned = .success(expectedPerson) var events = [String]() imageStoreSpy.onSave = { events.append("save") } coreDataGatewaySpy.onAdd = { events.append("database") } - let completed = expectation(description: "add completes") - sut.add(parameters: parameters) { result in - XCTAssertEqual(result, .success(expectedPerson)) - XCTAssertEqual(events, ["save", "save", "database"]) - completed.fulfill() - } - - waitForExpectations(timeout: 1) + let person = try await sut.add(parameters: parameters) + XCTAssertEqual(person, expectedPerson) + XCTAssertEqual(events, ["save", "save", "database"]) } - func testAddRollsBackPartialImageSaveAndSkipsCoreData() { + func testAddRollsBackPartialImageSaveAndSkipsCoreData() async { let parameters = parameters(appImage: PersonImage(uiImage: UIImage()), widgetImage: PersonImage(uiImage: UIImage())) imageStoreSpy.saveErrorAtCall = 2 - let completed = expectation(description: "add fails") - - sut.add(parameters: parameters) { result in - guard case .failure = result else { - return XCTFail("Expected image save failure") - } - XCTAssertFalse(self.coreDataGatewaySpy.addPersonCalled) - XCTAssertEqual(self.imageStoreSpy.deletedImages.map(\.id), self.imageStoreSpy.savedImages.map(\.id)) - completed.fulfill() - } - waitForExpectations(timeout: 1) + do { + _ = try await sut.add(parameters: parameters) + XCTFail("Expected image save failure") + } catch { + XCTAssertFalse(coreDataGatewaySpy.addPersonCalled) + XCTAssertEqual(imageStoreSpy.deletedImages.map(\.id), imageStoreSpy.savedImages.map(\.id)) + } } - func testEditCommitsNewImageBeforeDeletingReplacedImage() { + func testEditCommitsNewImageBeforeDeletingReplacedImage() async throws { let oldAppID = UUID() let person = person(appPicID: oldAppID, widgetPicID: nil) let newAppImage = PersonImage(uiImage: UIImage()) @@ -66,55 +57,38 @@ final class CachePersonsGatewayTests: XCTestCase { imageStoreSpy.onSave = { events.append("save") } coreDataGatewaySpy.onEdit = { events.append("database") } imageStoreSpy.onDelete = { events.append("delete") } - let completed = expectation(description: "edit completes") - sut.edit(person: person, with: parameters) { _ in - XCTAssertEqual(events, ["save", "database", "delete"]) - XCTAssertEqual(self.imageStoreSpy.deletedImages.map(\.id), [oldAppID]) - completed.fulfill() - } - - waitForExpectations(timeout: 1) + _ = try await sut.edit(person: person, with: parameters) + XCTAssertEqual(events, ["save", "database", "delete"]) + XCTAssertEqual(imageStoreSpy.deletedImages.map(\.id), [oldAppID]) } - func testEditRollsBackNewImageWhenCoreDataFails() { + func testEditRollsBackNewImageWhenCoreDataFails() async { let oldAppID = UUID() let person = person(appPicID: oldAppID, widgetPicID: nil) let newAppImage = PersonImage(uiImage: UIImage()) coreDataGatewaySpy.editPersonResultToBeReturned = .failure(.coreDataSaveFailed) - let completed = expectation(description: "edit fails") - sut.edit(person: person, with: parameters(appImage: newAppImage, widgetImage: nil)) { result in - XCTAssertEqual(result, .failure(.coreDataSaveFailed)) - XCTAssertEqual(self.imageStoreSpy.deletedImages.map(\.id), [newAppImage.id]) - XCTAssertFalse(self.imageStoreSpy.deletedImages.map(\.id).contains(oldAppID)) - completed.fulfill() + do { + _ = try await sut.edit(person: person, with: parameters(appImage: newAppImage, widgetImage: nil)) + XCTFail("Expected Core Data failure") + } catch { + XCTAssertEqual(error as? CoreError, .coreDataSaveFailed) + XCTAssertEqual(imageStoreSpy.deletedImages.map(\.id), [newAppImage.id]) + XCTAssertFalse(imageStoreSpy.deletedImages.map(\.id).contains(oldAppID)) } - - waitForExpectations(timeout: 1) } - func testRemoveReportsCoreDataSuccessAfterBestEffortImageCleanup() async { + func testRemoveReportsCoreDataSuccessAfterBestEffortImageCleanup() async throws { let person = Person.createPerson() coreDataGatewaySpy.removePersonResultToBeReturned = .success(()) imageStoreSpy.deleteError = CoreError.unknownError var events = [String]() coreDataGatewaySpy.onRemove = { events.append("database") } imageStoreSpy.onDelete = { events.append("delete") } - var completionCount = 0 - let completed = expectation(description: "remove completes") - - sut.remove(person: person) { result in - completionCount += 1 - if case .failure(let error) = result { - XCTFail("Expected success, got \(error)") - } - XCTAssertEqual(events, ["database", "delete", "delete"]) - completed.fulfill() - } - await fulfillment(of: [completed], timeout: 1) - XCTAssertEqual(completionCount, 1) + try await sut.remove(person: person) + XCTAssertEqual(events, ["database", "delete", "delete"]) } func testAsyncAddHonorsCancellationBeforeSideEffects() async { diff --git a/GrowingUpTests/Gateways/CoreDataPersonsGatewayTests.swift b/GrowingUpTests/Gateways/CoreDataPersonsGatewayTests.swift index cd01eab..3bb27f2 100644 --- a/GrowingUpTests/Gateways/CoreDataPersonsGatewayTests.swift +++ b/GrowingUpTests/Gateways/CoreDataPersonsGatewayTests.swift @@ -20,24 +20,15 @@ class CoreDataPersonsGatewayTests: XCTestCase { return CoreDataPersonsGateway(coreDataStack: inMemoryCoreDataStack) } - func test_SUT_AddPersonWithParameters_Succeed() { - // Given + func test_SUT_AddPersonWithParameters_Succeed() async throws { let addPersonParameters = AddPersonParameters.createParameters() - let addPersonCompletionHandlerExpectation = expectation(description: "Add person completion handler expectation") - // When - inMemoryCoreDataGateway.add(parameters: addPersonParameters) { (result) in - // Then - guard let person = try? result.get() else { - return XCTFail("Should've saved the person with success") - } - assert(person: person, builtFromParameters: addPersonParameters) - addPersonCompletionHandlerExpectation.fulfill() - } - // Exit - waitForExpectations(timeout: 5, handler: nil) + + let person = try await inMemoryCoreDataGateway.add(parameters: addPersonParameters) + + assert(person: person, builtFromParameters: addPersonParameters) } - func test_SUT_EditPerson_ShouldSucceedWithCorrectParameters() { + func test_SUT_EditPerson_ShouldSucceedWithCorrectParameters() async throws { // Given let cdPerson = inMemoryCoreDataStack.fakeEntity(withType: CoreDataPerson.self) cdPerson.id = UUID().uuidString @@ -49,54 +40,25 @@ class CoreDataPersonsGatewayTests: XCTestCase { editParams.isOnWidget = true editParams.dayOfBirth = Date().addingTimeInterval(-60 * 60 * 24 * 365 * 10) editParams.timeOfBirth = Date().addingTimeInterval(-60) - let expect = expectation(description: "Edit") - // When - inMemoryCoreDataGateway.edit(person: cdPerson.person, with: editParams) { result in - // Then - switch result { - case .success(let person): - assert(person: person, builtFromParameters: editParams) - case .failure(let error): - XCTFail("Expected to successfully edit person, but received error = \(error.localizedDescription)") - } - expect.fulfill() - } - // Exit - waitForExpectations(timeout: 1) + + let person = try await inMemoryCoreDataGateway.edit(person: cdPerson.person, with: editParams) + + assert(person: person, builtFromParameters: editParams) } - func test_SUT_RemovePerson_ShouldSucceed() { + func test_SUT_RemovePerson_ShouldSucceed() async throws { // Given let cdPerson = inMemoryCoreDataStack.fakeEntity(withType: CoreDataPerson.self) cdPerson.id = UUID().uuidString cdPerson.createdDate = Date() cdPerson.birthdate = Date() inMemoryCoreDataStack.saveContext() - let expect = expectation(description: "Remove") - // When - inMemoryCoreDataGateway.remove(person: cdPerson.person) { result in - // Then - assertOk(result, "Expected to successfully edit person, but received error") - expect.fulfill() - } - // Exit - waitForExpectations(timeout: 1) + + try await inMemoryCoreDataGateway.remove(person: cdPerson.person) } - func test_SUT_FetchPersons_ShouldSucceed() { - // Given - let workIsDone = expectation(description: "Fetch persons completion handler expectation") - // When - inMemoryCoreDataGateway.fetchPersons { result in - // Then - switch result { - case .failure: XCTFail("Should've fetched persons with success") - default: () - } - workIsDone.fulfill() - } - // Exit - waitForExpectations(timeout: 0.1, handler: nil) + func test_SUT_FetchPersons_ShouldSucceed() async throws { + _ = try await inMemoryCoreDataGateway.fetchPersons() } } diff --git a/GrowingUpTests/Helpers/Gateways/PersonsGatewaySpy.swift b/GrowingUpTests/Helpers/Gateways/PersonsGatewaySpy.swift index ed49f92..1c7dded 100644 --- a/GrowingUpTests/Helpers/Gateways/PersonsGatewaySpy.swift +++ b/GrowingUpTests/Helpers/Gateways/PersonsGatewaySpy.swift @@ -26,35 +26,32 @@ class PersonsGatewaySpy: PersonsGateway { var onEdit: (() -> Void)? var onRemove: (() -> Void)? - func add(parameters: AddPersonParameters, completionHandler: @escaping AddPersonEntityGatewayCompletionHandler) { + func add(parameters: AddPersonParameters) async throws -> Person { addPersonCalled = true addPersonParameters = parameters onAdd?() - completionHandler(addPersonResultToBeReturned) + return try addPersonResultToBeReturned.get() } - func fetchPersons(completionHandler: @escaping FetchPersonsEntityGatewayCompletionHandler) { + func fetchPersons() async throws -> [Person] { fetchPersonsCalled = true - completionHandler(fetchPersonsResultToBeReturned) + return try fetchPersonsResultToBeReturned.get() } - func fetchWidgetPersons(completion: @escaping FetchPersonsEntityGatewayCompletionHandler) { - completion(fetchPersonsResultToBeReturned) + func fetchWidgetPersons() async throws -> [Person] { + try fetchPersonsResultToBeReturned.get() } - func edit( - person: Person, with parameters: AddPersonParameters, - completionHandler: @escaping EditPersonEntityGatewayCompletionHandler - ) { + func edit(person: Person, with parameters: AddPersonParameters) async throws -> Person { editPersonCalled = true addPersonParameters = parameters onEdit?() - completionHandler(editPersonResultToBeReturned) + return try editPersonResultToBeReturned.get() } - func remove(person: Person, completionHandler: @escaping RemovePersonEntityGatewayCompletionHandler) { + func remove(person: Person) async throws { removePersonCalled = true onRemove?() - completionHandler(removePersonResultToBeReturned) + try removePersonResultToBeReturned.get() } } diff --git a/GrowingUpTests/Helpers/UserCases/AddPersonUseCaseSpy.swift b/GrowingUpTests/Helpers/UserCases/AddPersonUseCaseSpy.swift index 948e72b..45c6152 100644 --- a/GrowingUpTests/Helpers/UserCases/AddPersonUseCaseSpy.swift +++ b/GrowingUpTests/Helpers/UserCases/AddPersonUseCaseSpy.swift @@ -16,24 +16,18 @@ class AddPersonUseCaseSpy: AddPersonUseCase { var resultToBeReturned: Result! var callCompletionHandlerImmediate = true var personToAddParameters: AddPersonParameters? - private var completionHandler: AddPersonUseCaseCompletionHandler? - - func add(parameters: AddPersonParameters, completionHandler: @escaping AddPersonUseCaseCompletionHandler) { - personToAddParameters = parameters - self.completionHandler = completionHandler - if callCompletionHandlerImmediate { - callCompletionHandler() - } - } + private var continuation: CheckedContinuation? func add(parameters: AddPersonParameters) async throws -> Person { - try await withCheckedThrowingContinuation { continuation in - add(parameters: parameters) { continuation.resume(with: $0) } + personToAddParameters = parameters + if callCompletionHandlerImmediate { return try resultToBeReturned.get() } + return try await withCheckedThrowingContinuation { continuation in + self.continuation = continuation } } func callCompletionHandler() { - self.completionHandler?(resultToBeReturned) + continuation?.resume(with: resultToBeReturned.mapError { $0 as Error }) + continuation = nil } - } diff --git a/GrowingUpTests/Helpers/UserCases/DisplayPersonsUseCaseSpy.swift b/GrowingUpTests/Helpers/UserCases/DisplayPersonsUseCaseSpy.swift index b716e9a..87216d2 100644 --- a/GrowingUpTests/Helpers/UserCases/DisplayPersonsUseCaseSpy.swift +++ b/GrowingUpTests/Helpers/UserCases/DisplayPersonsUseCaseSpy.swift @@ -16,15 +16,6 @@ class DisplayPersonsUseCaseSpy: FetchPersonsUseCase { var displayPersonsCalled = false var resultToBeReturned: Result<[Person], CoreError>! - func fetchPersons(completionHandler: @escaping FetchPersonsUseCaseCompletionHandler) { - displayPersonsCalled = true - completionHandler(resultToBeReturned) - } - - func fetchWidgetPersons(completion: @escaping FetchPersonsUseCaseCompletionHandler) { - completion(resultToBeReturned) - } - func fetchPersons() async throws -> [Person] { displayPersonsCalled = true return try resultToBeReturned.get() diff --git a/GrowingUpTests/Helpers/UserCases/EditPersonUseCaseSpy.swift b/GrowingUpTests/Helpers/UserCases/EditPersonUseCaseSpy.swift index 41b74e7..5b669f0 100644 --- a/GrowingUpTests/Helpers/UserCases/EditPersonUseCaseSpy.swift +++ b/GrowingUpTests/Helpers/UserCases/EditPersonUseCaseSpy.swift @@ -16,27 +16,18 @@ class EditPersonUseCaseSpy: EditPersonUseCase { var resultToBeReturned: Result! var callCompletionHandlerImmediate = true var personToEditParameters: AddPersonParameters? - private var completionHandler: EditPersonUseCaseCompletionHandler? - - func edit( - person: Person, with parameters: AddPersonParameters, - completionHandler: @escaping EditPersonUseCaseCompletionHandler - ) { - personToEditParameters = parameters - self.completionHandler = completionHandler - if callCompletionHandlerImmediate { - callCompletionHandler() - } - } + private var continuation: CheckedContinuation? func edit(person: Person, with parameters: AddPersonParameters) async throws -> Person { - try await withCheckedThrowingContinuation { continuation in - edit(person: person, with: parameters) { continuation.resume(with: $0) } + personToEditParameters = parameters + if callCompletionHandlerImmediate { return try resultToBeReturned.get() } + return try await withCheckedThrowingContinuation { continuation in + self.continuation = continuation } } func callCompletionHandler() { - self.completionHandler?(resultToBeReturned) + continuation?.resume(with: resultToBeReturned.mapError { $0 as Error }) + continuation = nil } - } diff --git a/GrowingUpTests/Helpers/UserCases/FetchPersonsUseCaseSpy.swift b/GrowingUpTests/Helpers/UserCases/FetchPersonsUseCaseSpy.swift index 0d9d919..6c806bd 100644 --- a/GrowingUpTests/Helpers/UserCases/FetchPersonsUseCaseSpy.swift +++ b/GrowingUpTests/Helpers/UserCases/FetchPersonsUseCaseSpy.swift @@ -14,14 +14,6 @@ final class FetchPersonsUseCaseSpy: FetchPersonsUseCase { var completionResult: Result<[Person], CoreError>! - func fetchPersons(completionHandler: @escaping FetchPersonsUseCaseCompletionHandler) { - completionHandler(completionResult) - } - - func fetchWidgetPersons(completion: @escaping FetchPersonsUseCaseCompletionHandler) { - completion(completionResult) - } - func fetchPersons() async throws -> [Person] { try completionResult.get() } diff --git a/GrowingUpTests/Helpers/UserCases/RemovePersonUseCaseSpy.swift b/GrowingUpTests/Helpers/UserCases/RemovePersonUseCaseSpy.swift index 540343e..28cf18f 100644 --- a/GrowingUpTests/Helpers/UserCases/RemovePersonUseCaseSpy.swift +++ b/GrowingUpTests/Helpers/UserCases/RemovePersonUseCaseSpy.swift @@ -15,11 +15,6 @@ final class RemovePersonUseCaseSpy: RemovePersonUseCase { var didCallRemovePerson = false var resultToBeReturned: Result! - func remove(person: Person, completionHandler: @escaping RemovePersonUseCaseCompletionHandler) { - didCallRemovePerson = true - completionHandler(resultToBeReturned) - } - func remove(person: Person) async throws { didCallRemovePerson = true try resultToBeReturned.get() diff --git a/GrowingUpTests/Presenters/AddPersonPresenterTests.swift b/GrowingUpTests/Presenters/AddPersonPresenterTests.swift index afca332..623dcf4 100644 --- a/GrowingUpTests/Presenters/AddPersonPresenterTests.swift +++ b/GrowingUpTests/Presenters/AddPersonPresenterTests.swift @@ -11,6 +11,7 @@ import XCTest @testable import Core @testable import GrowingUp +@MainActor final class AddPersonPresenterTests: XCTestCase { // https://www.martinfowler.com/bliki/TestDouble.html @@ -59,34 +60,37 @@ final class AddPersonPresenterTests: XCTestCase { XCTAssertFalse(addPersonViewSpy.barButtonsEnabledState ?? true, "Bar buttons should've been set to disabled") } - func test_SUT_AddButtonPressed_AddAndCancelButtonsEnabledAfterCompletionHandlerCalled() { + func test_SUT_AddButtonPressed_AddAndCancelButtonsEnabledAfterCompletionHandlerCalled() async { // Given setupSUT_WithAddPersonData() addPersonUseCaseSpy.resultToBeReturned = .success(Person.createPerson()) // When sut.rightBarButtonPressed() + await waitUntil { self.addPersonViewSpy.barButtonsEnabledState == true } // Then XCTAssertTrue(addPersonViewSpy.barButtonsEnabledState ?? false, "Bar buttons should've been set to enabled") } - func test_SUT_AddButtonPressed_ShouldSavePerson() { + func test_SUT_AddButtonPressed_ShouldSavePerson() async { // Given let parameters = setupSUT_WithAddPersonData() addPersonUseCaseSpy.resultToBeReturned = .success(Person.createPerson()) // When sut.rightBarButtonPressed() + await waitUntil { self.addPersonUseCaseSpy.personToAddParameters != nil } // Then XCTAssertEqual( addPersonUseCaseSpy.personToAddParameters!, parameters, "Should have been called addPerson for AddPersonUseCase") } - func test_SUT_AddButtonPressed_CallingEditPersonDelegateMethod() { + func test_SUT_AddButtonPressed_CallingEditPersonDelegateMethod() async { // Given setupSUT_WithAddPersonData() let expectedPersonToAdd = Person.createPerson() addPersonUseCaseSpy.resultToBeReturned = .success(expectedPersonToAdd) // When sut.rightBarButtonPressed() + await waitUntil { self.addPersonPresenterDelegateSpy.didCalledAddPerson } // Then XCTAssertEqual( addPersonPresenterDelegateSpy.addedPerson, expectedPersonToAdd, "Should have been add expected person") @@ -94,7 +98,7 @@ final class AddPersonPresenterTests: XCTestCase { } - func test_SUT_AddButtonPressedWithError_ShouldDisplayErrorOnView() { + func test_SUT_AddButtonPressedWithError_ShouldDisplayErrorOnView() async { // Given setupSUT_WithAddPersonData() let expectedErrorTitle = "Error" @@ -103,6 +107,7 @@ final class AddPersonPresenterTests: XCTestCase { CoreError(title: expectedErrorTitle, message: expectedErrorMessage)) // When sut.rightBarButtonPressed() + await waitUntil { self.addPersonViewSpy.displayAddPersonErrorTitle != nil } // Then XCTAssertEqual(expectedErrorTitle, addPersonViewSpy.displayAddPersonErrorTitle, "Error title doesn't match") XCTAssertEqual(expectedErrorMessage, addPersonViewSpy.displayAddPersonErrorMessage, "Error message doesn't match") @@ -238,4 +243,12 @@ final class AddPersonPresenterTests: XCTestCase { name: "John", dayOfBirth: bDate, timeOfBirth: tDate, appImage: appPic, widgetImage: widgetPic, isOnWidget: isOnWidget) } + + private func waitUntil(_ condition: @escaping () -> Bool) async { + for _ in 0..<1_000 { + if condition() { return } + await Task.yield() + } + XCTFail("Timed out waiting for async presenter work") + } } diff --git a/GrowingUpTests/Presenters/EditPersonPresenterTests.swift b/GrowingUpTests/Presenters/EditPersonPresenterTests.swift index a9350d0..0d14842 100644 --- a/GrowingUpTests/Presenters/EditPersonPresenterTests.swift +++ b/GrowingUpTests/Presenters/EditPersonPresenterTests.swift @@ -11,6 +11,7 @@ import XCTest @testable import Core @testable import GrowingUp +@MainActor final class EditPersonPresenterTests: XCTestCase { // https://www.martinfowler.com/bliki/TestDouble.html @@ -61,34 +62,37 @@ final class EditPersonPresenterTests: XCTestCase { XCTAssertFalse(editPersonViewSpy.barButtonsEnabledState ?? true, "Bar buttons should've been set to disabled") } - func test_SUT_SaveButtonPressed_BarButtonsEnabledAfterCompletionHandlerCalled() { + func test_SUT_SaveButtonPressed_BarButtonsEnabledAfterCompletionHandlerCalled() async { // Given setupSUT_WithAddPersonData() editPersonUseCaseSpy.resultToBeReturned = .success(Person.createPerson()) // When sut.rightBarButtonPressed() + await waitUntil { self.editPersonViewSpy.barButtonsEnabledState == true } // Then XCTAssertTrue(editPersonViewSpy.barButtonsEnabledState ?? false, "Bar buttons should've been set to enabled") } - func test_SUT_SaveButtonPressed_ShouldStartEditingPerson() { + func test_SUT_SaveButtonPressed_ShouldStartEditingPerson() async { // Given let parameters = setupSUT_WithAddPersonData() editPersonUseCaseSpy.resultToBeReturned = .success(Person.createPerson()) // When sut.rightBarButtonPressed() + await waitUntil { self.editPersonUseCaseSpy.personToEditParameters != nil } // Then XCTAssertEqual( editPersonUseCaseSpy.personToEditParameters, parameters, "Should have been called addPerson for AddPersonUseCase") } - func test_SUT_SaveButtonPressed_CallingEditPersonDelegateMethod() { + func test_SUT_SaveButtonPressed_CallingEditPersonDelegateMethod() async { // Given setupSUT_WithAddPersonData() let expectedPersonToEdit = Person.createPerson() editPersonUseCaseSpy.resultToBeReturned = .success(expectedPersonToEdit) // When sut.rightBarButtonPressed() + await waitUntil { self.addPersonPresenterDelegateSpy.didCalledEditPerson } // Then XCTAssertEqual( addPersonPresenterDelegateSpy.editedPerson, expectedPersonToEdit, "Should have been edit expected person") @@ -96,7 +100,7 @@ final class EditPersonPresenterTests: XCTestCase { } - func test_SUT_SaveButtonPressedWithError_ShouldDisplayErrorOnView() { + func test_SUT_SaveButtonPressedWithError_ShouldDisplayErrorOnView() async { // Given setupSUT_WithAddPersonData() let expectedErrorTitle = "Error" @@ -105,6 +109,7 @@ final class EditPersonPresenterTests: XCTestCase { CoreError(title: expectedErrorTitle, message: expectedErrorMessage)) // When sut.rightBarButtonPressed() + await waitUntil { self.editPersonViewSpy.displayAddPersonErrorTitle != nil } // Then XCTAssertEqual(expectedErrorTitle, editPersonViewSpy.displayAddPersonErrorTitle, "Error title doesn't match") XCTAssertEqual(expectedErrorMessage, editPersonViewSpy.displayAddPersonErrorMessage, "Error message doesn't match") @@ -243,11 +248,12 @@ final class EditPersonPresenterTests: XCTestCase { XCTAssertEqual(editPersonViewSpy.displayedBarButtons.count, 2, "Expected to display only 2 type of buttons") } - func test_SUT_WhenCalledToRemovePerson_CallingRemovePersonUseCaseAndEditPresenterDelegate() { + func test_SUT_WhenCalledToRemovePerson_CallingRemovePersonUseCaseAndEditPresenterDelegate() async { // Given removePersonUseCaseSpy.resultToBeReturned = .success(()) // When sut.removePersonPressed() + await waitUntil { self.addPersonPresenterDelegateSpy.didCalledRemovePerson } // Then XCTAssertTrue(removePersonUseCaseSpy.didCallRemovePerson, "Expected to call remove person use case") XCTAssertTrue(addPersonPresenterDelegateSpy.didCalledRemovePerson, "Expected to call remove Person") @@ -272,4 +278,12 @@ final class EditPersonPresenterTests: XCTestCase { name: "John", dayOfBirth: bDate, timeOfBirth: tDate, appImage: appPic, widgetImage: widgetPic, isOnWidget: isOnWidget) } + + private func waitUntil(_ condition: @escaping () -> Bool) async { + for _ in 0..<1_000 { + if condition() { return } + await Task.yield() + } + XCTFail("Timed out waiting for async presenter work") + } } diff --git a/GrowingUpTests/Presenters/EmptyPersonPresenterTests.swift b/GrowingUpTests/Presenters/EmptyPersonPresenterTests.swift index 0f33751..e1a8e3e 100644 --- a/GrowingUpTests/Presenters/EmptyPersonPresenterTests.swift +++ b/GrowingUpTests/Presenters/EmptyPersonPresenterTests.swift @@ -11,7 +11,8 @@ import XCTest @testable import Core @testable import GrowingUp -class EmptyPersonPresenterTests: XCTestCase { +@MainActor +final class EmptyPersonPresenterTests: XCTestCase { var sut: EmptyPersonPresenterImplementation! let routerSpy = EmptyPersonViewRouterSpy() diff --git a/GrowingUpTests/Presenters/PersonsListPresenterTests.swift b/GrowingUpTests/Presenters/PersonsListPresenterTests.swift index e6252ff..38a7107 100644 --- a/GrowingUpTests/Presenters/PersonsListPresenterTests.swift +++ b/GrowingUpTests/Presenters/PersonsListPresenterTests.swift @@ -11,7 +11,8 @@ import XCTest @testable import Core @testable import GrowingUp -class PersonsListPresenterTests: XCTestCase { +@MainActor +final class PersonsListPresenterTests: XCTestCase { var sut: PersonsListPresenterImplementation! let personsListSpy = PersonsListViewSpy() @@ -23,7 +24,8 @@ class PersonsListPresenterTests: XCTestCase { sut = PersonsListPresenterImplementation(view: personsListSpy, displayPersonsUseCase: displayPersonsUseCaseSpy) } - func test_SUT_LoadingPersonsOnInit() { + func test_SUT_LoadingPersonsOnInit() async { + await waitUntil { self.personsListSpy.didCallUpdateListOfScreens } // Then XCTAssertTrue(displayPersonsUseCaseSpy.displayPersonsCalled, "Expected to call loadiing of Persons") XCTAssertTrue( @@ -33,7 +35,8 @@ class PersonsListPresenterTests: XCTestCase { "Expected to return number of pages according to returned Persons array") } - func test_SUT_ReturningPersonOverviewScreenForPersonAtCorrectIndex() { + func test_SUT_ReturningPersonOverviewScreenForPersonAtCorrectIndex() async { + await waitUntil { self.personsListSpy.didCallUpdateListOfScreens } // Given let index = 0 // When @@ -42,7 +45,8 @@ class PersonsListPresenterTests: XCTestCase { XCTAssertTrue(screen is PersonOverviewView, "Expected to return PersonOverview Screen") } - func test_SUT_ReturningNilForIncorrectPersonIndex() { + func test_SUT_ReturningNilForIncorrectPersonIndex() async { + await waitUntil { self.personsListSpy.didCallUpdateListOfScreens } // When let screenAt3 = sut.pageViewControllerScreen(atIndex: 2) let screenAtMinus1 = sut.pageViewControllerScreen(atIndex: -1) @@ -51,7 +55,8 @@ class PersonsListPresenterTests: XCTestCase { XCTAssertNil(screenAtMinus1, "Expected to return nil Screen for incorrect index") } - func test_SUT_ReturningEmptyPersonScreenForLastPageIndex() { + func test_SUT_ReturningEmptyPersonScreenForLastPageIndex() async { + await waitUntil { self.personsListSpy.didCallUpdateListOfScreens } // Given let lastIndex = sut.numberOfPages() - 1 // When @@ -59,4 +64,12 @@ class PersonsListPresenterTests: XCTestCase { // Then XCTAssertTrue(screen is EmptyPersonView, "Expected to return EmptyPerson Screen") } + + private func waitUntil(_ condition: @escaping () -> Bool) async { + for _ in 0..<1_000 { + if condition() { return } + await Task.yield() + } + XCTFail("Timed out waiting for async presenter work") + } } diff --git a/GrowingUpTests/Presenters/TextFieldCellPresenterTests.swift b/GrowingUpTests/Presenters/TextFieldCellPresenterTests.swift index 8d0138d..8b2da3f 100644 --- a/GrowingUpTests/Presenters/TextFieldCellPresenterTests.swift +++ b/GrowingUpTests/Presenters/TextFieldCellPresenterTests.swift @@ -54,6 +54,7 @@ final class TextFieldCellPresenterTests: XCTestCase { XCTAssertEqual(expectedName, sut.valueFor(row: 0), "The model value in presenter didn't updated after user input") } + @MainActor func test_SUT_WhenConfiguringCell_PassingObserverToIt() { // Given let tfCellSpy = TextFieldCellViewSpy() diff --git a/GrowingUpTests/UseCases/AddPersonUseCaseTests.swift b/GrowingUpTests/UseCases/AddPersonUseCaseTests.swift index 0e78284..d3b4b66 100644 --- a/GrowingUpTests/UseCases/AddPersonUseCaseTests.swift +++ b/GrowingUpTests/UseCases/AddPersonUseCaseTests.swift @@ -1,71 +1,20 @@ -// -// EditPersonUseCaseTests.swift -// GrowingUpTests -// -// Created by zigdanis on 16/03/2019. -// Copyright © 2019-2026 Danis Ziganshin. -// - import XCTest @testable import Core @testable import GrowingUp -class AddPersonUseCaseTests: XCTestCase { - - var sut: AddPersonUseCaseImplementation! - let personsGatewaySpy = PersonsGatewaySpy() - - override func setUp() { - super.setUp() - sut = AddPersonUseCaseImplementation(personsGateway: personsGatewaySpy) - } - - func test_SUT_AddPerson_PassingParamsToPersonsGatewayAndCallsCompletionHandler() { - // Given - let params = AddPersonParameters.createParameters() - let expectedResultToBeReturned: Result = .success(Person.createPerson()) - personsGatewaySpy.addPersonResultToBeReturned = expectedResultToBeReturned - let addPersonExpectation = expectation(description: "Add Person Expectation") - // When - sut.add(parameters: params) { result in - // Then - XCTAssertEqual( - self.personsGatewaySpy.addPersonParameters, params, - "Should have been call PersonsGateway AddPerson method with specified params") - - XCTAssertEqual(expectedResultToBeReturned, result, "Completion handler didn't return expected result") - addPersonExpectation.fulfill() - } - waitForExpectations(timeout: 1, handler: nil) - } - - func test_SUT_AddPersonFail_CallsCompletionHandler() { - // Given - let params = AddPersonParameters.createParameters() - let expectedResultToBeReturned: Result = .failure(CoreError(message: "Some Error")) - personsGatewaySpy.addPersonResultToBeReturned = expectedResultToBeReturned - let addPersonExpectation = expectation(description: "Add Person Expectation") - // When - sut.add(parameters: params) { result in - // Then - XCTAssertEqual( - self.personsGatewaySpy.addPersonParameters, params, - "Should have been call PersonsGateway AddPerson method with specified params") - XCTAssertEqual(expectedResultToBeReturned, result, "Completion handler didn'w return expected result") - addPersonExpectation.fulfill() - } - waitForExpectations(timeout: 1, handler: nil) - } +final class AddPersonUseCaseTests: XCTestCase { - func testAsyncAddReturnsPerson() async throws { + func testAddPassesParametersAndReturnsPerson() async throws { + let gateway = PersonsGatewaySpy() + let sut = AddPersonUseCaseImplementation(personsGateway: gateway) let parameters = AddPersonParameters.createParameters() let expectedPerson = Person.createPerson() - personsGatewaySpy.addPersonResultToBeReturned = .success(expectedPerson) + gateway.addPersonResultToBeReturned = .success(expectedPerson) let person = try await sut.add(parameters: parameters) XCTAssertEqual(person, expectedPerson) - XCTAssertEqual(personsGatewaySpy.addPersonParameters, parameters) + XCTAssertEqual(gateway.addPersonParameters, parameters) } } diff --git a/GrowingUpTests/UseCases/EditPersonUseCaseTests.swift b/GrowingUpTests/UseCases/EditPersonUseCaseTests.swift index 30eeb8c..909c3b2 100644 --- a/GrowingUpTests/UseCases/EditPersonUseCaseTests.swift +++ b/GrowingUpTests/UseCases/EditPersonUseCaseTests.swift @@ -1,59 +1,18 @@ -// -// EditPersonUseCaseTests.swift -// GrowingUpTests -// -// Created by zigdanis on 24/04/2019. -// Copyright © 2019-2026 Danis Ziganshin. -// - import XCTest @testable import Core -class EditPersonUseCaseTests: XCTestCase { - - var sut: EditPersonUseCaseImplementation! - let personsGatewaySpy = PersonsGatewaySpy() +final class EditPersonUseCaseTests: XCTestCase { - override func setUp() { - sut = EditPersonUseCaseImplementation(personsGateway: personsGatewaySpy) - } - - func test_SUT_EditPersonWithParams_PassingParamsToGatewayAndCallsCompletionHandler() { - // Given - let personToEdit = Person.createPerson() - let params = AddPersonParameters.createParameters() - let expectedResultToBeReturned: Result = .success(Person.createPerson()) - personsGatewaySpy.editPersonResultToBeReturned = expectedResultToBeReturned - let editPersonExpectation = expectation(description: "Edit Person expectation") - // When - sut.edit(person: personToEdit, with: params) { result in - // Then - XCTAssertEqual( - self.personsGatewaySpy.addPersonParameters, params, - "Should have been called Edit Person with provided parameters") - XCTAssertEqual(expectedResultToBeReturned, result, "Expected to receive success edited person result") - editPersonExpectation.fulfill() - } - waitForExpectations(timeout: 0.1, handler: nil) - } + func testEditPassesParametersAndReturnsPerson() async throws { + let gateway = PersonsGatewaySpy() + let sut = EditPersonUseCaseImplementation(personsGateway: gateway) + let person = Person.createPerson() + let parameters = AddPersonParameters.createParameters() + gateway.editPersonResultToBeReturned = .success(person) - func test_SUT_EditPersonFaile_CallsCompletionHandler() { - // Given - let personToEdit = Person.createPerson() - let params = AddPersonParameters.createParameters() - let expectedResultToBeReturned: Result = .failure(CoreError(message: "Some Edit failed error")) - personsGatewaySpy.editPersonResultToBeReturned = expectedResultToBeReturned - let editPersonExpectation = expectation(description: "Edit Person expectation") - // When - sut.edit(person: personToEdit, with: params) { result in - // Then - XCTAssertEqual( - self.personsGatewaySpy.addPersonParameters, params, - "Should have been called Edit Person with provided parameters") - XCTAssertEqual(expectedResultToBeReturned, result, "Expected to receive failure of editing person result") - editPersonExpectation.fulfill() - } - waitForExpectations(timeout: 0.1, handler: nil) + let editedPerson = try await sut.edit(person: person, with: parameters) + XCTAssertEqual(editedPerson, person) + XCTAssertEqual(gateway.addPersonParameters, parameters) } } diff --git a/GrowingUpTests/UseCases/FetchPersonsUseCaseTests.swift b/GrowingUpTests/UseCases/FetchPersonsUseCaseTests.swift index 9d5d31d..cb3d488 100644 --- a/GrowingUpTests/UseCases/FetchPersonsUseCaseTests.swift +++ b/GrowingUpTests/UseCases/FetchPersonsUseCaseTests.swift @@ -1,58 +1,24 @@ -// -// DisplayPersonsUseCaseTests.swift -// GrowingUpTests -// -// Created by zigdanis on 11/04/2019. -// Copyright © 2019-2026 Danis Ziganshin. -// - import XCTest @testable import Core -@testable import GrowingUp - -class FetchPersonsUseCaseTests: XCTestCase { - var sut: FetchPersonsUseCaseImplementation! - let gatewaySpy = PersonsGatewaySpy() +final class FetchPersonsUseCaseTests: XCTestCase { - override func setUp() { - sut = FetchPersonsUseCaseImplementation(personsGateway: gatewaySpy) - } + func testFetchReturnsPersons() async throws { + let gateway = PersonsGatewaySpy() + let sut = FetchPersonsUseCaseImplementation(personsGateway: gateway) + let expectedPersons = [Person.createPerson()] + gateway.fetchPersonsResultToBeReturned = .success(expectedPersons) - func test_SUT_WhenFetchPersons_CallingGatewayAndCompletion() { - // Given - let persons = [Person.createPerson()] - let expectedResult: Result<[Person], CoreError> = .success(persons) - gatewaySpy.fetchPersonsResultToBeReturned = expectedResult - let workIsDone = expectation(description: "Expecting to finish fetching PersonsGateway") - // When - sut.fetchPersons { result in - // Then - XCTAssertTrue(self.gatewaySpy.fetchPersonsCalled, "Expected to receive fetchPersons call in PersonsGateway") - XCTAssertEqual(result, expectedResult, "Expected to get fetched persons") - workIsDone.fulfill() - } - waitForExpectations(timeout: 0.1) - } - - func test_SUT_WhenFailedToFetchPersons_CallingCompletioWithError() { - // Given - let expectedResult: Result<[Person], CoreError> = .failure(CoreError.coreDataFetchFailed) - gatewaySpy.fetchPersonsResultToBeReturned = expectedResult - let workIsDone = expectation(description: "Expecting to finish fetching PersonsGateway") - // When - sut.fetchPersons { result in - // Then - XCTAssertTrue(self.gatewaySpy.fetchPersonsCalled, "Expected to receive fetchPersons call in PersonsGateway") - XCTAssertEqual(result, expectedResult, "Expected to get fetched persons") - workIsDone.fulfill() - } - waitForExpectations(timeout: 0.1) + let persons = try await sut.fetchPersons() + XCTAssertEqual(persons, expectedPersons) + XCTAssertTrue(gateway.fetchPersonsCalled) } - func testAsyncFetchPropagatesError() async { - gatewaySpy.fetchPersonsResultToBeReturned = .failure(.coreDataFetchFailed) + func testFetchPropagatesError() async { + let gateway = PersonsGatewaySpy() + let sut = FetchPersonsUseCaseImplementation(personsGateway: gateway) + gateway.fetchPersonsResultToBeReturned = .failure(.coreDataFetchFailed) do { _ = try await sut.fetchPersons() diff --git a/GrowingUpTests/UseCases/RemovePersonUseCaseTests.swift b/GrowingUpTests/UseCases/RemovePersonUseCaseTests.swift index 05c859f..471b4f4 100644 --- a/GrowingUpTests/UseCases/RemovePersonUseCaseTests.swift +++ b/GrowingUpTests/UseCases/RemovePersonUseCaseTests.swift @@ -1,60 +1,16 @@ -// -// RemovePersonUseCaseTests.swift -// GrowingUpTests -// -// Created by zigdanis on 24/04/2019. -// Copyright © 2019-2026 Danis Ziganshin. -// - import XCTest @testable import Core -class RemovePersonUseCaseTests: XCTestCase { - - var sut: RemovePersonUseCaseImplementation! - let personsGatewaySpy = PersonsGatewaySpy() +final class RemovePersonUseCaseTests: XCTestCase { - override func setUp() { - sut = RemovePersonUseCaseImplementation(personsGateway: personsGatewaySpy) - } + func testRemoveCallsGateway() async throws { + let gateway = PersonsGatewaySpy() + let sut = RemovePersonUseCaseImplementation(personsGateway: gateway) + gateway.removePersonResultToBeReturned = .success(()) - func test_SUT_WhenRemovingPerson_CallsPersonsGatewayAndCallsCompletion() { - // Given - let personToDelete = Person.createPerson() - let expectedResultToBeReturned: Result = .success(()) - personsGatewaySpy.removePersonResultToBeReturned = expectedResultToBeReturned - let removePersonExpectation = expectation(description: "Remove Person expectation") - // When - sut.remove(person: personToDelete) { result in - // Then - assertOk(result, "Expected to receive success remove person result") - XCTAssertTrue(self.personsGatewaySpy.removePersonCalled, "Expected to call Persons Gateway") - removePersonExpectation.fulfill() - } - waitForExpectations(timeout: 0.1, handler: nil) - } + try await sut.remove(person: Person.createPerson()) - func test_SUT_WhenRemovingPersonFailed_CallsCompletionHandler() { - // Given - let personToDelete = Person.createPerson() - let expectedError = CoreError(message: "Some Errror") - personsGatewaySpy.removePersonResultToBeReturned = .failure(expectedError) - let removePersonExpectation = expectation(description: "Remove Person expectation") - // When - sut.remove(person: personToDelete) { result in - // Then - do { - try result.get() - } catch { - XCTAssertEqual(error as? CoreError, expectedError, "Expected to call completion with specified error") - } - removePersonExpectation.fulfill() - } - waitForExpectations(timeout: 0.1, handler: nil) + XCTAssertTrue(gateway.removePersonCalled) } } - -func assertOk(_ result: Result, _ message: String) { - XCTAssertNoThrow(try result.get(), message) -} diff --git a/Widget/AgeWidgetProvider.swift b/Widget/AgeWidgetProvider.swift index 1069b76..b2a6cd2 100644 --- a/Widget/AgeWidgetProvider.swift +++ b/Widget/AgeWidgetProvider.swift @@ -56,13 +56,14 @@ struct AgeWidgetProvider: TimelineProvider { completion(AgeEntry(date: Date(), persons: Self.samplePersons())) return } - loadPersons { persons in - completion(AgeEntry(date: Date(), persons: persons)) + Task { + completion(AgeEntry(date: Date(), persons: await loadPersons())) } } func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { - loadPersons { persons in + Task { + let persons = await loadPersons() let start = Date() var entries: [AgeEntry] = [] for offset in 0.. Void) { - fetchUseCase.fetchWidgetPersons { result in - switch result { - case .success(let people): - let limited = Array(people.prefix(rowsLimit)) - resolveImages(for: limited, completion: completion) - case .failure(let error): - Logging.logError(error) - completion([]) - } + private func loadPersons() async -> [WidgetPerson] { + do { + let people = try await fetchUseCase.fetchWidgetPersons() + return await resolveImages(for: Array(people.prefix(rowsLimit))) + } catch { + Logging.logError(CoreError(error: error)) + return [] } } - private func resolveImages(for people: [Person], completion: @escaping ([WidgetPerson]) -> Void) { - var widgetPersons = [WidgetPerson?](repeating: nil, count: people.count) - let group = DispatchGroup() - + private func resolveImages(for people: [Person]) async -> [WidgetPerson] { + var resolved = [WidgetPerson]() for (index, person) in people.enumerated() { - func store(_ image: UIImage?) { - widgetPersons[index] = WidgetPerson( + var image: UIImage? + if let personImage = PersonImage(id: person.widgetPicId) { + image = try? await ImagesCache.loadImageFromDiskOrMemory(image: personImage) + } + resolved.append( + WidgetPerson( id: person.id, index: index, name: person.name, birthday: person.birthday, - image: image) - } - guard let personImage = PersonImage(id: person.widgetPicId) else { - store(nil) - continue - } - group.enter() - ImagesCache.loadImageFromDiskOrMemory(image: personImage) { imageResult in - switch imageResult { - case .success(let image): store(image) - case .failure: store(nil) - } - group.leave() - } - } - - group.notify(queue: .main) { - completion(widgetPersons.compactMap { $0 }) + image: image)) } + return resolved } // MARK: - Previews / placeholder