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

연락처 관리 앱 프로젝트 [STEP 2] Matthew, Asher #69

Open
wants to merge 16 commits into
base: d_Asher
Choose a base branch
from

Conversation

kimbs5899
Copy link

@kimbs5899 kimbs5899 commented Jan 13, 2024

연락처 관리 앱 프로젝트 [STEP 2]

멤버 : @kimbs5899@tawans
리뷰어 : @ictechgy

연락처 관리 앱 프로젝트의 두번째 Step입니다!

정리하여 PR 드립니다. 감사합니다!

이번 스텝을 통해서 배운 점

  • Data 전달방식

    • 기존 데이터 전달 방식중 closure를 사용해봤습니다.
  • ContactManager

    • UUID에 대한 고민과 내부 저장에 대한 방향성 고민 (json, coreData)
  • formatPhoneNumber

    func formatPhoneNumber() -> String {
        var formatText = self.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
    
        guard !formatText.isEmpty else {
            return ""
        }
    
        let hyphen: Character = "-"
    
        if formatText.hasPrefix("02") {
            switch formatText.count {
            case 3...5:
                formatText.insert(hyphen, at: formatText.index(formatText.startIndex, offsetBy: 2))
            case 6...9:
                formatText.insert(hyphen, at: formatText.index(formatText.startIndex, offsetBy: 2))
                formatText.insert(hyphen, at: formatText.index(formatText.endIndex, offsetBy: 6 - formatText.count))
            case 10:
                formatText.insert(hyphen, at: formatText.index(formatText.startIndex, offsetBy: 2))
                formatText.insert(hyphen, at: formatText.index(formatText.endIndex, offsetBy: -4))
            default:
                break
            }
        } else {
            switch formatText.count {
            case 4...6:
                formatText.insert(hyphen, at: formatText.index(formatText.startIndex, offsetBy: 3))
            case 7...10:
                formatText.insert(hyphen, at: formatText.index(formatText.startIndex, offsetBy: 3))
                formatText.insert(hyphen, at: formatText.index(formatText.endIndex, offsetBy: 7 - formatText.count))
            case 11:
                formatText.insert(hyphen, at: formatText.index(formatText.startIndex, offsetBy: 3))
                formatText.insert(hyphen, at: formatText.index(formatText.endIndex, offsetBy: -4))
            default:
                break
            }
        }
        return formatText
    }

구성

Model

Contact.swift

  • Contact Class
    • property : 이름, 전화번호, 나이

ContactListManager.swift

  • ContactListManager
    • property: 싱글톤 패턴을 위한 shared, contactArray
    • method: ContactList 생성, ContactList 보기, Contact 추가(변경), Contact 삭제

View

ContectListView.swift

  • ContectListView 커스텀 UIView 클래스

DetailContectView.swift

  • DetailContectView 커스텀 UIView 클래스

Controller

ContactListViewController.swift

  • ContactListViewController : 연락처 리스트 뷰 컨트롤러

DetailContactViewController.swift

  • DetailContactViewController : 연락처 등록 상세페이지 뷰 컨트롤러

Support

String+

  • formatPhoneNumber : 연락처 텍스트 입력시 - 자동완성

UIViewController+

  • sendMessageAlert : 확인 버튼 Alert 생성
  • canceledAlert : 예 아니오 버튼 Alert 생성

CustomString

  • 휴먼에러 방지를 위한 description 생성

질문

  • PreView기능을 위해 해당하는 곳에서 SwiftUI를 사용해봤는데 혹시 실무에서 UIKit으로 개발하실때 미리보기 기능을 위해 해당 방식을 사용하시는지 궁금합니다!
  • extension 을 통해 뷰의 라이프사이클을 뺴고 메서드를 분리하는 리팩토링을 해봤는데 이런식으로 코드 분리를 실무에서도 하는지 궁금합니다!

UML

image

@ictechgy
Copy link

ictechgy commented Jan 15, 2024

안녕하세요 @tawans , @kimbs5899 !
STEP2 고생하셨습니다.
다름이 아니라 UML을 추가해주셔야 하는데 빠뜨리신 것 같아요~ 작성해서 PR 업데이트 부탁드리겠습니다!

더불어서 target 브랜치�도 확인 부탁드립니다(현재는 diff를 확인하기가 어렵습니다)

@kimbs5899 kimbs5899 changed the base branch from main to d_Asher January 16, 2024 01:32
Copy link

@ictechgy ictechgy left a comment

Choose a reason for hiding this comment

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

안녕하세요 @kimbs5899 !
STEP2 진행하느라 고생 많으셨습니다!! 🥳🙌

작성해주신 코드들에 대해 라인별로 코멘트를 남겼는데요! 확인 부탁드리겠습니다.

여기서는 작성해주신 PR description에 대해 답변 드릴게요!


미리보기 기능을 위해 UIKit에서 preview를 쓰는지

회사마다 다를 수 있겠지만 저희는 잘 안씁니다 ㅎㅎ. 특별한 이유는 없고 빌드 시간이 오래 걸리는 편은 아니다보니 직접 실행해서 화면을 보는 것 같아요.

빌드 시간이 길어진다거나 depth가 깊어지는 화면이라면 고려해볼만한 방식인 것 같습니다!


extension 코드 분리에 대해

네 당연히 작성해주신 것처럼 분리해서 선언 및 사용합니다!
다만 개별 extension들 위에 MARK 주석을 달아서 아래 코드가 무엇에 대한 확장인지 간략하게 마킹합니다.

추가적으로, initializer와 loadView, viewDidLoad같은 부분은 같이 두기도 하는데(lifecycle) 이러한 부분은 선택(컨벤션)의 차이라 크게 의미를 두시진 않아도 될 것 같습니다.


작성해주신 배운점 관련하여

UUID에 대한 고민과 내부 저장에 대한 방향성 고민 (json, coreData)

이라고 써주셨는데 어떤 고민이었는지 조금 자세히 설명해주실 수 있으실까요? 궁금합니다!


UML 관련하여

UML 정말 잘 만들어주셨는데요! 궁금한 점이 몇가지가 있습니다.

  • 클래스는 C라고 해주신 것 같은데 UIViewControllerRepresentable과 같은 것들은 Protocol이지 않을까요?
  • 프로토콜들에 대한 채택은 inherit(상속)보다는 conform(준수)이라고 표기하는게 낫지 않을까요?
  • 작성 툴은 어떤 것을 쓰신걸까요? (draw.io같은데 맞을까요?)

ios-contact-manager/AppDelegate.swift Show resolved Hide resolved
ios-contact-manager/Views/DetailContactView.swift Outdated Show resolved Hide resolved
ios-contact-manager/Views/DetailContactView.swift Outdated Show resolved Hide resolved
ios-contact-manager/Views/DetailContactView.swift Outdated Show resolved Hide resolved
ios-contact-manager/Views/DetailContactView.swift Outdated Show resolved Hide resolved
@kimbs5899
Copy link
Author

kimbs5899 commented Jan 17, 2024

@ictechgy
주신 질문에 대한 답변입니다!

UUID에 대한 고민과 내부 저장에 대한 방향성 고민 (json, coreData)
이라고 써주셨는데 어떤 고민이었는지 조금 자세히 설명해주실 수 있으실까요? 궁금합니다!

-> 개인적으로 이번 프로젝트를 통해 coreData 또는 userDefault, json에 데이터를 저장하여 앱이 종료되어도 수정한 데이터가 유지 시키도록 할 것을 예상했습니다! 그래서 데이터의 고유값을 UUID를 주어 해당 부분에 데이터 처리를 할지 id값을 따로 할당하여 처리할지에 대한 고민을 했습니다!

이렇게 많은 데이터를 담을 경우가 아닌 경우에 굳이 UUID를 채택하여 하는 부분이 낭비가 아닐까 하는 고민을 했습니다!
클래스는 C라고 해주신 것 같은데 UIViewControllerRepresentable과 같은 것들은 Protocol이지 않을까요?

-> UIViewControllerRepresentable은 프로토콜이군요..ㅜ 프리뷰의 기능을 넣을때 사용하는 바람의 SwiftUI에서 처음 사용하는 바람에 놓친 부분 같습니다! 수정하도록 하겠습니다!

프로토콜들에 대한 채택은 inherit(상속)보다는 conform(준수)이라고 표기하는게 낫지 않을까요?
-> 사실 명칭의 경우 프로토콜을 채택하는경우 준수가 맞지만 swift 언어 특성상 단일 상속이 되는 언어의 경우 상속과 준수의 개념에 약간의 경계선이 크지 않다고 판단했습니다! 결국 단일 상속되는 과정에서 추가적인 상속이 필요하여 프로토콜이라는 것을 추가적으로 채택하여 필요 기능을 구현한다라고 판단하여 inherit라고 작성했는데 이부분은 다시 수정해보도록 하겠습니다!

작성 툴은 어떤 것을 쓰신걸까요? (draw.io같은데 맞을까요?)
-> 제가 사용하는 툴은 [SwiftPlantUML]입니다! 이번에 처음 사용하는 부분이라 부족하고 놓친 부분이 많았네요 ㅜ
다음에는 보다 꼼꼼하고 제대로 확인하도록 하겠습니다!

Copy link

@ictechgy ictechgy left a comment

Choose a reason for hiding this comment

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

안녕하세요 @kimbs5899 ! 리뷰 반영하시느라 고생하셨습니다~ 👍👍👍

현재 연락처 추가화면쪽이 안보이는데 확인 부탁드릴게요!
해당 부분만 수정되면 머지할 수 있을 것 같습니다.

@@ -27,30 +22,24 @@ extension ContactListViewController {

override func viewDidLoad() {
super.viewDidLoad()
contactListView.tableView.delegate = self

Choose a reason for hiding this comment

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

STEP3의 내용이 들어간 것 같네요! 맞을까요??

override func viewDidLoad() {
super.viewDidLoad()
override func loadView() {
super.loadView()

Choose a reason for hiding this comment

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

super.loadView()를 써주셨네요! 해당 구문은 꼭 필요한걸까요?? loadView 공식문서를 한번 살펴봐주시면 좋을 것 같습니다!

Comment on lines 61 to 62
let isTextFieldValidResultDictionary = isTextFieldValid(name: name, age: age, phoneNumber: phoneNumber)
checkNameTextFieldIsEmpty(name: name) ? showMessageAlertWithActions(inputMessage: CustomAlertString.isEmptyNameAlertMessage.description, actionList: [okAction]) : isValidCheckAndShowAlert(inputValidDictionary: isTextFieldValidResultDictionary,contactInfoName: name, contactInfoAge: Int(age) ?? 0, contactInfoPhoneNumber: phoneNumber)

Choose a reason for hiding this comment

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

분리 잘 해주셨습니다. 다만 조금 더 분리해볼 수 있을 것 같아요!
더불어 삼항연산자를 써주셨는데 한줄이 너무 길어지는 것 같아요. 다른 방법으로 하면 어떨까요?

return formatTextVaildCheck(formatText: formatText)
}

private func formatTextVaildCheck(formatText: String) -> String {

Choose a reason for hiding this comment

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

유효성 검사 로직은 없는 것 같은데 ValidCheck라는 네이밍이 들어간 이유가 있을까요??

Comment on lines 19 to 44
override init(frame: CGRect) {

nameLabel = UILabel()
ageLabel = UILabel()
phoneNumberLabel = UILabel()

nameTextField = UITextField()
ageTextField = UITextField()
phoneNumberTextField = UITextField()

nameTextFieldView = UIStackView()
ageTextFieldView = UIStackView()
phoneNumberTextFieldView = UIStackView()
stackView = UIStackView()

super.init(frame: frame)

setupLabel()
setupTextField()
setupTextFieldView()
setupStackView()

setupUI()
setupSubView()
setupKeyboard()
setupConstraints()

Choose a reason for hiding this comment

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

깔끔하네요!

Comment on lines 55 to 57
nameLabel = configureLabel(with: CustomValidString.nameText.description)
ageLabel = configureLabel(with: CustomValidString.ageText.description)
phoneNumberLabel = configureLabel(with: CustomValidString.phoneNumberText.description)

Choose a reason for hiding this comment

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

세 label 인스턴스는 init에서 이미 생성해주었는데 여기서 새로 생성해서 재할당을 해주네요! 이렇게 만드신 이유가 있을까요? 🤔

Comment on lines 101 to 106
if label == nameLabel {
nameTextFieldView = stack
} else if label == ageLabel {
ageTextFieldView = stack
} else if label == phoneNumberLabel {
phoneNumberTextFieldView = stack

Choose a reason for hiding this comment

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

여기도 프로퍼티에 이미 StackView 인스턴스가 초기화되어있는데 재할당을 해주고 계시네요! 🤔 확인 부탁드릴게요~

stackView.translatesAutoresizingMaskIntoConstraints = false
}

private func setupConstraints() {

Choose a reason for hiding this comment

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

연락처 추가화면쪽에 뷰가 제대로 표시되지 않는 것 같아요~! 확인 부탁드릴게요!

Copy link

@ictechgy ictechgy left a comment

Choose a reason for hiding this comment

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

@kimbs5899 고생하셨습니다!
라인별로 코멘트를 추가적으로 남겼습니다. 마지막으로 확인 부탁드릴게요.

더불어 아래의 사항들 확인해보시면 좋을 것 같습니다.

  • 앱 시작 후 연락처 추가 시 'Save'를 누르면 앱 crash
  • 연락처에 12자리 이상 입력하면 '-'이 다 사라짐

개발 후에 동작여부를 꼼꼼히 체크하는 것은 반드시 필요한 과정이라는 점, 알아주시면 좋을 것 같아요~! 😄

3주동안 고생 많으셨습니다!

Comment on lines +62 to +63
let isTextFieldValidResultDictionary = isTextFieldValid(inputContact: decodeContact(input: textList))
checkNameTextFieldIsEmpty(name: name) ? showMessageAlertWithActions(inputMessage: CustomAlertString.isEmptyNameAlertMessage.description, actionList: [okAction]) : isValidCheck(inputValidDictionary: isTextFieldValidResultDictionary,contactInfo: decodeContact(input: textList))

Choose a reason for hiding this comment

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

지금도 충분히 잘 분리해주셨는데요! 제가 생각했던 코드는 아래와 같습니다. 한번 참고해보시면 좋을 것 같아요!

    @objc
    private func saveButtonTapped() {
        let name: String = detailView.nameTextField.text?.replacingOccurrences(of: " ", with: "") ?? ""
        let age: String = detailView.ageTextField.text ?? ""
        let phoneNumber: String = detailView.phoneNumberTextField.text ?? ""
        
        let isNameValid = isNameValid(name)
        let isAgeValid = isAgeValid(age)
        let isPhoneNumberValid = isPhoneNumberValid(phoneNumber)
        
        if isNameValid && isAgeValid && isPhoneNumberValid {
            showAllFieldIsValid()
            createNewContactItem()
            moveToContactList()
        } else {
            let validationReport = createValidationCheckReport(
                isNameValid: isNameValid,
                isAgeValid: isAgeValid,
                isPhoneNumberValid: isPhoneNumberValid
            )
            showFieldIsInvalid(from: validationReport)
        }
    }
    
    private func isNameValid(_ name: String) -> Bool {
        // name 유효성 검사
    }
    
    private func isAgeValid(_ age: String) -> Bool {
        // age 유효성 검사
    }
    
    private func isPhoneNumberValid(_ phoneNumber: String) -> Bool {
        // phone number 유효성 검사
    }
    
    private func createValidationCheckReport(
        isNameValid: isNameValid,
        isAgeValid: isAgeValid,
        isPhoneNumberValid: isPhoneNumberValid
    ) -> InvalidField {
        enum InvalidField {
            case name
            case age
            case phone
        }
        // 세가지 조건을 비교하여 위 3개의 케이스 중 하나 리턴
    }

@@ -76,34 +77,39 @@ extension DetailContactViewController {
}

private func checkNameTextFieldIsEmpty(name: String) -> Bool {

Choose a reason for hiding this comment

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

Bool을 반환하는 함수는 is/shoud/need 등~한가?라는 이름으로 떨어지게 지어주시는게 좋을 것 같아요~

return name.isEmpty ? true : false
}

private func decodeContact(input: [String]) -> Contact{

Choose a reason for hiding this comment

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

decode라는 이름은 '데이터를 디코딩한다' 라는 뜻으로 이해할 수도 있어서 여기서는 다른 네이밍으로 가져가는게 좋을 것 같습니다.
만든다, 생성한다 정도의 이름이면 좋을 것 같습니다.

let namePattern: String = CustomValidString.RegularExpressionNamePatthenText.description
let agePattern: String = CustomValidString.RegularExpressionAgePatthenText.description
let phoneNumberPattern: String = CustomValidString.RegularExpressionPhoneNumberPatthenText.description
private func isTextFieldValid(inputContact: Contact) -> [String:Bool] {

Choose a reason for hiding this comment

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

이 함수는 Bool을 반환하는 함수가 아니니 다른 네이밍을 해주는게 좋을 것 같네요!


return [CustomValidString.validDictionaryNameKeyText.description: isNameValidResult,
CustomValidString.validDictionaryAgeKeyText.description: isAgeValidResult,
CustomValidString.validDictionaryPhoneNumberKeyText.description: isPhoneNumberValidResult]
}

private func isValidCheckAndShowAlert(inputValidDictionary: [String: Bool], contactInfoName: String, contactInfoAge: Int, contactInfoPhoneNumber: String) {
private func isValidCheck(inputValidDictionary: [String: Bool], contactInfo: Contact) {

Choose a reason for hiding this comment

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

isValid와 check라는 동사가 들어가서 조금 헷갈리는데요.
checkValidation 정도면 어떨까요?

Comment on lines 5 to 17
private lazy var nameLabel: UILabel = setupLabel(text: CustomTextFieldString.nameText.description)
private lazy var ageLabel: UILabel = setupLabel(text: CustomTextFieldString.ageText.description)
private lazy var phoneNumberLabel: UILabel = setupLabel(text: CustomTextFieldString.phoneNumberText.description)

var nameTextField: UITextField
var ageTextField: UITextField
var phoneNumberTextField: UITextField
lazy var nameTextField: UITextField = setupTextField()
lazy var ageTextField: UITextField = setupTextField()
lazy var phoneNumberTextField: UITextField = setupTextField()

var nameTextFieldView: UIStackView
var ageTextFieldView: UIStackView
var phoneNumberTextFieldView: UIStackView
lazy var nameTextFieldView: UIStackView = setupTextFieldView(label: nameLabel, textField: nameTextField)
lazy var ageTextFieldView: UIStackView = setupTextFieldView(label: ageLabel, textField: ageTextField)
lazy var phoneNumberTextFieldView: UIStackView = setupTextFieldView(label: phoneNumberLabel, textField: phoneNumberTextField)

private var stackView: UIStackView
private lazy var stackView: UIStackView = setupStackView()

Choose a reason for hiding this comment

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

변수를 lazy로 만들어서 초기화시킬수도 있지만 함수 자체를 static 함수로 만들어서 처음부터 접근 가능하게 만들 수도 있을 것 같네요. 나중에 한번 시도해보세요!

}

private func configureLabel(with text: String) -> UILabel {
private func setupLabel(text: String) -> UILabel {

Choose a reason for hiding this comment

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

setup도 좋지만 여기서는 UILabel을 만들어서 return하므로 -> create/generate 정도의 이름이 좋을 것 같습니다

nameTextFieldView = configureTextFieldView(with: nameLabel, textField: nameTextField)
ageTextFieldView = configureTextFieldView(with: ageLabel, textField: ageTextField)
phoneNumberTextFieldView = configureTextFieldView(with: phoneNumberLabel, textField: phoneNumberTextField)
private func setupPhoneNumerAddTarget(_ textField: UITextField) {

Choose a reason for hiding this comment

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

동사가 두개 들어가는 네이밍인 것 같은데 하나로 만들면 어떨까요?

Comment on lines 64 to 83
let stack: UIStackView = UIStackView(arrangedSubviews: [label, textField])
stack.axis = .horizontal
stack.distribution = .fillProportionally
stack.alignment = .fill
stack.spacing = 14

if label == nameLabel {
nameTextFieldView = stack
} else if label == ageLabel {
ageTextFieldView = stack
} else if label == phoneNumberLabel {
phoneNumberTextFieldView = stack
}

stack.addSubview(label)
stack.addSubview(textField)
return stack
}

private func setupStackView() {
stackView = configureStackView()
}

private func configureStackView() -> UIStackView {
private func setupStackView() -> UIStackView {
let stack: UIStackView = UIStackView(arrangedSubviews: [nameTextFieldView, ageTextFieldView, phoneNumberTextFieldView])
stack.axis = .vertical
stack.distribution = .fillEqually
stack.alignment = .fill
stack.spacing = 14
stack.addSubview(nameTextFieldView)
stack.addSubview(ageTextFieldView)
stack.addSubview(phoneNumberTextFieldView)
return stack

Choose a reason for hiding this comment

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

UIStackview를 초기화할 때 arrangedSubviews로 하위뷰들을 추가하셨는데 아래에서 또 추가해주신 이유가 있을까요?

Comment on lines 101 to 110
nameLabel.translatesAutoresizingMaskIntoConstraints = false
nameTextField.translatesAutoresizingMaskIntoConstraints = false
nameTextFieldView.translatesAutoresizingMaskIntoConstraints = false
ageLabel.translatesAutoresizingMaskIntoConstraints = false
ageTextField.translatesAutoresizingMaskIntoConstraints = false
ageTextFieldView.translatesAutoresizingMaskIntoConstraints = false
phoneNumberLabel.translatesAutoresizingMaskIntoConstraints = false
phoneNumberTextField.translatesAutoresizingMaskIntoConstraints = false
phoneNumberTextFieldView.translatesAutoresizingMaskIntoConstraints = false
stackView.translatesAutoresizingMaskIntoConstraints = false

Choose a reason for hiding this comment

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

stackview 내부에 들어가는 요소들도 translatesAutoresizingMaskIntoConstraints 설정이 필요할까요?

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.

2 participants