Skip to content
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

연락처 관리 앱 [Step3] Yuni, L #91

Open
wants to merge 36 commits into
base: d_Yuni
Choose a base branch
from

Conversation

LeeSe0ngYe0n
Copy link

@LeeSe0ngYe0n LeeSe0ngYe0n commented Jan 21, 2024

@ICS-Asan
안녕하세요, 아샌!! yuni, L 입니다!!
STEP3 보내드립니다! 확인 부탁드립니다!


Step3 요구사항

✅ 연락처 삭제

  • 필수 테이블뷰의 셀을 스와이프하여 삭제하는 메뉴를 보이고, 실제로 삭제할 수 있도록 구현합니다.

✅ Cell Layout

  • 검색기능 통해 연락처 목록 내의 특정 연락처만 테이블뷰에 남길 수 있도록 구현해 주세요.
  • Cell을 사용자정의하여 원하는 레이아웃으로 표현하도록합니다. (예시와 똑같지 않아도 됩니다.)

✅ Dynamic Type

  • Dynamic Type을 활용하여 시스템 설정에 따라 앱 글자 크기가 변경될 수 있도록 구현해주세요.

Bonus STEP 요구사항

✅ 해외 연락처 입력

  • 국제전화 및 해외 전화번호 입력 형식을 고민하고 구현해보세요.

✅ 연락처 정보변경

  • 연락처 목록에서 연락처를 선택하면 Step2의 연락처 추가 화면과 동일한 화면이 나옵니다. (단. 상단의 title 은 기존 연락처 로 변경합니다.)

📦 ContactManager
+-- 🗂 App
|    +-- 🗂 AppDelegate
|    +-- 🗂 SceneDelegate
+-- 🗂 Model
|    +-- 🗂 Contact
|    +-- 🗂 ContactFileManager
|    +-- 🗂 ContactManagerError
|    +-- 🗂 RegularExpressionCheck
|    +-- 🗂 ValidError
+-- 🗂 Extensions
|    +-- 🗂 Alert+Extension
+-- 🗂 Protocols
|    +-- 🗂 Protocol
+-- 🗂 ViewController
|    +-- 🗂 ContactListViewController
|    +-- 🗂 NewContactViewController
+-- 🗂 View
|    +-- 🗂 Main
+-- 🗂 Resource
|    +-- 🗂 Assets
|    +-- 🗂 LanchScreen
+-- 🗂 Info

고민했던점

  • 검색기능

    • localizedStandardContains 를 사용해서 입력값 대/소문자 구분없이 검색 가능하게 구현했습니다.
    func updateSearchResults(for searchController: UISearchController) {
            guard let text = searchController.searchBar.text else { return }
            filteredDataSource = contactFileManager.contacts.filter { contact in
                return contact.nameAndAge.localizedStandardContains(text) || contact.phoneNumber.localizedStandardContains(text)
            }
            tableView.reloadData()
        }
    • 검색하는 중에 삭제하면 에러나서 에러 발생합니다.

      • 검색된 데이터를 저장하는 배열을 따로 만들었기때문에 그런거 같은데
      • 해결방법이 떠오르지 않아서 검색중에는 데이터를 삭제할 수 없게 구현해보았습니다.
      func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
              if !isFiltering {
                  let delete = UIContextualAction(style: .destructive, title: "delete") { (_, _, success: @escaping (Bool) -> Void) in
                      self.contactFileManager.removeContact(indexPath.row)
                      tableView.deleteRows(at: [indexPath], with: .automatic)
                      success(true)
                  }
                  return UISwipeActionsConfiguration(actions: [delete])
              }
              return nil
          }
    • 검색결과 일치하는 데이터가 없을때 전체데이터를 보여주는 이슈가 있었습니다.

    2024-01-21_6 02 17
     // 수정전
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
             return filteredDataSource.isEmpty ? contactFileManager.contacts.count : filteredDataSource.count
            }
          func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
                let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
                let contact = filteredDataSource.isEmpty ? contactFileManager.contacts[indexPath.row] : filteredDataSource[indexPath.row]
               cell.textLabel?.text = contact.nameAndAge
               cell.detailTextLabel?.text = contact.phoneNumber
               return cell
         }
     // 수정후
        private var isFiltering: Bool {
                let searchController = navigationItem.searchController
                let isActive = searchController?.isActive ?? false
                let isSearchBarHasText = searchController?.searchBar.text?.isEmpty == false
               return isActive && isSearchBarHasText
            }
     
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
                return isFiltering ? filteredDataSource.count : contactFileManager.contacts.count
           }
     
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
                let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
                let contact = isFiltering ? filteredDataSource[indexPath.row] : contactFileManager.contacts[indexPath.row]
               cell.textLabel?.text = contact.nameAndAge
               cell.detailTextLabel?.text = contact.phoneNumber
               return cell
            }
  • ViewController 재사용

    • 해당 셀을 터치 시 보여주는 화면과 새로운 연락처를 추가하는 화면이 같기 때문에 뷰를 하나 더 만들지 않고 조건을 통해 데이터를 전달해 주는 방식을 사용했습니다.
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
            showNewContactView()
        }

