-
Notifications
You must be signed in to change notification settings - Fork 36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
연락처 관리 앱 [STEP 3] Hong, Effie #89
base: d_Effie
Are you sure you want to change the base?
Changes from 15 commits
17f86b2
37f4f3a
16a961d
11e175f
0ec0abb
ec093bc
835ae1e
fcfd0ab
25dfabf
364d03f
f3252ab
f4e8c5d
28007e1
ef44e5e
7cfe5e2
0c722f0
b507657
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
}() | ||
} | ||
Comment on lines
+10
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 여기 생성해둔 객체들은 전역에서 접근이 가능한데요. 화면 내부에서 접근하도록 만드는 방법이 나았을지 궁금합니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이런 식으로 접근할 때마다 생성하는 방식도 있을 것 같습니다. 어느 편을 더 선호하시나요? enum Configuration {
case noContacts
case noSearchingResults
private var text: String {
switch self {
case .noContacts:
return "저장된 연락처 없음"
case .noSearchingResults:
return "검색 결과 없음"
}
}
private var secondaryText: String {
switch self {
case .noContacts:
return "저장된 연락처 목록이 여기 표시됩니다."
case .noSearchingResults:
return "조건과 일치하는 검색 결과가 없습니다."
}
}
private var image: UIImage? {
switch self {
case .noContacts:
return UIImage(systemName: "person.crop.circle")
case .noSearchingResults:
return nil
}
}
var configuration: UIContentUnavailableConfiguration {
switch self {
case .noContacts:
return UIContentUnavailableConfiguration.empty()
case .noSearchingResults:
return UIContentUnavailableConfiguration.search()
}
}
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
} | ||
Comment on lines
47
to
51
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 연락처 목록이 비어 있는 상태에 대응하기 위해 에러를 던지도록 구현을 수정했습니다! |
||
|
||
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 | ||
} | ||
Comment on lines
+85
to
+99
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 코드를 리뷰하는 시점에서 보니 repository의 역할과 어울리지 않는 듯한 느낌이 있네요. |
||
} | ||
|
||
extension ContactRepositoryImpl { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
} | ||
} | ||
} | ||
Comment on lines
8
to
31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 하나의 화면을 재사용하다보니 데이터를 전달하기 위해 선언한 타입도 추가될 수 밖에 없었습니다. 그래서 네임스페이스를 한 단계 더 들여 구현하게 되었어요. 이렇게 재사용하면서 경험했던 문제점은 두 가지였는데요.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
이렇게 같은 화면을 재사용하는 상황에서 사용할 수 있는지 좋은 솔루션이 있는지 궁금합니다....! |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,8 @@ | |
// | ||
|
||
struct Contact: Hashable, Decodable { | ||
let id: Int | ||
|
||
Comment on lines
8
to
+10
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 편집 기능을 구현하면서 view 차원의 index보다는 객체 고유값을 사용하는 편이 나을 것 같다고 판단해 id 속성을 추가하게 되었습니다. hash value를 id로 계산하면 diffable data source에서 hash의 diff를 인식하지 못하는 문제가 생겨서 전체 속성의 값으로 hash를 구성하는 방식을 유지했습니다. |
||
let name: String | ||
|
||
let phoneNumber: String | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 전에는 view 차원의 index를 전달받아 목록을 변경했었는데요. view controller에서 Contact의 id를 전달해, 이후 계층부터는 id를 통해 객체에 식별할 수 있도록 구현을 수정했습니다. |
||
|
||
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 | ||
Comment on lines
+48
to
+52
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 뷰 차원의 index을 validate 하는 구현을 제거하고 id를 통해 source Contact 배열 차원의 index를 구하는 구현을 추가했습니다. |
||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
편집 화면에서 입력된 내용을 Contact로 만들어 전달하는데요. 이때 입력된 content와 id를 분리하는 방식과 기존 id를 바탕으로 새로운 Contact 인스턴스를 만들어 전달하는 방식 사이에서 고민이 있었습니다. 라이언은 어떤 편이 더 낫다고 생각하시나요?