-
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
Open
vanism2091
wants to merge
34
commits into
tasty-code:1_Sunny
Choose a base branch
from
SunnnySong:Step3
base: 1_Sunny
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
34 commits
Select commit
Hold shift + click to select a range
1020789
feat: Navigation bar 추가, 새 view controller 스토리보드에 생성
vanism2091 b3eea1a
feat: 새연락처 추가 화면 layout 구성
SunnnySong f233bce
feat: 연락처 추가 화면 TextField 별 Keyboard 설정
vanism2091 8e419b1
feat: AddContactView View Controller 추가
vanism2091 69b5763
feat: 연락처 hyphen 처리 함수 구현 test
vanism2091 c9e4ec2
feat: Hyphen 관련 refactoring
vanism2091 30ef6f1
refactor: LocationsOfHyphen 상수화, 주석 제거
vanism2091 7f2a628
chore: 폴더 구조 변경
SunnnySong 52196ef
feat: save, cancel 버튼 Alert 생성
SunnnySong c3fc777
chore: 지금은 사용하지 않는 변수 _ 처리
vanism2091 3524eae
chore: Rename UserInfo computed property
vanism2091 55e72e8
feat: UITableViewDiffableDataSource 구현
vanism2091 412e2d0
feat: JSONCodable protocol 생성 및 ModelData 생성
SunnnySong afdf538
Merge branch 'Step3-protocol' into Step3
SunnnySong bb48077
merge: Step3 merge with Step3-protocol
SunnnySong 33d74ac
feat: load()함수 삭제, private(set)으로 변경
SunnnySong 10d6d63
chore: JSONError의 localizedDescription 정의 수정
vanism2091 3052e8f
feat: DIffableDataSource에 swipe remove 구현
vanism2091 d432b82
feat: custom ContactCell 생성 및 tableView 연결
SunnnySong 60b2c8c
feat: 연락처 hyphen 처리 함수 구현 test
vanism2091 2db829e
chore: fix merge error...
vanism2091 2f50ef3
fix: merge를 해결하며 발생한 오류 수정
vanism2091 045de4c
chore: Protocols directory 생성
vanism2091 fdc8904
feat: dequeueresuablecell 관련 코드 extension 정의
SunnnySong b6508ee
Merge branch 'Step3-extension' into Step3
SunnnySong 4d97263
feat: 연락처 추가 구현 및 연락처 encoding
SunnnySong 14f6592
refactor: 은닉화 적용, 컨벤션 통일
vanism2091 279f6aa
refactor: UserInfo Hashable extension 수정
vanism2091 daf7dda
feat: file error 추가
vanism2091 059a47f
style: 붙이지 않아도 되는 self 삭제
vanism2091 949c28b
feat: AddContactView에서 TextField AutoLayout Dynamic type 적용
vanism2091 485f186
fix: hyphen 추가 로직 수정
vanism2091 9791041
refactor: Literal Number 10을 상수로 설정
vanism2091 8810ce3
style: 주석 파일 이름 수정
vanism2091 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Binary file not shown.
86 changes: 86 additions & 0 deletions
86
ios-contact-manager-ui/ContactManagerUI/AddContactViewController.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| // | ||
| // AddContactViewController.swift | ||
| // ContactManagerUI | ||
| // | ||
| // Created by sei_dev on 2/6/23. | ||
| // | ||
|
|
||
| import UIKit | ||
|
|
||
| final class AddContactViewController: UIViewController { | ||
| @IBOutlet weak var contactTextField: UITextField! | ||
|
|
||
| override func viewDidLoad() { | ||
| super.viewDidLoad() | ||
| contactTextField.delegate = self | ||
| } | ||
| } | ||
|
|
||
| extension AddContactViewController: UITextFieldDelegate { | ||
| func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { | ||
| if let oldText = textField.text, let textRange = Range(range, in: oldText) { | ||
| // newText == 현재 text field에 있는 텍스트. 아직 textField에는 업데이트 되지 않은 상태 | ||
| let newText = oldText.replacingCharacters(in: textRange, with: string) | ||
|
|
||
| // range 정보를 갖고 parse한 결과를 textField의 text에 할당 | ||
| textField.text = parse(newText, using: range) | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| 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 | ||
| } | ||
| } | ||
|
|
||
| private func removeLastHyphen(from text: String) -> String { | ||
| if text.last == Character.hyphen { | ||
| return String(text.dropLast()) | ||
| } | ||
| return text | ||
| } | ||
|
|
||
| private func relocateHyphen(of string: String, locationsOfHyphen indices: [Int]) -> String { | ||
| var relocated = string.replacingOccurrences(of: String.hyphen, with: String.empty) | ||
| for index in indices { | ||
| relocated.insert(Character.hyphen, at: index) | ||
| } | ||
| return relocated | ||
| } | ||
| } | ||
|
|
||
|
|
||
| extension String { | ||
| static var hyphen: String { "-" } | ||
| static var empty: String { "" } | ||
|
|
||
| mutating func insert(_ newElement: Character, at i: Int) { | ||
| self.insert(newElement, at: self.index(self.startIndex, offsetBy: i)) | ||
| } | ||
|
|
||
| func inserting(_ newElement: Character, at i: Int) -> String { | ||
| var newString = self | ||
| newString.insert(newElement, at: self.index(self.startIndex, offsetBy: i)) | ||
| return newString | ||
| } | ||
| } | ||
|
|
||
| extension Character { | ||
| static var hyphen: Character { "-" } | ||
| } | ||
57 changes: 0 additions & 57 deletions
57
ios-contact-manager-ui/ContactManagerUI/Base.lproj/Main.storyboard
This file was deleted.
Oops, something went wrong.
File renamed without changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
14 changes: 0 additions & 14 deletions
14
ios-contact-manager-ui/ContactManagerUI/ContactError.swift
This file was deleted.
Oops, something went wrong.
28 changes: 0 additions & 28 deletions
28
ios-contact-manager-ui/ContactManagerUI/ContactViewController.swift
This file was deleted.
Oops, something went wrong.
117 changes: 117 additions & 0 deletions
117
ios-contact-manager-ui/ContactManagerUI/Controllers/AddContactViewController.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| // | ||
| // AddContactViewController.swift | ||
| // ContactManagerUI | ||
| // | ||
| // Created by sei_dev on 2/6/23. | ||
| // | ||
|
|
||
| import UIKit | ||
|
|
||
| final class AddContactViewController: UIViewController { | ||
|
|
||
| @IBOutlet private weak var nameTextField: UITextField! | ||
| @IBOutlet private weak var ageTextField: UITextField! | ||
| @IBOutlet private weak var contactTextField: UITextField! | ||
|
|
||
| weak var newContactDelegate: NewContactDelegate? | ||
|
|
||
| private let contactTextHyphenPolicyLength = 10 | ||
|
|
||
| override func viewDidLoad() { | ||
| super.viewDidLoad() | ||
| contactTextField.delegate = self | ||
| } | ||
|
|
||
| @IBAction private func saveButtonTapped(_ sender: Any) { | ||
| guard let name = nameTextField.text, | ||
| let age = ageTextField.text, | ||
| let contact = contactTextField.text else { | ||
| return | ||
| } | ||
|
|
||
| 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) | ||
| } | ||
| } | ||
|
|
||
| @IBAction private func cancelButtonTapped(_ sender: Any) { | ||
| makeCancelAlert() | ||
| } | ||
| } | ||
|
|
||
| extension AddContactViewController { | ||
| private func makeErrorAlert(description: String) { | ||
| let alert = UIAlertController( | ||
| title: description, | ||
| message: nil, | ||
| preferredStyle: .alert | ||
| ) | ||
| let checkAction = UIAlertAction(title: "확인", style: .default) | ||
| alert.addAction(checkAction) | ||
| present(alert, animated: true) | ||
| } | ||
|
|
||
| private func makeCancelAlert() { | ||
| let alert = UIAlertController( | ||
| title: "정말로 취소하시겠습니까?", | ||
| message: nil, | ||
| preferredStyle: .alert | ||
| ) | ||
| let noAction = UIAlertAction(title: "아니오", style: .default) | ||
| let yesAction = UIAlertAction(title: "예", style: .destructive) { [weak self] _ in | ||
| self?.dismiss(animated: true) | ||
| } | ||
| alert.addAction(noAction) | ||
| alert.addAction(yesAction) | ||
| alert.preferredAction = yesAction | ||
| present(alert, animated: true) | ||
| } | ||
| } | ||
|
|
||
| extension AddContactViewController: UITextFieldDelegate { | ||
| func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { | ||
| if let oldText = textField.text, let textRange = Range(range, in: oldText) { | ||
| let newText = oldText.replacingCharacters(in: textRange, with: string) | ||
| textField.text = parse(newText, using: range) | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| private enum LocationsOfHyphen { | ||
| static let forTwoFourFour = [2, 7] | ||
| static let forTwoThreeFour = [2, 6] | ||
| static let forThreeFourFour = [3, 8] | ||
| } | ||
|
|
||
| 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 removeLastHyphen(from text: String) -> String { | ||
| if text.last == Character.hyphen { | ||
| return String(text.dropLast()) | ||
| } | ||
| return text | ||
| } | ||
|
|
||
| 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 | ||
| } | ||
| } |
78 changes: 78 additions & 0 deletions
78
ios-contact-manager-ui/ContactManagerUI/Controllers/ContactViewController.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| // | ||
| // ContactViewController.swift | ||
| // ContactManagerUI | ||
| // | ||
| // Created by 송선진 on 2023/01/30. | ||
| // | ||
|
|
||
| import UIKit | ||
|
|
||
| final class ContactViewController: UIViewController, UITableViewDelegate { | ||
| private enum Section { | ||
| case main | ||
| } | ||
|
|
||
| @IBOutlet private weak var tableView: UITableView! | ||
| private var dataSource: DataSource? | ||
|
|
||
| override func viewDidLoad() { | ||
| super.viewDidLoad() | ||
| setupTableView() | ||
| setupDataSource() | ||
| } | ||
|
|
||
| override func prepare(for segue: UIStoryboardSegue, sender: Any?) { | ||
| guard let addViewController = segue.destination as? AddContactViewController else { | ||
| return | ||
| } | ||
| addViewController.newContactDelegate = self | ||
| } | ||
|
|
||
| private func setupTableView() { | ||
| tableView.dataSource = dataSource | ||
| tableView.register(ContactCell.self) | ||
| } | ||
| } | ||
|
|
||
| extension ContactViewController { | ||
| private func setupDataSource() { | ||
| dataSource = DataSource(tableView: tableView) { tableView, indexPath, itemIdentifier in | ||
| let cell = tableView.dequeue(ContactCell.self, cellForRowAt: indexPath) | ||
| cell.content = itemIdentifier | ||
| return cell | ||
| } | ||
| dataSource?.update(animatingDifferences: false) | ||
| } | ||
| } | ||
|
|
||
| extension ContactViewController { | ||
| private final class DataSource: UITableViewDiffableDataSource<Section, UserInfo> { | ||
| typealias Snapshot = NSDiffableDataSourceSnapshot<Section, UserInfo> | ||
|
|
||
| func update(animatingDifferences: Bool) { | ||
| var snapshot = Snapshot() | ||
| snapshot.appendSections([.main]) | ||
| snapshot.appendItems(ContactsController.shared.contacts) | ||
| apply(snapshot, animatingDifferences: animatingDifferences) | ||
| } | ||
|
|
||
| override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { | ||
| return true | ||
| } | ||
|
|
||
| override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { | ||
| if editingStyle == .delete { | ||
| if itemIdentifier(for: indexPath) != nil { | ||
| ContactsController.shared.remove(index: indexPath.row) | ||
| update(animatingDifferences: true) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| extension ContactViewController: NewContactDelegate { | ||
| func addNewContact() { | ||
| dataSource?.update(animatingDifferences: true) | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
오우... 결국 각 케이스를 구분해서 정리하셨군요
고생하셨습니다 😆