조언 및 궁금한 부분

  • 원본 데이터와 검색결과에 따른 필터가 된 배열을 나누어서 사용하였는데 이 방법이 맞는지 궁금합니다!
  • 검색 후 셀에 있는 데이터를 변경하고 돌아왔을 때 테이블 뷰에 변경된 데이터가 적용이 되지 않습니다.
    • 마땅한 해결방안이 떠오르지 않는데 아샌의 생각이 궁금합니다!

LeeSe0ngYe0n and others added 30 commits January 12, 2024 16:31
Copy link

@ICS-Asan ICS-Asan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

안녕하세요 유니, 엘~
이번 Step3도 고생 많으셨습니다!
이전 스텝보다 기능이 많아서 어려운 부분이 많으셨을텐데 잘 구현해주신 것 같아요.
먼저 코멘트로 남겨주신 부분에 대해서 먼저 답변을 드리도록 할게요!

[테이블뷰의 데이터 관리]


검색, 삭제, 원본데이터와 검색 결과에 따른 배열 관리, 데이터 수정시 업데이트에 대해 질문을 주셨는데 한번에 답변드리는 게 좋을 것 같아요.
먼저 API를 통해 검색, 삭제, 수정을 하지 않는다면 이렇게 원본데이터와 보여주기 위한 데이터를 관리하는 방법을 실제로 사용하기도 합니다.
그래서 현재의 데이터 노출 방식은 잘 구현해주신 것 같아요.
근데 여러분들이 어려워 하셨던 부분을 생각해보면 두가지 배열의 역할을 명확하게 하지 않아서 생긴 문제가 아닌가 생각이 들었습니다.
contactFileManager.contacts가 원본 전체 데이터이고 filteredDataSource가 검색 결과를 보여주기위한 데이터로 사용을 하면 조금 더 명확해질 거라고 생각이 들었습니다.
삭제, 수정, 추가는 contactFileManager.contacts이 배열에서만 이루어지게 하고 filteredDataSource는 보여주기만 하는 데이터로 사용을 하면 어느정도 해결이 될 것 같다고 생각했어요.
현재 구현된 연락처 Model인 Contact를 보면 id 프로퍼티를 가지고 있어요.
그렇다면 각각의 연락처의 고유 값을 가지고 있기 때문에 다른 데이터들이 동일하더라도 다른 연락처라는 구분을 할 수 있고, 반대로 원하는 연락처를 id로 찾을 수 있지 않을까요?
그럼 원본데이터에서 id를 통해 검색해서 삭제나 수정을 진행하고 다시 reload를 한다면 그 수정사항이 반영되어 있을 것 같네요!
그리고 이렇게 구현하게 된다면 검색어가 없어 전체를 보여줄때도 contactFileManager.contacts를 보여주는 것이 아닌 filteredDataSource에 전체 데이터를 할당해서 보여준다면 역할도 명확해지고 로직도 정리가 될 것 같아요.
답변이 조금 길어졌는데 요약하자면
contactFileManager.contacts - 원본 데이터
filteredDataSource - 보여주기 위한 데이터
데이터 수정은 id를 통해 원본 데이터에서 반영
으로 정리해볼 수 있을 것 같아요.

[검색결과 일치하는 데이터가 없을때 전체데이터를 보여주는 이슈]


위의 답변에서 드린것을 반영한다면 해결이 될 것 같지만 그 이전에 로직만 일부 수정되어도 해결이 될 수 있을 것 같아 답변을 남깁니다~
수정전 코드의
filteredDataSource.isEmpty ? contactFileManager.contacts.count : filteredDataSource.count
let contact = filteredDataSource.isEmpty ? contactFileManager.contacts[indexPath.row] : filteredDataSource[indexPath.row]
이 두코드를 보면 filteredDataSource가 비어있다면 원본데이터인 contactFileManager.contacts를 사용하고 있어서 검색결과가 없으면 전체데이터가 보여졌던 것 같아요.
검색결과와 일치하는 것이 없으면 빈 배열일텐데 그때 전체 데이터를 보여주게 되어 있어 이런 이슈가 발생했던 것 같아요.
없으면 없는대로 빈 배열을 보여준다면 이슈가 해결될 것 같아요!

