diff --git a/ContactManager/ContactManager.xcodeproj/project.pbxproj b/ContactManager/ContactManager.xcodeproj/project.pbxproj index 416508b2..16e35085 100644 --- a/ContactManager/ContactManager.xcodeproj/project.pbxproj +++ b/ContactManager/ContactManager.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ F42BF4772B54CA9C0067C8E8 /* ContactValidationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F42BF4762B54CA9C0067C8E8 /* ContactValidationError.swift */; }; F42BF4792B54CB6C0067C8E8 /* AddContactError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F42BF4782B54CB6C0067C8E8 /* AddContactError.swift */; }; F42BF47B2B54CC620067C8E8 /* AddContact.swift in Sources */ = {isa = PBXBuildFile; fileRef = F42BF47A2B54CC620067C8E8 /* AddContact.swift */; }; + F44184BE2B594C3D0011CB5B /* ContactUnavailableConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F44184BD2B594C3D0011CB5B /* ContactUnavailableConfiguration.swift */; }; F456EA732B49511C001BE636 /* Contact.swift in Sources */ = {isa = PBXBuildFile; fileRef = F456EA722B49511C001BE636 /* Contact.swift */; }; F456EA762B4951E9001BE636 /* ContactList.swift in Sources */ = {isa = PBXBuildFile; fileRef = F456EA752B4951E9001BE636 /* ContactList.swift */; }; F456EA7A2B4954E4001BE636 /* ListContactUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = F456EA792B4954E4001BE636 /* ListContactUseCase.swift */; }; @@ -49,6 +50,7 @@ F42BF4762B54CA9C0067C8E8 /* ContactValidationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactValidationError.swift; sourceTree = ""; }; F42BF4782B54CB6C0067C8E8 /* AddContactError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactError.swift; sourceTree = ""; }; F42BF47A2B54CC620067C8E8 /* AddContact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContact.swift; sourceTree = ""; }; + F44184BD2B594C3D0011CB5B /* ContactUnavailableConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactUnavailableConfiguration.swift; sourceTree = ""; }; F456EA722B49511C001BE636 /* Contact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contact.swift; sourceTree = ""; }; F456EA752B4951E9001BE636 /* ContactList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactList.swift; sourceTree = ""; }; F456EA792B4954E4001BE636 /* ListContactUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListContactUseCase.swift; sourceTree = ""; }; @@ -137,6 +139,7 @@ F4B1787B2B539320005B8E78 /* Formatter.swift */, F4B1788A2B53F0B1005B8E78 /* ContactMakable.swift */, F42BF4762B54CA9C0067C8E8 /* ContactValidationError.swift */, + F44184BD2B594C3D0011CB5B /* ContactUnavailableConfiguration.swift */, ); path = Common; sourceTree = ""; @@ -392,6 +395,7 @@ F456EA902B497E16001BE636 /* ContactRepository.swift in Sources */, F4B178892B53C095005B8E78 /* AddContactCoordinator.swift in Sources */, F456EA8A2B495C14001BE636 /* ContactListDataSource.swift in Sources */, + F44184BE2B594C3D0011CB5B /* ContactUnavailableConfiguration.swift in Sources */, F4B178702B529351005B8E78 /* AddContactViewController.swift in Sources */, F456EA762B4951E9001BE636 /* ContactList.swift in Sources */, F456EA8C2B495E13001BE636 /* ReusableCell.swift in Sources */, diff --git a/ContactManager/ContactManager/Common/ContactMakable.swift b/ContactManager/ContactManager/Common/ContactMakable.swift index 20088280..250cfcd3 100644 --- a/ContactManager/ContactManager/Common/ContactMakable.swift +++ b/ContactManager/ContactManager/Common/ContactMakable.swift @@ -6,15 +6,31 @@ // protocol ContactMakable { - func makeContact(from request: AddContact.Request) throws -> Contact + func makeContact(from request: AddContact.CreatContact.Request) throws -> Contact + func makeExistingContact(from request: AddContact.UpdateContact.Request) throws -> Contact } +import Foundation + struct ContactFactory: ContactMakable { - func makeContact(from request: AddContact.Request) throws -> Contact { + func makeContact(from request: AddContact.CreatContact.Request) throws -> Contact { + let id = makeID() + let name = try validateName(request.name) + let age = try validateAge(request.age) + let phoneNumber = try validatePhoneNumber(request.phoneNumber) + return Contact(id: id, name: name, phoneNumber: phoneNumber, age: age) + } + + func makeExistingContact(from request: AddContact.UpdateContact.Request) throws -> Contact { + let id = request.id let name = try validateName(request.name) let age = try validateAge(request.age) let phoneNumber = try validatePhoneNumber(request.phoneNumber) - return Contact(name: name, phoneNumber: phoneNumber, age: age) + return Contact(id: id, name: name, phoneNumber: phoneNumber, age: age) + } + + private func makeID() -> Int { + return UUID().hashValue } private func validateName(_ name: String) throws -> String { diff --git a/ContactManager/ContactManager/Common/ContactUnavailableConfiguration.swift b/ContactManager/ContactManager/Common/ContactUnavailableConfiguration.swift new file mode 100644 index 00000000..40d5885d --- /dev/null +++ b/ContactManager/ContactManager/Common/ContactUnavailableConfiguration.swift @@ -0,0 +1,25 @@ +// +// ContactUnavailableConfiguration.swift +// ContactManager +// +// Created by Effie on 1/18/24. +// + +import UIKit + +enum ContactUnavailableConfiguration { + static let noContacts: UIContentUnavailableConfiguration = { + var config = UIContentUnavailableConfiguration.empty() + config.image = UIImage(systemName: "person.crop.circle") + config.text = "저장된 연락처 없음" + config.secondaryText = "저장된 연락처 목록이 여기 표시됩니다." + return config + }() + + static let noSearchingResults: UIContentUnavailableConfiguration = { + var config = UIContentUnavailableConfiguration.search() + config.text = "검색 결과 없음" + config.secondaryText = "조건과 일치하는 검색 결과가 없습니다." + return config + }() +} diff --git a/ContactManager/ContactManager/Data/ContactRepository.swift b/ContactManager/ContactManager/Data/ContactRepository.swift index 25b33ed8..de8e1275 100644 --- a/ContactManager/ContactManager/Data/ContactRepository.swift +++ b/ContactManager/ContactManager/Data/ContactRepository.swift @@ -8,9 +8,17 @@ import Foundation protocol ContactRepository { + func requestContact(id: Int) throws -> Contact + func requestContacts() throws -> [Contact] func addContact(_ newContact: Contact) throws + + func removeContact(contactID: Int) throws + + func searchContact(with queries: [String]) throws -> [Contact] + + func updateContact(with updatedContact: Contact) throws } struct ContactRepositoryImpl: ContactRepository { @@ -28,13 +36,67 @@ struct ContactRepositoryImpl: ContactRepository { self.fileProvider = fileProvider } + func requestContact(id: Int) throws -> Contact { + do { + return try self.contactList.getContact(id: id) + } catch ContactListError.invalidID { + throw ContactRepositoryError.notFound + } + } + func requestContacts() throws -> [Contact] { - return self.contactList.getContacts() + let contacts = self.contactList.getContacts() + guard contacts.isEmpty == false else { throw ContactRepositoryError.noContacts } + return contacts } func addContact(_ newContact: Contact) throws { self.contactList.addContact(newContact) } + + func removeContact(contactID: Int) throws { + do { + try self.contactList.deleteContact(contactID: contactID) + } catch ContactListError.invalidIndex { + throw ContactRepositoryError.cannotRemove + } catch { + throw error + } + } + + func searchContact(with queries: [String]) throws -> [Contact] { + let contacts = self.contactList.getContacts() + var matches = contacts + for query in queries { + matches = matches.filter { contact in self.match(query, to: contact) } + } + guard matches.isEmpty == false else { throw ContactRepositoryError.noSearchingResult } + return matches + } + + func updateContact(with updatedContact: Contact) throws { + do { + try self.contactList.updateContact(with: updatedContact) + } catch { + throw ContactRepositoryError.cannotUpdate + } + } + + private func match(_ query: String, to contact: Contact) -> Bool { + // 이름에 쿼리가 포함되는지 확인 + var result = contact.name.localizedCaseInsensitiveContains(query) + + // 숫자로 바꿀 수 있는 쿼리라면 나이와 같은지 확인 + if let number = Int(query) { + result = result || (contact.age == number) + } + + // 하이픈을 제외한 전화번호와 일치하는 부분이 있는지 + let purePhoneNumberString = contact.phoneNumber.filter { ch in ch.isNumber } + result = result || (purePhoneNumberString.localizedStandardContains(query)) + + return result + } } extension ContactRepositoryImpl { diff --git a/ContactManager/ContactManager/Data/ContactRepositoryError.swift b/ContactManager/ContactManager/Data/ContactRepositoryError.swift index 4f31392b..fc8a3b70 100644 --- a/ContactManager/ContactManager/Data/ContactRepositoryError.swift +++ b/ContactManager/ContactManager/Data/ContactRepositoryError.swift @@ -11,6 +11,12 @@ enum ContactRepositoryError: LocalizedError { case notFoundAtBundle case cannotDecode + case noContacts + case cannotRemove + case noSearchingResult + case notFound + case cannotUpdate + var errorDescription: String? { var description: String = "\(String(describing: Self.self)).\(String(describing: self)): " switch self { @@ -18,6 +24,16 @@ enum ContactRepositoryError: LocalizedError { description += "요청된 파일을 번들에서 찾을 수 없음" case .cannotDecode: description += "요청된 타입으로 디코딩 실패" + case .cannotRemove: + description += "인덱스 문제로 삭제 실패" + case .noContacts: + description += "연락처 데이터 없음" + case .noSearchingResult: + description += "조건에 맞는 연락처 없음" + case .notFound: + description += "전달한 ID의 연락처 없음" + case .cannotUpdate: + description += "ID 문제로 업데이트 실패" } return description } @@ -30,6 +46,16 @@ extension ContactRepositoryError: AlertableError { return .init(body: "표시할 연락처가 없어요.") case .cannotDecode: return .init(body: "데이터를 알맞은 형태로 바꿔줄 수 없어요.") + case .cannotRemove: + return .init(body: "선택한 연락처를 삭제할 수 없어요.") + case .noContacts: + return .init(body: "저장된 연락처가 없어요.") + case .noSearchingResult: + return .init(body: "검색 결과가 없어요.") + case .notFound: + return .init(body: "선택한 연락처가 존재하지 않아요.") + case .cannotUpdate: + return .init(body: "편집한 내용을 저장할 수 없어요.") } } } diff --git a/ContactManager/ContactManager/Domain/AddContact/AddContact.swift b/ContactManager/ContactManager/Domain/AddContact/AddContact.swift index 4d8302db..ddbb9c2b 100644 --- a/ContactManager/ContactManager/Domain/AddContact/AddContact.swift +++ b/ContactManager/ContactManager/Domain/AddContact/AddContact.swift @@ -6,9 +6,26 @@ // enum AddContact { - struct Request { - let name: String - let age: String - let phoneNumber: String + enum CreatContact { + struct Request { + let name: String + let age: String + let phoneNumber: String + } + } + + enum FetchContact { + struct Request { + let id: Int + } + } + + enum UpdateContact { + struct Request { + let id: Int + let name: String + let age: String + let phoneNumber: String + } } } diff --git a/ContactManager/ContactManager/Domain/AddContact/AddContactUseCase.swift b/ContactManager/ContactManager/Domain/AddContact/AddContactUseCase.swift index 201b8736..9a175882 100644 --- a/ContactManager/ContactManager/Domain/AddContact/AddContactUseCase.swift +++ b/ContactManager/ContactManager/Domain/AddContact/AddContactUseCase.swift @@ -20,7 +20,17 @@ struct AddContactUseCase { self.factory = factory } - func saveNewContact(request: AddContact.Request) { + func fetchContact(request: AddContact.FetchContact.Request) { + do { + let id = request.id + let contact = try self.repository.requestContact(id: id) + presenter?.presentFetchContact(result: .success(contact)) + } catch { + presenter?.presentFetchContact(result: .failure(error)) + } + } + + func saveNewContact(request: AddContact.CreatContact.Request) { do { let contact = try factory.makeContact(from: request) try repository.addContact(contact) @@ -30,7 +40,17 @@ struct AddContactUseCase { } } - func confirmCancel(request: AddContact.Request) { + func updateNewContact(request: AddContact.UpdateContact.Request) { + do { + let contact = try factory.makeExistingContact(from: request) + try repository.updateContact(with: contact) + presenter?.presentUpdateContact(result: .success(())) + } catch { + presenter?.presentUpdateContact(result: .failure(error)) + } + } + + func confirmCancel(request: AddContact.CreatContact.Request) { do { try confirmIfCancellable(request: request) presenter?.presentCancelConfirmation(result: .success(())) @@ -39,7 +59,7 @@ struct AddContactUseCase { } } - private func confirmIfCancellable(request: AddContact.Request) throws { + private func confirmIfCancellable(request: AddContact.CreatContact.Request) throws { guard request.name.isEmpty, request.age.isEmpty, request.phoneNumber.isEmpty else { @@ -48,9 +68,9 @@ struct AddContactUseCase { } } -import Foundation - -protocol AddContactPresentable: NSObjectProtocol { +protocol AddContactPresentable: AnyObject { + func presentFetchContact(result: Result) func presentAddContact(result: Result) func presentCancelConfirmation(result: Result) + func presentUpdateContact(result: Result) } diff --git a/ContactManager/ContactManager/Domain/ListContact/ListContactUseCase.swift b/ContactManager/ContactManager/Domain/ListContact/ListContactUseCase.swift index 592ec749..3265d627 100644 --- a/ContactManager/ContactManager/Domain/ListContact/ListContactUseCase.swift +++ b/ContactManager/ContactManager/Domain/ListContact/ListContactUseCase.swift @@ -23,10 +23,31 @@ struct ListContactUseCase { presenter?.presentListContact(result: .failure(error)) } } + + func deleteContact(contactID: Int) { + do { + try repository.removeContact(contactID: contactID) + presenter?.presentDeleteContact(result: .success(())) + } catch { + presenter?.presentDeleteContact(result: .failure(error)) + } + } + + func searchContact(with query: String) { + let trimmedQuery = query.trimmingCharacters(in: .whitespacesAndNewlines) + let queries = trimmedQuery.components(separatedBy: .whitespacesAndNewlines) + do { + let matchingContacts = try repository.searchContact(with: queries) + let successInfo = ListContact.SuccessInfo(contacts: matchingContacts) + presenter?.presentSearchContact(result: .success(successInfo)) + } catch { + presenter?.presentSearchContact(result: .failure(error)) + } + } } -import Foundation - -protocol ListContactPresentable: NSObjectProtocol { +protocol ListContactPresentable: AnyObject { func presentListContact(result: Result) + func presentDeleteContact(result: Result) + func presentSearchContact(result: Result) } diff --git a/ContactManager/ContactManager/Domain/Model/Contact.swift b/ContactManager/ContactManager/Domain/Model/Contact.swift index 4438c09a..d864a6fd 100644 --- a/ContactManager/ContactManager/Domain/Model/Contact.swift +++ b/ContactManager/ContactManager/Domain/Model/Contact.swift @@ -6,6 +6,8 @@ // struct Contact: Hashable, Decodable { + let id: Int + let name: String let phoneNumber: String diff --git a/ContactManager/ContactManager/Domain/Model/ContactList.swift b/ContactManager/ContactManager/Domain/Model/ContactList.swift index 90d224e5..fb8ec404 100644 --- a/ContactManager/ContactManager/Domain/Model/ContactList.swift +++ b/ContactManager/ContactManager/Domain/Model/ContactList.swift @@ -14,6 +14,13 @@ final class ContactList { self.contacts = contacts } + func getContact(id: Int) throws -> Contact { + guard let contact = self.contacts.first(where: { contact in contact.id == id }) else { + throw ContactListError.invalidID + } + return contact + } + func getContacts() -> [Contact] { return self.contacts } @@ -26,19 +33,22 @@ final class ContactList { self.contacts.insert(newContact, at: 0) } - func deleteContact(at index: Index) throws { - guard validateIndex(index) else { throw ContactListError.invalidIndex } + func deleteContact(contactID: Int) throws { + let index = try getIndexofContact(id: contactID) self.contacts.remove(at: index) } - func updateContact(at index: Index, with newContact: Contact) throws { - guard validateIndex(index) else { throw ContactListError.invalidIndex } + func updateContact(with newContact: Contact) throws { + let index = try getIndexofContact(id: newContact.id) self.contacts[index] = newContact } } extension ContactList { - private func validateIndex(_ index: Index) -> Bool { - return self.contacts.indices.contains(index) + private func getIndexofContact(id: Int) throws -> Index { + guard let index = self.contacts.firstIndex(where: { contact in contact.id == id }) else { + throw ContactListError.invalidID + } + return index } } diff --git a/ContactManager/ContactManager/Domain/Model/ContactListError.swift b/ContactManager/ContactManager/Domain/Model/ContactListError.swift index bcb6f845..bfb8ac6e 100644 --- a/ContactManager/ContactManager/Domain/Model/ContactListError.swift +++ b/ContactManager/ContactManager/Domain/Model/ContactListError.swift @@ -7,4 +7,5 @@ enum ContactListError: Error { case invalidIndex + case invalidID } diff --git a/ContactManager/ContactManager/Presentation/AddContact/View/InputView.swift b/ContactManager/ContactManager/Presentation/AddContact/View/InputView.swift index fd060b41..2d41a9d8 100644 --- a/ContactManager/ContactManager/Presentation/AddContact/View/InputView.swift +++ b/ContactManager/ContactManager/Presentation/AddContact/View/InputView.swift @@ -71,6 +71,10 @@ final class InputView: UIView { self.textField.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 0.8), ]) } + + func configure(with content: String) { + self.textField.text = content + } } extension InputView: UITextFieldDelegate { diff --git a/ContactManager/ContactManager/Presentation/AddContact/ViewController/AddContactViewController.swift b/ContactManager/ContactManager/Presentation/AddContact/ViewController/AddContactViewController.swift index fde8a04b..bbe2aefd 100644 --- a/ContactManager/ContactManager/Presentation/AddContact/ViewController/AddContactViewController.swift +++ b/ContactManager/ContactManager/Presentation/AddContact/ViewController/AddContactViewController.swift @@ -8,12 +8,12 @@ import UIKit final class AddContactViewController: UIViewController { - private static let title = "새 연락처" - private var addContactUseCase: AddContactUseCase private weak var coordinator: AddContactViewControllerDelegate? + private let contactId: Int? + private let nameField = InputView(fieldName: "이름", keyboardType: .default) { input in var formattedName = input if input.contains(where: { $0 == " " }) { @@ -88,9 +88,11 @@ final class AddContactViewController: UIViewController { } init( + contactId: Int?, useCase: AddContactUseCase, coordinator: AddContactViewControllerDelegate ) { + self.contactId = contactId self.addContactUseCase = useCase self.coordinator = coordinator super.init(nibName: nil, bundle: nil) @@ -100,19 +102,30 @@ final class AddContactViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() setupViews() + setUpFields() } private func didTapSaveButton() { - let request = AddContact.Request( - name: self.nameField.currentValue, - age: self.ageField.currentValue, - phoneNumber: self.phoneNumberField.currentValue - ) - self.addContactUseCase.saveNewContact(request: request) + if let id = self.contactId { + let request = AddContact.UpdateContact.Request( + id: id, + name: self.nameField.currentValue, + age: self.ageField.currentValue, + phoneNumber: self.phoneNumberField.currentValue + ) + self.addContactUseCase.updateNewContact(request: request) + } else { + let request = AddContact.CreatContact.Request( + name: self.nameField.currentValue, + age: self.ageField.currentValue, + phoneNumber: self.phoneNumberField.currentValue + ) + self.addContactUseCase.saveNewContact(request: request) + } } private func didTapCancelButton() { - let request = AddContact.Request( + let request = AddContact.CreatContact.Request( name: self.nameField.currentValue, age: self.ageField.currentValue, phoneNumber: self.phoneNumberField.currentValue @@ -122,7 +135,7 @@ final class AddContactViewController: UIViewController { private func setupViews() { self.view.backgroundColor = .systemBackground - self.title = Self.title + self.title = (contactId == nil) ? "새 연락처" : "연락처 편집" self.navigationItem.leftBarButtonItem = cancelButton self.navigationItem.rightBarButtonItem = saveButton @@ -133,9 +146,26 @@ final class AddContactViewController: UIViewController { fieldStack.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor, constant: -16), ]) } + + private func setUpFields() { + guard let id = self.contactId else { return } + let request = AddContact.FetchContact.Request(id: id) + self.addContactUseCase.fetchContact(request: request) + } } extension AddContactViewController: AddContactPresentable { + func presentFetchContact(result: Result) { + switch result { + case .success(let contact): + self.nameField.configure(with: contact.name) + self.ageField.configure(with: "\(contact.age)") + self.phoneNumberField.configure(with: contact.phoneNumber) + case .failure(let error): + handleError(error) + } + } + func presentAddContact(result: Result) { switch result { case .success: @@ -145,6 +175,15 @@ extension AddContactViewController: AddContactPresentable { } } + func presentUpdateContact(result: Result) { + switch result { + case .success: + self.coordinator?.endAddContact() + case .failure(let error): + handleError(error) + } + } + func presentCancelConfirmation(result: Result) { switch result { case .success: diff --git a/ContactManager/ContactManager/Presentation/ListContact/ViewController/ListContactViewController.swift b/ContactManager/ContactManager/Presentation/ListContact/ViewController/ListContactViewController.swift index b6019cfe..e31f9843 100644 --- a/ContactManager/ContactManager/Presentation/ListContact/ViewController/ListContactViewController.swift +++ b/ContactManager/ContactManager/Presentation/ListContact/ViewController/ListContactViewController.swift @@ -8,6 +8,12 @@ import UIKit final class ListContactViewController: UIViewController { + enum ListState { + case noContacts + case noSearchingResults + case noProblem + } + private var listContactUseCase: ListContactUseCase? private weak var coordinator: ListContactViewControllerDelegate? @@ -20,6 +26,14 @@ final class ListContactViewController: UIViewController { private lazy var contactListDataSource: ContactListDataSource = ContactListDataSource(self.contactListView) + private var searchController: UISearchController? + + private var listIsEmpty: ListState = .noProblem { + didSet { + setNeedsUpdateContentUnavailableConfiguration() + } + } + @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") @@ -33,6 +47,7 @@ final class ListContactViewController: UIViewController { self.coordinator = coordinator super.init(nibName: nil, bundle: nil) self.listContactUseCase?.presenter = self + self.contactListView.delegate = self } override func viewDidLoad() { @@ -40,6 +55,20 @@ final class ListContactViewController: UIViewController { setupViews() self.listContactUseCase?.fetchAllContacts() } + + override func updateContentUnavailableConfiguration(using state: UIContentUnavailableConfigurationState) { + UIView.animate(withDuration: 0.3) { [weak self] in + guard let self else { return } + switch self.listIsEmpty { + case .noContacts: + self.contentUnavailableConfiguration = ContactUnavailableConfiguration.noContacts + case .noSearchingResults: + self.contentUnavailableConfiguration = ContactUnavailableConfiguration.noSearchingResults + case .noProblem: + self.contentUnavailableConfiguration = nil + } + } + } } extension ListContactViewController { @@ -56,6 +85,7 @@ extension ListContactViewController { contactListView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), ]) setButtons() + setSearchController() } private func setButtons() { @@ -64,9 +94,49 @@ extension ListContactViewController { self.navigationItem.rightBarButtonItem = button } + private func setSearchController() { + self.searchController = UISearchController(searchResultsController: nil) + self.searchController?.searchResultsUpdater = self + navigationItem.searchController = searchController + } + private func didTapCreateButton() { self.coordinator?.startAddContact() } + + private func handle(error: Error) { + if let error = error as? LocalizedError { + print(error.localizedDescription) + } + if let error = error as? AlertableError { + showErrorAlert(error: error) + } + } +} + +extension ListContactViewController: UITableViewDelegate { + func tableView( + _ tableView: UITableView, + trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath + ) -> UISwipeActionsConfiguration? { + guard let listItem = contactListDataSource.itemIdentifier(for: indexPath) else { return nil } + switch listItem { + case .contact(let contact): + let deleteAction = UIContextualAction(style: .destructive, title: "Delete") { _, _, _ in + self.listContactUseCase?.deleteContact(contactID: contact.id) + } + return UISwipeActionsConfiguration(actions: [deleteAction]) + } + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + guard let listItem = contactListDataSource.itemIdentifier(for: indexPath) else { return } + switch listItem { + case .contact(let contact): + self.coordinator?.startUpdateContact(contactID: contact.id) + } + } } extension ListContactViewController: ListContactPresentable { @@ -75,15 +145,36 @@ extension ListContactViewController: ListContactPresentable { snapshot.appendSections([.contact]) switch result { case .success(let successInfo): - let contacts = successInfo.contacts.map(ContactListItem.contact) + self.listIsEmpty = .noProblem + let contacts = successInfo.contacts.map(ContactListItem.contact) snapshot.appendItems(contacts, toSection: .contact) case .failure(let error): - if let error = error as? LocalizedError { - print(error.localizedDescription) - } - if let error = error as? AlertableError { - showErrorAlert(error: error) - } + self.listIsEmpty = .noContacts + handle(error: error) + } + self.contactListDataSource.apply(snapshot) + } + + func presentDeleteContact(result: Result) { + switch result { + case .success: + self.listContactUseCase?.fetchAllContacts() + case .failure(let error): + handle(error: error) + } + } + + func presentSearchContact(result: Result) { + var snapshot = ContactListSnapShot() + snapshot.appendSections([.contact]) + switch result { + case .success(let successInfo): + self.listIsEmpty = .noProblem + let contacts = successInfo.contacts.map(ContactListItem.contact) + snapshot.appendItems(contacts, toSection: .contact) + case .failure(let error): + self.listIsEmpty = .noSearchingResults + handle(error: error) } self.contactListDataSource.apply(snapshot) } @@ -110,3 +201,13 @@ extension ListContactViewController: ModalViewControllerDismissingHandlable { self.listContactUseCase?.fetchAllContacts() } } + +extension ListContactViewController: UISearchResultsUpdating { + func updateSearchResults(for searchController: UISearchController) { + guard let query = searchController.searchBar.text, query.isEmpty == false else { + self.listContactUseCase?.fetchAllContacts() + return + } + self.listContactUseCase?.searchContact(with: query) + } +} diff --git a/ContactManager/ContactManager/Routing/AddContactCoordinator.swift b/ContactManager/ContactManager/Routing/AddContactCoordinator.swift index 2be995a1..613c48ce 100644 --- a/ContactManager/ContactManager/Routing/AddContactCoordinator.swift +++ b/ContactManager/ContactManager/Routing/AddContactCoordinator.swift @@ -11,15 +11,19 @@ final class AddContactCoordinator: Coordinator { var childCoordinators: [Coordinator] = [] var parentCoordinator: AddContactCoordinatorDelegate? + private let contactID: Int? + private let navigationController: UINavigationController private let contactRepository: ContactRepository init( navigationController: UINavigationController, - contactRepository: ContactRepository + contactRepository: ContactRepository, + contactID: Int? ) { self.navigationController = navigationController self.contactRepository = contactRepository + self.contactID = contactID } func start() { @@ -28,11 +32,17 @@ final class AddContactCoordinator: Coordinator { factory: ContactFactory() ) let addContactViewController = AddContactViewController( + contactId: self.contactID, useCase: useCase, coordinator: self ) - let destinationViewController = UINavigationController(rootViewController: addContactViewController) - self.navigationController.present(destinationViewController, animated: true) + + if let contactID { + self.navigationController.pushViewController(addContactViewController, animated: true) + } else { + let destinationViewController = UINavigationController(rootViewController: addContactViewController) + self.navigationController.present(destinationViewController, animated: true) + } } } @@ -40,17 +50,24 @@ final class AddContactCoordinator: Coordinator { protocol AddContactViewControllerDelegate: AnyObject { func endAddContact() - func cancelAddContact() } extension AddContactCoordinator: AddContactViewControllerDelegate { func endAddContact() { - self.navigationController.dismiss(animated: true) + if self.contactID == nil { + self.navigationController.dismiss(animated: true) + } else { + self.navigationController.popViewController(animated: true) + } self.parentCoordinator?.didEndAddContact(self) } func cancelAddContact() { - self.navigationController.dismiss(animated: true) + if self.contactID == nil { + self.navigationController.dismiss(animated: true) + } else { + self.navigationController.popViewController(animated: true) + } } } diff --git a/ContactManager/ContactManager/Routing/ListContactCoordinator.swift b/ContactManager/ContactManager/Routing/ListContactCoordinator.swift index 96c3ea39..cf286428 100644 --- a/ContactManager/ContactManager/Routing/ListContactCoordinator.swift +++ b/ContactManager/ContactManager/Routing/ListContactCoordinator.swift @@ -33,13 +33,27 @@ final class ListContactCoordinator: Coordinator { protocol ListContactViewControllerDelegate: AnyObject { func startAddContact() + + func startUpdateContact(contactID: Int) } extension ListContactCoordinator: ListContactViewControllerDelegate { func startAddContact() { let coordinator = AddContactCoordinator( navigationController: self.navigationController, - contactRepository: self.contactRepository + contactRepository: self.contactRepository, + contactID: nil + ) + coordinator.parentCoordinator = self + coordinator.start() + self.childCoordinators.append(coordinator) + } + + func startUpdateContact(contactID: Int) { + let coordinator = AddContactCoordinator( + navigationController: self.navigationController, + contactRepository: self.contactRepository, + contactID: contactID ) coordinator.parentCoordinator = self coordinator.start()