-
Notifications
You must be signed in to change notification settings - Fork 36
[STEP 3] 연락처 관리 프로그램(UI Ver) - Sunny, Is #21
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
base: 1_Sunny
Are you sure you want to change the base?
Conversation
향후 사용하지만 지금은 아직 구현하지 않아서 사용하지 않는 newContact variable을 임시로 _로 처리한다. 사용하지 않는 변수인 경우 _로 치환하라는 컴파일러의 경고를 없애기 위함
cell configuration와의 통일성을 위해 diffableDataSource의 cellProvider 구현을 위한 리네이밍
- UITableViewDiffableDataSource 를 위한 구현 사항들 ContactViewController.Section 구조, setupDataSource() method - 기존의 TableViewDataSouce 삭제
- 기존 method를 override하기 위해 DiffableDS를 상속받는 DataSource class를 구현 - nested class인 DataSource class 안에서 데이터에 접근하기 위해 ModelData에 shared instance 추가 : 실행 방식 상 하나의 json 파일을 기반으로 동작하므로 ModelData의 instance가 의미 없을 것 같긴 한데...
- ContactCell로 타입 캐스팅 - DataSource type을 instantiate - Register method를 사용하기 때문에 필요하지 않은 스토리보드의 prototype cell 삭제
struct인 UserInfo의 모든 stored property는 Hashable, Equatable하므로 required method는 모든 프로퍼티를 바탕으로 기본 구현됨 기본 구현된 method가 기존의 의도와 부합하므로 커스텀 method 삭제
fatalError -> throw FileError.notFound
- 가운데에 숫자 추가 및 삭제 시 제대로 수정되지 않는 문제 해결
| private func parse(_ newText: String, using range: NSRange) -> String { | ||
| /* | ||
| range.location == newText.count | ||
| range.length == 삭제된 character 수 | ||
| */ | ||
| switch (range.location, range.length) { | ||
| case (12, 1...), (11, 0): // 2-4-4로 바뀌어야 하는 경우 | ||
| return relocateHyphen(of: newText, locationsOfHyphen: [2, 7]) | ||
| case (11, 1...): // 2-3-4로 바뀌어야 하는 경우 | ||
| return relocateHyphen(of: newText, locationsOfHyphen: [2, 6]) | ||
| case (12, 0): // 3-4-4로 바뀌어야 하는 경우 | ||
| return relocateHyphen(of: newText, locationsOfHyphen: [3, 8]) | ||
| case (3, 1...), (7, 1...): // 맨 뒤 -를 없애는 경우 | ||
| return removeLastHyphen(from: newText) | ||
| case (2, _), (6, _): // -를 추가해야 하는 경우 | ||
| return newText.inserting(Character.hyphen, at: range.location) | ||
| default: // 그 외의 경우 | ||
| return newText | ||
| } | ||
| } |
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.
오우... 결국 각 케이스를 구분해서 정리하셨군요
고생하셨습니다 😆
| extension UITableView { | ||
| func register<T: UITableViewCell>(_ Type: T.Type) { | ||
| register(Type, forCellReuseIdentifier: Type.reuseIdentifier) | ||
| } | ||
|
|
||
| func dequeue<T: UITableViewCell>(_ t: T.Type, cellForRowAt indexPath: IndexPath) -> T { | ||
| guard let cell = dequeueReusableCell(withIdentifier: T.reuseIdentifier, for: indexPath) as? T else { | ||
| fatalError("\(T.self) is not registered!") | ||
| } | ||
| return cell | ||
| } | ||
| } |
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.
🤔...👏
| private func decoding() -> [UserInfo] { | ||
| do { | ||
| return try decoder() | ||
| } catch { | ||
| print(error.localizedDescription) | ||
| return [] | ||
| } | ||
| } | ||
|
|
||
| func save() { | ||
| do { | ||
| try encoder(data: contacts) | ||
| } catch { | ||
| print(error.localizedDescription) | ||
| } | ||
| } |
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.
Codable을 채택해서 사용할 수 있음에도 함수까지 따로 빼서 만든 이유가 있으신가요?
분리하기 위해서일까요?!
SungPyo
left a comment
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.
[STEP3] 연락처 관리 프로그램(UI Ver) - Sunny, Is
안녕하세요 웨더!(@SungPyo) 연락처 관리 프로그램(UI Ver)의 Sunny, Is입니다😁 Step3는 간단하던 Step 1, 2와 달리 구현할 거리가 많았기 때문에 저희의 고민도 몇 배가 늘어났던 Step이었어요.🥹 Step3의 요구사항을 충족하면서 이번주에 배웠던 내용들도 적용하기 위해 노력했어요! 이번 PR의 궁금한 점은 DM으로도 물어보았지만, 기록을 위해 PR에도 동일하게 작성했어요. review를 받게 되면 또 열심히 공부하고 고민해 수정하도록 할게요!!
요구 사항 정리
- 연락처 목록 화면의 우상단 버튼을 통해 연락처 추가 화면에 진입합니다.
- 각 필드에 맞는 키보드 종류를 지정합니다.
- 취소 버튼을 선택하면 정말 취소할 것인지 묻도록합니다
- 저장 버튼을 선택하면 입력한 정보가 올바르게 입력되었는지 확인하도록 합니다
- 이름, 나이, 연락처의 입력은 첫번째 프로젝트와 동일한 기준으로 검증합니다.
- 모든 정보가 올바로 입력되었다면 저장 버튼을 선택했을 때 이전 화면으로 돌아가고, 추가한 연락처가 연락처 리스트에 추가됩니다
이번 스텝 수행 중 핵심 경험
- UIAlertController의 활용
- 오토레이아웃 구현
- 스택뷰 활용
- 테이블뷰 셀 편집
- 사용자정의 테이블뷰 셀 구현
- 테이블뷰 검색기능 구현
- Dynamic Type 적용
구현 중 궁금한 점
1. 스와이프 삭제는 TableView의 DataSource, Delegate 중 어디에서?
❓ 우리가 삭제 기능을 구현하면서 학습한 결과, Table View의 Swipe 삭제 기능이 DataSource와 Delegate, 두 객체의 method로 모두 구현할 수 있더라고요. 둘 중 어디서 하든 상관없는지, 아니면 둘 중 더 대중적인? 방식이 있는지 궁금해요!
ㄴ 이게 DadaSource에도 있나요? 전혀 생각지 못했네요 일단 저라면 delegate에서 할 것 같습니다.
2. 지금은 Swipe 삭제 기능을 DiffableDataSource 에서 구현했어요. 이를 위해 UITableViewDiffableDataSource class를 상속받는 DataSource class를 구현했습니다. 그런데 이걸 ViewController의 nested class로 구현하는게 좋은지, 그렇지 않은지 궁금해요.
- 이전에는 ViewController가 너무 커지는 것 같아서 TableViewDataSource class를 새로운 파일에 구현했어요
- DiffableDataSource로 바꾸면서 init만 하는 과정에서 해당 파일을 삭제했어요
- swipe 삭제 기능을 구현하며 method override의 필요성이 있었고, 따라서 새로운 DataSource class를 구현해야 했어요.
- 레퍼런스 코드를 찾는 과정에서 nested class 형태로 DataSource를 구현하는 경우가 왕왕 있었어요.
- 지금 프로젝트에서는 해당 DataSource를 ContactView 한 군데에서 사용하기 때문에, 두 방법 모두 상관이 없지만 해당 DataSource가 다른 View에서도 참조된다면, 새로운 파일로 빼는게 맞는 것 같아요.
❓ nested로 한다면 기존의 문제의식이었던 'ViewController'가 너무 커지는 문제가 동일하게 발생할 것 같은데, 두 방법 모두 가능한 상황에서, 어떤 방식이 더 좋은 방식인가요? 둘 중 하나를 선택하기 위해서 어떤 걸 고려하면 좋을까요?
ㄴ 음 이건 너무 취향이에요 저는 따로 중첩클래스를 만들지 않지만 중첩클래스를 만드는 사람도 있습니다. 더 편하신 방법 사용하시면 될 것 같은데요?!
3. 지금은 DataSource를 ViewController의 nested로 구현했기 때문에, 스와이프 삭제 시 모델을 update하는 과정에서 ModelController(ModelData.swift)를 Static으로 접근할 필요성이 있었어요.
❓ 데이터 모델? 모델 컨트롤러?가 shared(singleton)나 enum(-type method, property)으로 구현하면 단일한 객체가 되는데, MVC 알못이라 모델이 단일한 객체인게 조금 어색한 것 같아요. 데이터 모델이 단일한 객체여도 괜찮은가요?
ㄴ 음... 글쎼요? 어디든 쉽게 접근 가능하다는 장점은 있을 수 있겠지만 내부 객체의 동시성이라던가 다른 객체들의 의존성이 높아지죠. 또한 한번 �생성하면 사용하지 않는 모델이라도 메모리에서 사라지지 않죠. 이러한 단점들을 다 극복할 수 있다면 상관없다고 보여집니다.
세부 구현 내용
1. 연락처 로드 및 연락처 추가 로직
Step2에서 웨더가 주신 리뷰를 적용해 JSONCodable protocol을 생성하고, extension으로 구체적인 코드를 구현했어요.
protocol JSONCodable { var fileName: String { get } func fileURL() throws -> URL func encoder<E: Encodable>(data newData: E) throws func decoder<D: Decodable>() throws -> D } final class ContactsController: JSONCodable { static let shared = ContactsController() private init() { } var fileName: String { return "contacts.json" } private(set) lazy var contacts: [UserInfo] = decoding() }
- contacts는 JSON파일 형태로 존재하는 연락처 목록을 decoding을 통해 [UserInfo] 형식으로 변환한 배열이 기본값으로 설정했어요.
- App 실행 중 연락처가 추가/삭제 될 때, [UserInfo]에서 .append(), .remove(at: )이 되고 있어요.
// SceneDelegate.swift func sceneWillResignActive(_ scene: UIScene) { ContactsController.shared.save() // Called when the scene will move from an active state to an inactive state. // This may occur due to temporary interruptions (ex. an incoming phone call). }
- SceneDelegate class의 sceneWillResignActive 메서드 안에 contacts를 encoding하여 JSON파일 형식으로 저장하는 코드를 넣었어요.
- 화면이 inactive한 상태가 되었을 때, 지금까지 수정한 contacts를 JSON파일로 변환하는 타이밍이 언제가 좋을까 고민하다가 이 메서드 안에 넣게 되었어요.
2. 연락처 입력시 실시간 hyphen 입력
동작 방식
- User가 AddContactView의 contact text field에 연락처를 입력한다.
- UITextFieldDelegate를 conform하는 AddContactViewController가 구현한
textField(:shouldChangeCharactersIn:replacementString:)이 호출된다.- 새로 수정된 텍스트인 newText를 parse하면 올바른 위치에 hyphen이 추가된 문자열이 반환된다.
- 3의 결과를 textField.text에 할당한다.
textField method의 반환값이 false인 이유
textField(:shouldChangeCharactersIn:replacementString:)method의 매개변수로 전달되는 textField 인자는 아직 수정이 적용되지 않은 textField입니다. 이 메소드에서 false를 반환하면 textField는 업데이트되지 않습니다. 초기에는 특정 길이일 때에만 hyphen을 추가/삭제 후 return true로 변경을 적용하는 식으로 구현했는데 hyphen 추가와 숫자 추가 사이에 딜레이가 느껴졌어요. 그래서 parse method로 결과 텍스트를 만들어서 textField에 할당합니다.parse 함수 동작
private func parse(_ newText: String, using range: NSRange) -> String { switch (range.location, range.length) { case (12, 1...), (11, 0): return relocateHyphen(of: newText, at: LocationsOfHyphen.forTwoFourFour) case (11, 1...): return relocateHyphen(of: newText, at: LocationsOfHyphen.forTwoThreeFour) case (12, 0): return relocateHyphen(of: newText, at: LocationsOfHyphen.forThreeFourFour) case (3, 1...), (7, 1...): return removeLastHyphen(from: newText) case (2, _), (6, _): return newText.inserting(Character.hyphen, at: range.location) default: return newText } }기존에는 마지막에 숫자가 추가될 때, 삭제될 때 갯수를 바탕으로 복잡하게 switch문을 나눴었어요. 필요한 때에만 hyphen을 relocate 하고 싶었기 때문이예요. 그런데 연락처 중간에 숫자를 추가하니 제대로 동작하지 않음을 확인했어요.
private func parse(_ newText: String, using range: NSRange) -> String { let digitString = newText.replacingOccurrences(of: String.hyphen, with: String.empty) switch digitString.count { case ..<contactTextHyphenPolicyLength: return relocateHyphen(of: digitString, at: LocationsOfHyphen.forTwoThreeFour) case contactTextHyphenPolicyLength: return relocateHyphen(of: digitString, at: LocationsOfHyphen.forTwoFourFour) default: return relocateHyphen(of: digitString, at: LocationsOfHyphen.forThreeFourFour) } } private func relocateHyphen(of string: String, at indices: [Int]) -> String { var relocated = string for (index, stringIndex) in indices.enumerated() where stringIndex - index < string.count { relocated.insert(Character.hyphen, at: stringIndex) } return relocated }지금은 모든 입력에 대해 relocateHyphen(of:at) method를 호출합니다.
3. UITableView의 dequeueReusableCell extension
UITableView를 사용할 때 UITableViewCell을 register, dequeueReusableCell 하는 코드를 더 편리하게 사용하기 위해 아래와 같은 extension을 추가했어요.
// Extension+TableView.swift extension UITableView { func register<T: UITableViewCell>(_ Type: T.Type) { register(Type, forCellReuseIdentifier: Type.reuseIdentifier) } func dequeue<T: UITableViewCell>(_ t: T.Type, cellForRowAt indexPath: IndexPath) -> T { guard let cell = dequeueReusableCell(withIdentifier: T.reuseIdentifier, for: indexPath) as? T else { fatalError("\(T.self) is not registered!") } return cell } } // Reusable.swift protocol Reusable { static var reuseIdentifier: String { get } } extension Reusable { static var reuseIdentifier: String { String(describing: self) } } // Extension+TableViewCell.swift extension UITableViewCell: Reusable { }
- Reusable protocol은 자기 자신의 이름을 reuseIdentifier로 갖도록 하는 프로토콜이에요.
- UITableViewCell이 Reusable protocol을 채택해 자기 자신을 identifier로 갖게 되었어요.
- register와 dequeue의 파라미터로 UITableViewCell을 넣어주면 간단하게 tableViewCell 사용이 가능해요.
4. UITableViewDiffableDataSource
기존 UITableViewDataSource는 데이터를 수동으로 관리해요. tableView.reloadData() 메서드를 통한 업데이트는 뚝뚝 끊어지는 사용자 경험을 제공합니다.
저희는 이러한 단점을 보안하기 위해 데이터의 변화된 부분을 인지하고 자연스럽게 UI를 업데이트 하는 UITableViewDiffableDataSource를 사용해요.
// NewContactDelegate.swift protocol NewContactDelegate: AnyObject { func addNewContact() } // ContactViewController.swift extension ContactViewController: NewContactDelegate { func addNewContact() { dataSource?.update(animatingDifferences: true) } } // ContactViewController.swift 중 update()메서드 관련 func update(animatingDifferences: Bool) { var snapshot = Snapshot() snapshot.appendSections([.main]) snapshot.appendItems(ContactsController.shared.contacts) apply(snapshot, animatingDifferences: animatingDifferences) } // AddContactViewController.swift do { let newContact = try UserInfo(name: name, age: age, phone: contact) ContactsController.shared.add(user: newContact) newContactDelegate?.addNewContact() dismiss(animated: true) } catch { makeErrorAlert(description: error.localizedDescription) }저희는 AddContactViewController에서 새로운 User가 생성이 된다면 NewContactDelegate를 통해 ContactViewController에게 메세지를 전달해요. 메세지를 받은 ContactViewController는 update 메서드를 실행시켜 snapshot으로 data를 업데이트해요.
5. Cell Configuration
// ContactCell.swift override func updateConfiguration(using state: UICellConfigurationState) { var configuration = defaultContentConfiguration().updated(for: state) configuration.text = content?.mainText configuration.textProperties.font = .preferredFont(forTextStyle: .headline) configuration.secondaryText = content?.subText configuration.secondaryTextProperties.font = .preferredFont(forTextStyle: .body) contentConfiguration = configuration }WWDC2020 Modern cell configuration을 참고해 Cell Configuration을 적용했어요. ContactCell.swift에서 cell을 reuse할 때마다 불리는 updateConfiguration 메서드를 override해요. 이전의 상태를 신경쓰지 않고 defaultContentConfiguration을 불러와서 원하는 configuration으로 설정하고 이를 cell에 적용하기만 하면, 변경 사항은 UIKit이 알아서 추적하고 반영해주는 방식이 굉장히 편했습니다. 이전에는 그냥 인터넷에서 찾은 코드를 복붙했었어요. 그런데 길면 길고 짧으면 짧은 30분짜리 영상을 보면서 새로 알게된 게 굉장히 많았어요. 복붙만 하던 코드를 왜 그렇게 했는지 설명할 수 있게 된 점이 좋은 경험이었습니다..!
이번 커밋은 굉장히 헤비하네요.
중간에 codable 관련 질문을 하나 남겨놨는데요. 해당 질문을 제외하면 나머지는 특별히 볼만한게 없는 것 같아요.
TableView Extension 추가하신건 잘하셨구요 😆
질문 관련 부분은 이전에 물어보셨던 내용 그대로네요!
답변도 그대로 달아두도록 하겠습니다.
프로젝트 기간이 남았으니 숙제(?) 비슷한거 하나만 드려볼게요.
코다블 사용법은 어느정도 잘 알고 계신것 같은데, 실무에선 코다블을 사용하지 않는 경우도 상당수 있습니다.
'Encodable, Decodable' 을 사용하지 않고 처리를 해보는것도 도움이 될거에요 :)
고생하셨습니다.🙏
[STEP3] 연락처 관리 프로그램(UI Ver) - Sunny, Is
안녕하세요 웨더!(@SungPyo) 연락처 관리 프로그램(UI Ver)의 Sunny, Is입니다😁 Step3는 간단하던 Step 1, 2와 달리 구현할 거리가 많았기 때문에 저희의 고민도 몇 배가 늘어났던 Step이었어요.🥹 Step3의 요구사항을 충족하면서 이번주에 배웠던 내용들도 적용하기 위해 노력했어요!
이번 PR의 궁금한 점은 DM으로도 물어보았지만, 기록을 위해 PR에도 동일하게 작성했어요. review를 받게 되면 또 열심히 공부하고 고민해 수정하도록 할게요!!
요구 사항 정리
이번 스텝 수행 중 핵심 경험
구현 중 궁금한 점
1. 스와이프 삭제는 TableView의 DataSource, Delegate 중 어디에서?
❓ 우리가 삭제 기능을 구현하면서 학습한 결과, Table View의 Swipe 삭제 기능이 DataSource와 Delegate, 두 객체의 method로 모두 구현할 수 있더라고요. 둘 중 어디서 하든 상관없는지, 아니면 둘 중 더 대중적인? 방식이 있는지 궁금해요!
2. 지금은 Swipe 삭제 기능을 DiffableDataSource 에서 구현했어요. 이를 위해 UITableViewDiffableDataSource class를 상속받는 DataSource class를 구현했습니다. 그런데 이걸 ViewController의 nested class로 구현하는게 좋은지, 그렇지 않은지 궁금해요.
❓ nested로 한다면 기존의 문제의식이었던 'ViewController'가 너무 커지는 문제가 동일하게 발생할 것 같은데, 두 방법 모두 가능한 상황에서, 어떤 방식이 더 좋은 방식인가요? 둘 중 하나를 선택하기 위해서 어떤 걸 고려하면 좋을까요?
3. 지금은 DataSource를 ViewController의 nested로 구현했기 때문에, 스와이프 삭제 시 모델을 update하는 과정에서 ModelController(ModelData.swift)를 Static으로 접근할 필요성이 있었어요.
❓ 데이터 모델? 모델 컨트롤러?가 shared(singleton)나 enum(-type method, property)으로 구현하면 단일한 객체가 되는데, MVC 알못이라 모델이 단일한 객체인게 조금 어색한 것 같아요. 데이터 모델이 단일한 객체여도 괜찮은가요?
세부 구현 내용
1. 연락처 로드 및 연락처 추가 로직
Step2에서 웨더가 주신 리뷰를 적용해 JSONCodable protocol을 생성하고, extension으로 구체적인 코드를 구현했어요.
2. 연락처 입력시 실시간 hyphen 입력
동작 방식
textField(:shouldChangeCharactersIn:replacementString:)이 호출된다.textField method의 반환값이 false인 이유
textField(:shouldChangeCharactersIn:replacementString:)method의 매개변수로 전달되는 textField 인자는 아직 수정이 적용되지 않은 textField입니다. 이 메소드에서 false를 반환하면 textField는 업데이트되지 않습니다.초기에는 특정 길이일 때에만 hyphen을 추가/삭제 후 return true로 변경을 적용하는 식으로 구현했는데 hyphen 추가와 숫자 추가 사이에 딜레이가 느껴졌어요.
그래서 parse method로 결과 텍스트를 만들어서 textField에 할당합니다.
parse 함수 동작
기존에는 마지막에 숫자가 추가될 때, 삭제될 때 갯수를 바탕으로 복잡하게 switch문을 나눴었어요. 필요한 때에만 hyphen을 relocate 하고 싶었기 때문이예요. 그런데 연락처 중간에 숫자를 추가하니 제대로 동작하지 않음을 확인했어요.
지금은 모든 입력에 대해 relocateHyphen(of:at) method를 호출합니다.
3. UITableView의 dequeueReusableCell extension
UITableView를 사용할 때 UITableViewCell을 register, dequeueReusableCell 하는 코드를 더 편리하게 사용하기 위해 아래와 같은 extension을 추가했어요.
4. UITableViewDiffableDataSource
기존 UITableViewDataSource는 데이터를 수동으로 관리해요. tableView.reloadData() 메서드를 통한 업데이트는 뚝뚝 끊어지는 사용자 경험을 제공합니다.
저희는 이러한 단점을 보안하기 위해 데이터의 변화된 부분을 인지하고 자연스럽게 UI를 업데이트 하는 UITableViewDiffableDataSource를 사용해요.
저희는 AddContactViewController에서 새로운 User가 생성이 된다면 NewContactDelegate를 통해 ContactViewController에게 메세지를 전달해요.
메세지를 받은 ContactViewController는 update 메서드를 실행시켜 snapshot으로 data를 업데이트해요.
5. Cell Configuration
WWDC2020 Modern cell configuration을 참고해 Cell Configuration을 적용했어요. ContactCell.swift에서 cell을 reuse할 때마다 불리는 updateConfiguration 메서드를 override해요.
이전의 상태를 신경쓰지 않고 defaultContentConfiguration을 불러와서 원하는 configuration으로 설정하고 이를 cell에 적용하기만 하면, 변경 사항은 UIKit이 알아서 추적하고 반영해주는 방식이 굉장히 편했습니다. 이전에는 그냥 인터넷에서 찾은 코드를 복붙했었어요. 그런데 길면 길고 짧으면 짧은 30분짜리 영상을 보면서 새로 알게된 게 굉장히 많았어요. 복붙만 하던 코드를 왜 그렇게 했는지 설명할 수 있게 된 점이 좋은 경험이었습니다..!