Comment on lines +8 to +10
protocol UpdateNewContact: AnyObject {
func updateNewContact()
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

프로토콜의 이름이 메서드의 네이밍 같아요.
프로토콜의 역할이 무엇인지 생각해보고 기존에 구현되어있는 swift의 프로토콜들의 네이밍을 한번 살펴보시는걸 추천 드려요.
그리고 다른 사람들은 어떻게 사용하고 있는지 swift protocol naming과 같은 검색어로 찾아보시면 더 좋을 것 같아요!

Comment on lines +24 to +32
static func isValidString(string: String, forPattern pattern: RegularExpressionCheck) -> Bool {
do {
let regex = try NSRegularExpression(pattern: pattern.regex)
let range = NSRange(location: 0, length: string.utf16.count)
return regex.firstMatch(in: string, options: [], range: range) != nil
} catch {
return false
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


import Foundation

enum ValidError: LocalizedError {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네이밍이 조금 더 구체적이면 좋을 것 같아요!
어떤것이 유효하지 않을때 발생하는 에러인지 직관적으로 알기 어려운 것 같아요

Comment on lines +42 to +54
@objc private func showNewContactView() {
var selectedContact: Contact?
if let selectedIndexPath = tableView.indexPathForSelectedRow {
selectedContact = isFiltering ? filteredDataSource[selectedIndexPath.row] : contactFileManager.contacts[selectedIndexPath.row]
}

guard let newContactViewController = storyboard?.instantiateViewController(identifier: "NewContactViewController", creator: { coder in
NewContactViewController(coder: coder, contactFileManager: self.contactFileManager, delegate: self, selectedContact: selectedContact)
}) as? NewContactViewController else {
return
}
present(newContactViewController, animated: true)
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

로직을 봤을때 선택된 데이터가 있으면 그 데이터로 NewContactViewController의 입력을 채워서 보여주고 +버튼을 눌렀을때는 신규 연락처 등록을 할 수 있도록 빈칸으로 보이고 있네요.
하나의 뷰가 연락처 수정, 연락처 추가 라는 2가지 역할을 하고 있는 것 같아요.
그런데 네이밍은 전체적으로 NewContact로 사용하고 있어서 조금 헷갈리는 것 같아요.
NewContact 라는 네이밍을 연락처를 관리하는 이라는 의미로 ManagementContact 또는 연락처 정보를 입력하는 이라는 의미로 ContactInfromation등의 네이밍을 사용해보는 건 어떠신가요?

Comment on lines +42 to +54
@objc private func showNewContactView() {
var selectedContact: Contact?
if let selectedIndexPath = tableView.indexPathForSelectedRow {
selectedContact = isFiltering ? filteredDataSource[selectedIndexPath.row] : contactFileManager.contacts[selectedIndexPath.row]
}

guard let newContactViewController = storyboard?.instantiateViewController(identifier: "NewContactViewController", creator: { coder in
NewContactViewController(coder: coder, contactFileManager: self.contactFileManager, delegate: self, selectedContact: selectedContact)
}) as? NewContactViewController else {
return
}
present(newContactViewController, animated: true)
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

로직을 보면 showNewContactView이라는 메서드 안에서 tableView의 선택된 indexPath도 확인하고 있다면 indexPath로 선택된 데이터를 찾고 다음 화면으로 넘겨주고 있어 많은 일을 하고 있지 않나 생각이 들어요.
네이밍에 맞게 NewContactView를 보여주는 역할에 집중해보는건 어떨까요?
그렇게 되면 밖에서 Contact를 받아와서 newContactViewController를 만들고 보여주기만 할 수 있을 것 같아요!


// MARK: - UITextFieldDelegate Methods
extension NewContactViewController: UITextFieldDelegate {
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이번에 보너스 스텝으로 국가번호가 포함되는 경우도 고려하시다 보니 더 힘드셨을 것 같아요.
하지만 아쉽게도 전체적으로 해피케이스가 아닌 경우에는 예외처리가 되고 있지 않은 것 같아요.
그리고 전화번호 국가코드로 찾아보시면 우리나라는 +82이지만 +세자리도 있어요.
저는 기존 휴대폰 번호, 일반 전화번호 입력만을 가지고 예외처리를 완벽하게 해보는 것도 좋은 것 같습니다.
어떤 것이 입력가능한지, 입력하면 안되는 값이 들어왔을때(특수문자, 글자 초과 등)는 어떻게 처리해줄 것인지, 입력중에는 어떤 케이스들이 발생하고 어떻게 보여주는 것이 좋을 것인지 등이 있을 것 같아요.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants