diff --git a/SWDestinyTrades/Classes/AddCard/Controller/AddCardViewController.swift b/SWDestinyTrades/Classes/AddCard/Controller/AddCardViewController.swift index 22776c46..edc74e4b 100644 --- a/SWDestinyTrades/Classes/AddCard/Controller/AddCardViewController.swift +++ b/SWDestinyTrades/Classes/AddCard/Controller/AddCardViewController.swift @@ -8,38 +8,17 @@ import UIKit -enum AddCardType { - case lent - case borrow - case collection -} - final class AddCardViewController: UIViewController { private let addCardView: AddCardViewType - private let destinyService: SWDestinyServiceProtocol - private let addCardType: AddCardType - private var cards = [CardDTO]() - private var personDTO: PersonDTO? - private var userCollectionDTO: UserCollectionDTO? - private lazy var navigator = AddCardNavigator(self.navigationController) - private let database: DatabaseProtocol? + + var presenter: AddCardPresenterProtocol? // MARK: - Life Cycle - init(with view: AddCardViewType = AddCardView(), - service: SWDestinyServiceProtocol = SWDestinyService(), - database: DatabaseProtocol?, - person: PersonDTO? = nil, - userCollection: UserCollectionDTO? = nil, - type: AddCardType) { + init(with view: AddCardViewType = AddCardView()) { addCardView = view - destinyService = service - self.database = database - addCardType = type super.init(nibName: nil, bundle: nil) - personDTO = person - userCollectionDTO = userCollection } @available(*, unavailable) @@ -54,18 +33,18 @@ final class AddCardViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - fetchAllCards() + presenter?.fetchAllCards() addCardView.didSelectCard = { [weak self] card in - self?.insert(card: card) + self?.presenter?.insert(card: card) } addCardView.didSelectAccessory = { [weak self] card in - self?.navigateToNextController(with: card) + self?.presenter?.cardDetailButtonTouched(with: card) } addCardView.doingSearch = { [weak self] query in - self?.addCardView.doingSearch(query) + self?.presenter?.doingSearch(query) } } @@ -73,81 +52,23 @@ final class AddCardViewController: UIViewController { super.viewWillAppear(animated) navigationItem.title = L10n.addCard } +} - // MARK: - Helpers +extension AddCardViewController: AddCardViewProtocol { - private func fetchAllCards() { + func startLoading() { addCardView.startLoading() - Task { [weak self] in - guard let self else { return } - - do { - let allCards = try await self.destinyService.retrieveAllCards() - self.addCardView.stopLoading() - self.addCardView.updateSearchList(allCards) - self.cards = allCards - } catch { - ToastMessages.showNetworkErrorMessage() - LoggerManager.shared.log(event: .allCards, parameters: ["error": error.localizedDescription]) - } - } - } - - private func insert(card: CardDTO) { - switch addCardType { - case .lent: - insertToLentMe(card: card) - case .borrow: - insertToBorrowed(card: card) - case .collection: - insertToCollection(card: card) - } } - private func insertToBorrowed(card: CardDTO) { - if let person = personDTO, !person.borrowed.contains(where: { $0.code == card.code }) { - try? database?.update { [weak self] in - person.borrowed.append(card) - self?.showSuccessMessage(card: card) - } - let personDataDict: [String: PersonDTO] = ["personDTO": person] - NotificationCenter.default.post(name: NotificationKey.reloadTableViewNotification, object: nil, userInfo: personDataDict) - } else { - ToastMessages.showInfoMessage(title: "", message: L10n.alreadyAdded) - } - } - - private func insertToLentMe(card: CardDTO) { - if let person = personDTO, !person.lentMe.contains(where: { $0.code == card.code }) { - try? database?.update { [weak self] in - person.lentMe.append(card) - self?.showSuccessMessage(card: card) - } - let personDataDict: [String: PersonDTO] = ["personDTO": person] - NotificationCenter.default.post(name: NotificationKey.reloadTableViewNotification, object: nil, userInfo: personDataDict) - } else { - ToastMessages.showInfoMessage(title: "", message: L10n.alreadyAdded) - } + func stopLoading() { + addCardView.stopLoading() } - private func insertToCollection(card: CardDTO) { - if let userCollection = userCollectionDTO, !userCollection.myCollection.contains(where: { $0.code == card.code }) { - try? database?.update { [weak self] in - userCollection.myCollection.append(card) - self?.showSuccessMessage(card: card) - } - } else { - ToastMessages.showInfoMessage(title: "", message: L10n.alreadyAdded) - } - } - - private func showSuccessMessage(card: CardDTO) { - LoadingHUD.show(.labeledSuccess(title: L10n.added, subtitle: card.name)) + func updateSearchList(_ cards: [CardDTO]) { + addCardView.updateSearchList(cards) } - // MARK: - Navigation - - func navigateToNextController(with card: CardDTO) { - navigator.navigate(to: .cardDetail(database: database, with: cards, card: card)) + func doingSearch(_ query: String) { + addCardView.doingSearch(query) } } diff --git a/SWDestinyTrades/Classes/AddCard/Factory/AddCardViewControllerFactory.swift b/SWDestinyTrades/Classes/AddCard/Factory/AddCardViewControllerFactory.swift new file mode 100644 index 00000000..4a0522e7 --- /dev/null +++ b/SWDestinyTrades/Classes/AddCard/Factory/AddCardViewControllerFactory.swift @@ -0,0 +1,42 @@ +// +// AddCardViewControllerFactory.swift +// SWDestinyTrades +// +// Created by Diogo Autilio on 30/12/23. +// Copyright © 2023 Diogo Autilio. All rights reserved. +// + +import Foundation +import UIKit + +final class AddCardViewControllerFactory: ViewControllerFactory { + + private let database: DatabaseProtocol? + private let addCardType: AddCardType + private let personDTO: PersonDTO? + private let userCollectionDTO: UserCollectionDTO? + + init(database: DatabaseProtocol?, + addCardType: AddCardType, + personDTO: PersonDTO? = nil, + userCollectionDTO: UserCollectionDTO? = nil) { + self.database = database + self.addCardType = addCardType + self.personDTO = personDTO + self.userCollectionDTO = userCollectionDTO + } + + func createViewController() -> UIViewController { + let viewController = AddCardViewController() + let router = AddCardNavigator(viewController) + let interactor = AddCardInteractor() + let viewModel = AddCardViewModel(person: personDTO, userCollection: userCollectionDTO, type: addCardType) + let presenter = AddCardPresenter(view: viewController, + interactor: interactor, + database: database, + navigator: router, + viewModel: viewModel) + viewController.presenter = presenter + return viewController + } +} diff --git a/SWDestinyTrades/Classes/AddCard/Interactor/AddCardInteractor.swift b/SWDestinyTrades/Classes/AddCard/Interactor/AddCardInteractor.swift new file mode 100644 index 00000000..6cf09650 --- /dev/null +++ b/SWDestinyTrades/Classes/AddCard/Interactor/AddCardInteractor.swift @@ -0,0 +1,26 @@ +// +// AddCardInteractor.swift +// SWDestinyTrades +// +// Created by Diogo Autilio on 30/12/23. +// Copyright © 2023 Diogo Autilio. All rights reserved. +// + +import Foundation + +protocol AddCardInteractorProtocol { + func retrieveAllCards() async throws -> [CardDTO] +} + +final class AddCardInteractor: AddCardInteractorProtocol { + + private let service: SWDestinyServiceProtocol + + init(service: SWDestinyServiceProtocol = SWDestinyService()) { + self.service = service + } + + func retrieveAllCards() async throws -> [CardDTO] { + return try await service.retrieveAllCards() + } +} diff --git a/SWDestinyTrades/Classes/AddCard/Navigator/AddCardNavigator.swift b/SWDestinyTrades/Classes/AddCard/Navigator/AddCardNavigator.swift index e176317a..bbabbf69 100644 --- a/SWDestinyTrades/Classes/AddCard/Navigator/AddCardNavigator.swift +++ b/SWDestinyTrades/Classes/AddCard/Navigator/AddCardNavigator.swift @@ -14,19 +14,18 @@ final class AddCardNavigator: Navigator { case cardDetail(database: DatabaseProtocol?, with: [CardDTO], card: CardDTO) } - private weak var navigationController: UINavigationController? + private weak var viewController: UIViewController? // MARK: - Initializer - init(_ navigationController: UINavigationController?) { - self.navigationController = navigationController + init(_ viewController: UIViewController?) { + self.viewController = viewController } // MARK: - Navigator func navigate(to destination: Destination) { - let viewController = makeViewController(for: destination) - navigationController?.pushViewController(viewController, animated: true) + viewController?.navigationController?.pushViewController(makeViewController(for: destination), animated: true) } // MARK: - Private diff --git a/SWDestinyTrades/Classes/AddCard/Presenter/AddCardPresenter.swift b/SWDestinyTrades/Classes/AddCard/Presenter/AddCardPresenter.swift new file mode 100644 index 00000000..72dc3c6d --- /dev/null +++ b/SWDestinyTrades/Classes/AddCard/Presenter/AddCardPresenter.swift @@ -0,0 +1,122 @@ +// +// AddCardPresenter.swift +// SWDestinyTrades +// +// Created by Diogo Autilio on 30/12/23. +// Copyright © 2023 Diogo Autilio. All rights reserved. +// + +import Foundation + +protocol AddCardPresenterProtocol { + func fetchAllCards() + func insert(card: CardDTO) + func doingSearch(_ query: String) + func cardDetailButtonTouched(with card: CardDTO) +} + +final class AddCardPresenter: AddCardPresenterProtocol { + + private let interactor: AddCardInteractorProtocol + private let database: DatabaseProtocol? + private let navigator: AddCardNavigator + private let viewModel: AddCardViewModel + + private weak var view: AddCardViewProtocol? + private var cards = [CardDTO]() + + init(view: AddCardViewProtocol, + interactor: AddCardInteractorProtocol, + database: DatabaseProtocol?, + navigator: AddCardNavigator, + viewModel: AddCardViewModel) { + self.view = view + self.interactor = interactor + self.database = database + self.navigator = navigator + self.viewModel = viewModel + } + + func fetchAllCards() { + view?.startLoading() + Task { [weak self] in + guard let self else { return } + + do { + let allCards = try await self.interactor.retrieveAllCards() + await MainActor.run { [weak self] in + self?.view?.stopLoading() + self?.view?.updateSearchList(allCards) + self?.cards = allCards + } + } catch { + await MainActor.run { + ToastMessages.showNetworkErrorMessage() + LoggerManager.shared.log(event: .allCards, parameters: ["error": error.localizedDescription]) + } + } + } + } + + func insert(card: CardDTO) { + switch viewModel.type { + case .lent: + insertToLentMe(card: card) + case .borrow: + insertToBorrowed(card: card) + case .collection: + insertToCollection(card: card) + } + } + + func doingSearch(_ query: String) { + view?.doingSearch(query) + } + + func cardDetailButtonTouched(with card: CardDTO) { + navigator.navigate(to: .cardDetail(database: database, with: cards, card: card)) + } + + // MARK: - Helpers + + private func insertToBorrowed(card: CardDTO) { + if let person = viewModel.person, !person.borrowed.contains(where: { $0.code == card.code }) { + try? database?.update { [weak self] in + person.borrowed.append(card) + self?.showSuccessMessage(card: card) + } + let personDataDict: [String: PersonDTO] = ["personDTO": person] + NotificationCenter.default.post(name: NotificationKey.reloadTableViewNotification, object: nil, userInfo: personDataDict) + } else { + ToastMessages.showInfoMessage(title: "", message: L10n.alreadyAdded) + } + } + + private func insertToLentMe(card: CardDTO) { + if let person = viewModel.person, !person.lentMe.contains(where: { $0.code == card.code }) { + try? database?.update { [weak self] in + person.lentMe.append(card) + self?.showSuccessMessage(card: card) + } + let personDataDict: [String: PersonDTO] = ["personDTO": person] + NotificationCenter.default.post(name: NotificationKey.reloadTableViewNotification, object: nil, userInfo: personDataDict) + } else { + ToastMessages.showInfoMessage(title: "", message: L10n.alreadyAdded) + } + } + + private func insertToCollection(card: CardDTO) { + if let userCollection = viewModel.userCollection, !userCollection.myCollection.contains(where: { $0.code == card.code }) { + try? database?.update { [weak self] in + userCollection.myCollection.append(card) + self?.showSuccessMessage(card: card) + } + } else { + ToastMessages.showInfoMessage(title: "", message: L10n.alreadyAdded) + } + } + + private func showSuccessMessage(card: CardDTO) { + LoadingHUD.show(.labeledSuccess(title: L10n.added, subtitle: card.name)) + } +} diff --git a/SWDestinyTrades/Classes/AddCard/View/AddCardView.swift b/SWDestinyTrades/Classes/AddCard/View/AddCardView.swift index 1370260e..f134923a 100644 --- a/SWDestinyTrades/Classes/AddCard/View/AddCardView.swift +++ b/SWDestinyTrades/Classes/AddCard/View/AddCardView.swift @@ -8,6 +8,13 @@ import UIKit +protocol AddCardViewProtocol: AnyObject { + func startLoading() + func stopLoading() + func updateSearchList(_ cards: [CardDTO]) + func doingSearch(_ query: String) +} + final class AddCardView: UIView, AddCardViewType { private let searchBar = SearchBar(frame: .zero) diff --git a/SWDestinyTrades/Classes/AddCard/ViewModel/AddCardViewModel.swift b/SWDestinyTrades/Classes/AddCard/ViewModel/AddCardViewModel.swift new file mode 100644 index 00000000..7afa895e --- /dev/null +++ b/SWDestinyTrades/Classes/AddCard/ViewModel/AddCardViewModel.swift @@ -0,0 +1,22 @@ +// +// AddCardViewModel.swift +// SWDestinyTrades +// +// Created by Diogo Autilio on 30/12/23. +// Copyright © 2023 Diogo Autilio. All rights reserved. +// + +import Foundation + +enum AddCardType { + case lent + case borrow + case collection +} + +struct AddCardViewModel { + + let person: PersonDTO? + let userCollection: UserCollectionDTO? + let type: AddCardType +} diff --git a/SWDestinyTrades/Classes/LoanDetail/Navigator/LoanDetailNavigator.swift b/SWDestinyTrades/Classes/LoanDetail/Navigator/LoanDetailNavigator.swift index 273e542e..eca848ce 100644 --- a/SWDestinyTrades/Classes/LoanDetail/Navigator/LoanDetailNavigator.swift +++ b/SWDestinyTrades/Classes/LoanDetail/Navigator/LoanDetailNavigator.swift @@ -36,7 +36,10 @@ final class LoanDetailNavigator: Navigator { case let .cardDetail(database, cardList, card): return CardDetailViewController(database: database, cardList: cardList, selected: card) case let .addCard(database, person, type): - return AddCardViewController(database: database, person: person, type: type) + return AddCardViewControllerFactory(database: database, + addCardType: type, + personDTO: person) + .createViewController() } } } diff --git a/SWDestinyTrades/Classes/UserCollection/Navigator/UserCollectionNavigator.swift b/SWDestinyTrades/Classes/UserCollection/Navigator/UserCollectionNavigator.swift index eb0f4f7c..79d48bdc 100644 --- a/SWDestinyTrades/Classes/UserCollection/Navigator/UserCollectionNavigator.swift +++ b/SWDestinyTrades/Classes/UserCollection/Navigator/UserCollectionNavigator.swift @@ -36,7 +36,10 @@ final class UserCollectionNavigator: Navigator { case let .cardDetail(database, cardList, card): return CardDetailViewController(database: database, cardList: cardList, selected: card) case let .addCard(database, userCollection): - return AddCardViewController(database: database, userCollection: userCollection, type: .collection) + return AddCardViewControllerFactory(database: database, + addCardType: .collection, + userCollectionDTO: userCollection) + .createViewController() } } } diff --git a/SWDestinyTradesTests/Screens/AddCard/Controller/AddCardViewControllerTests.swift b/SWDestinyTradesTests/Screens/AddCard/Controller/AddCardViewControllerTests.swift index 6296181d..2b5d784b 100644 --- a/SWDestinyTradesTests/Screens/AddCard/Controller/AddCardViewControllerTests.swift +++ b/SWDestinyTradesTests/Screens/AddCard/Controller/AddCardViewControllerTests.swift @@ -27,17 +27,16 @@ final class AddCardViewControllerTests: XCTestCase { database = RealmDatabaseHelper.createMemoryDatabase(identifier: "UserCollection") service = SWDestinyService(client: HttpClientMock()) view = AddCardViewSpy() - sut = AddCardViewController(with: view, - service: service, - database: database, - person: .stub(), - userCollection: .stub(), - type: .collection) + sut = createSUT(database: database, person: .stub(), userCollection: .stub(), type: .collection) let navigationController = UINavigationController(rootViewController: sut) keyWindow.showTestWindow(controller: navigationController) } override func tearDown() { + database = nil + service = nil + view = nil + sut = nil keyWindow.cleanTestWindow() super.tearDown() } @@ -57,13 +56,7 @@ final class AddCardViewControllerTests: XCTestCase { func testDidSelectCardInsertsIntoCollectionDatabaseSuccessfully() { let collection = UserCollectionDTO.stub() - - sut = AddCardViewController(with: view, - service: service, - database: database, - person: .stub(), - userCollection: collection, - type: .collection) + sut = createSUT(database: database, person: .stub(), userCollection: collection, type: .collection) let navigationController = UINavigationController(rootViewController: sut) keyWindow.showTestWindow(controller: navigationController) @@ -76,13 +69,7 @@ final class AddCardViewControllerTests: XCTestCase { func testDidSelectCardDoesNotInsertIntoCollectionDatabase() { let collection = UserCollectionDTO.stub() collection.addCard(.stub()) - - sut = AddCardViewController(with: view, - service: service, - database: database, - person: .stub(), - userCollection: collection, - type: .collection) + sut = createSUT(database: database, person: .stub(), userCollection: collection, type: .collection) let navigationController = UINavigationController(rootViewController: sut) keyWindow.showTestWindow(controller: navigationController) @@ -107,4 +94,23 @@ final class AddCardViewControllerTests: XCTestCase { XCTAssertEqual(view.didCallDoingSearch.count, 1) XCTAssertEqual(view.didCallDoingSearch[0], "jabba") } + + // MARK: - Helpers + + func createSUT(database: DatabaseProtocol, + person: PersonDTO? = nil, + userCollection: UserCollectionDTO? = nil, + type: AddCardType) -> AddCardViewController { + let viewController = AddCardViewController(with: view) + let router = AddCardNavigator(viewController) + let interactor = AddCardInteractor(service: service) + let viewModel = AddCardViewModel(person: person, userCollection: userCollection, type: type) + let presenter = AddCardPresenter(view: viewController, + interactor: interactor, + database: database, + navigator: router, + viewModel: viewModel) + viewController.presenter = presenter + return viewController + } } diff --git a/SWDestinyTradesTests/Screens/AddCard/Navigator/AddCardNavigatorTests.swift b/SWDestinyTradesTests/Screens/AddCard/Navigator/AddCardNavigatorTests.swift index 21505b74..b897adf3 100644 --- a/SWDestinyTradesTests/Screens/AddCard/Navigator/AddCardNavigatorTests.swift +++ b/SWDestinyTradesTests/Screens/AddCard/Navigator/AddCardNavigatorTests.swift @@ -16,13 +16,18 @@ final class AddCardNavigatorTests: XCTestCase { private var sut: AddCardNavigator! private var navigationController: UINavigationControllerMock! - private var panda: UIViewController! override func setUp() { super.setUp() - panda = UIViewController() - navigationController = UINavigationControllerMock(rootViewController: panda) - sut = AddCardNavigator(navigationController) + let controller = UIViewController() + navigationController = UINavigationControllerMock(rootViewController: controller) + sut = AddCardNavigator(controller) + } + + override func tearDown() { + navigationController = nil + sut = nil + super.tearDown() } func testNavigateToCardDetailPushesToCardDetailViewController() { diff --git a/SWDestinyTradesTests/Screens/AddCard/View/AddCardViewControllerSnapshotTests.swift b/SWDestinyTradesTests/Screens/AddCard/View/AddCardViewControllerSnapshotTests.swift index 5ace0f9a..ff10507a 100644 --- a/SWDestinyTradesTests/Screens/AddCard/View/AddCardViewControllerSnapshotTests.swift +++ b/SWDestinyTradesTests/Screens/AddCard/View/AddCardViewControllerSnapshotTests.swift @@ -26,12 +26,14 @@ final class AddCardViewTests: XCSnapshotableTestCase { } override func tearDown() { + service = nil + sut = nil window.cleanTestWindow() super.tearDown() } func testLayoutWhenIsLentMeIsTrue() { - sut = AddCardViewController(service: service, database: nil, person: .stub(), type: .lent) + sut = createSUT(person: .stub(), type: .lent) navigation = UINavigationController(rootViewController: sut) window.showTestWindow(controller: navigation) @@ -39,10 +41,26 @@ final class AddCardViewTests: XCSnapshotableTestCase { } func testLayoutWhenIsLentMeIsFalse() { - sut = AddCardViewController(service: service, database: nil, person: .stub(), type: .borrow) + sut = createSUT(person: .stub(), type: .borrow) navigation = UINavigationController(rootViewController: sut) window.showTestWindow(controller: navigation) XCTAssertTrue(snapshot(navigation, named: "AddCardViewController layout when isLentMe is false")) } + + // MARK: - Helpers + + func createSUT(person: PersonDTO? = nil, userCollection: UserCollectionDTO? = nil, type: AddCardType) -> AddCardViewController { + let viewController = AddCardViewController() + let router = AddCardNavigator(viewController) + let interactor = AddCardInteractor(service: service) + let viewModel = AddCardViewModel(person: person, userCollection: userCollection, type: type) + let presenter = AddCardPresenter(view: viewController, + interactor: interactor, + database: nil, + navigator: router, + viewModel: viewModel) + viewController.presenter = presenter + return viewController + } }