# ios-cantact-manager-UI
## ๐ฐTeam BUJA
๐๐ป๐๐ปโโ๏ธ๐จ **ํ๋ก์ ํธ ๊ธฐ๊ฐ:** `23.01.30` ~ `23.02.10`
|<img src="https://avatars.githubusercontent.com/u/71758542?v=4" width=200 style="border-radius: 70%">|<img src="https://avatars.githubusercontent.com/u/92699723?v=4" width=200 style="border-radius: 70%">|
|:---:|:---:|
|[Blu](https://github.com/bomyuniverse)|[Jason](https://github.com/JasonLee0223)|
----
[[(Ver.Console) Previous Project] ios-contact-manager](https://github.com/calledBlu/ios-cantact-manager)
<a href ="#1-Step1---iOS App Target ์ถ๊ฐ">Step1 - iOS App Target ์ถ๊ฐ</a>
<a href ="#2-Step2---์ฐ๋ฝ์ฒ ๋ชฉ๋ก ๊ตฌํ">Step2 - ์ฐ๋ฝ์ฒ ๋ชฉ๋ก ๊ตฌํ</a>
<a href ="#3-Step3---์ฐ๋ฝ์ฒ ์ถ๊ฐ ๊ธฐ๋ฅ ๊ตฌํ
">Step3 - ์ฐ๋ฝ์ฒ ์ถ๊ฐ ๊ธฐ๋ฅ ๊ตฌํ
</a>
----
## ๐ ์ญํ ๋ถ๋ฐฐ
|Controller|์ญํ |
|:---|:---|
|ContactManagerTableViewController|- ํ๋ก์ ํธ ์คํ์ ์ฒซ ํ๋ฉด <br/>- ์ฐ์ธก ์๋จ์ '+'๋ฒํผ์ ํตํด AddNewContactViewController ํ๋ฉด์ผ๋ก Modalํํ๋ก ๋์ด๊ฐ๋ค. <br/>- ์๋ก์ด ์ฐ๋ฝ์ฒ๊ฐ ์ ์ฅ๋๋ฉด reload()๋ฅผ ํตํด ์
๋ฐ์ดํธํ๋ค.|
|AddNewContactViewController|- ์ด๋ฆ, ๋์ด, ์ฐ๋ฝ์ฒ ์์๋ก ๋ฐ์ดํฐ๋ฅผ ์
๋ ฅํ ์ ์๋๋ก ํ๋ค. <br/>- ํ๋ฉด ์๋จ์ ์ข,์ฐ์ธก์ผ๋ก ์ ์ฅํ ์ ์๊ฑฐ๋ ์
๋ ฅ์ ์ทจ์ํ๊ณ ์ฒซ ํ๋ฉด์ผ๋ก ๋์๊ฐ๋ค.|
|View|์ญํ |
|:---|:---|
|UITableView|TableView ํํ๋ก ์ฐ๋ฝ์ฒ์ ๋ชฉ๋ก์ ๋ณด์ฌ์ค๋ค.|
|Alert|์ฌ์ฉ์ ์
๋ ฅ์ ๋ง์ถฐ ์๋ชป์
๋ ฅํ๋ฉด ์ฌ๋ฐ๋ฅด๊ฒ ์
๋ ฅํ๋ผ๋ ๊ฒฝ๊ณ ๋ฌธ์ Alertํํ๋ก ๋์์ค๋ค.|
|struct/class|์ญํ |
|:---|:---|
|JSONManangement|Local File์ธ `dummy.json`ํ์ผ์ ๋ถ๋ฌ์์ ContactInformation์ JSONํํ๋ก Decodingํ์ฌ Loadํด์ค๋ค.|
|enum|์ญํ |
|:---|:---|
|JSONErrors|JSON Parsing ๊ณผ์ ์ค ์๋ฌ๊ฐ ๋ฐ์ํ๋ ์ผ์ด์ค๋ฅผ ์ ์ ๋ฐ ํธ์ถ๋๋ค.|
|protocol|์ญํ |
|:---|:---|
|ReusableTableViewCell|ReusableTableViewCellํ์
๊ณผ ๋ง๋ Cell๋ง์ ์ฌ์ฌ์ฉํ๋๋ก ํ๋ค.|
|SendContactDataDelegate|delegate๋ฅผ ํตํด ์ฌ์ฉ์๊ฐ Save ๋ฒํผ์ ๋๋ฅด๋ฉด ContactInformationํํ๋ก ์ ์ฅํ๋ค.|
|JSONParsable|JSON Parsing์ ๊ดํ ๋ฉ์๋ ์ ์|
----
## Step1 - iOS App Target ์ถ๊ฐ
- ์ด์ ํ๋ก์ ํธ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก UIKit App Target ์ถ๊ฐ
- ContactManager ํ๊น์ ์ฃผ์ ํ์ผ์ ContactManagerUI์ ํ๊น ๋ฉค๋ฒ์ญ ํ์ผ๋ก ์ถ๊ฐ
๊ธฐ์กด์ ํ๋ก์ ํธ์ UI ๋ฒ์ ์ ์ถ๊ฐ ๊ตฌํํ๊ธฐ ์ํด ์ ํ๋์ด์ผ ํ๋ ๋จ๊ณ๋ก, ์ฑ ํ๊ฒ ์ค์ ๊ณผ์ ์ ๋ค์๊ณผ ๊ฐ๋ค.
1. ContactManager์ ํ๋ก์ ํธ ์ค์ ์์ TARGETS ๋ชฉ๋ก ํ๋จ์ + ๋ฒํผ์ ๋๋ฅธ๋ค.
2. iOS-App์ ์ ํํ๋ค
3. ContactManagerUI๋ก ๋ค์ด๋ฐ ํ ํ๊ฒ์ ์์ฑํ๋ค.
4. ContactManager์ ์ฃผ์ ํ์ผ์ ์ธ์คํํฐ์์ ContactManagerUI๋ฅผ ํ๊น ๋ฉค๋ฒ์ญ ํ์ผ๋ก ์ถ๊ฐํ๋ค.
## Step2 - ์ฐ๋ฝ์ฒ ๋ชฉ๋ก ๊ตฌํ
[PR #7 | Step1~2 - iOS App Target ์ถ๊ฐ ๋ฐ ์ฐ๋ฝ์ฒ ๋ชฉ๋ก ๊ตฌํ](https://github.com/tasty-code/ios-contact-manager-ui/pull/7#issue-1563831481)
- Table View๋ฅผ ํ์ฉํ์ฌ ์ฐ๋ฝ์ฒ ๋ชฉ๋ก์ ํ๋ฉด์ ํ์
- ๊ฐ ํ์ cell์ subtitle style ์ ์ฉ
- ํ ํ์ ์ด๋ฆ, ๋์ด, ์ฐ๋ฝ์ฒ ํ์
- Dummy Data๋ฅผ JSON์ผ๋ก ๊ตฌํํ์ฌ Decoding ํTable View Data์ ์ ์ฉ
- Protocol & Generic์ ์ฌ์ฉํ์ฌ cell identifier๋ฅผ ์ง์ ์
๋ ฅํ์ง ์๊ณ ๋ฉํํ์
์ผ๋ก ์ถ๋ก ํ์ฌ ์ฌ์ฌ์ฉํ๋๋ก ๊ตฌํ
UITableView๋ฅผ ์ด์ฉํ์ฌ ์ฐ๋ฝ์ฒ๋ค์ ํ๋ฉด์ ํ์ํ๋ ๊ธฐ๋ฅ์ ๊ตฌํํ๋ ๋จ๊ณ์ด๋ค. Custom cell์ ์์ฑํ์ง ์๊ณ , ์คํ ๋ฆฌ๋ณด๋์์ ๊ธฐ๋ณธ์ ์ผ๋ก ์ ๊ณตํ๋ Prototype cell์ ์ฌ์ฉํ์๋ค.
iOS 14๋ถํฐ `cell.textLabel?.text` ํํ๋ก ์ง์ ์ ๊ทผํ๋ ๊ฒ์ด ์๋ content configuration์ ํตํ์ฌ cell์ ๊ตฌ์ฑํ๋๋ก ๊ฐํธ๋์ด์ ํด๋น ๋ถ๋ถ์ ์ ์ฉํด ๋ณด์๋ค.
### ๐ ์ ์ฉํ๋ ค๊ณ ๋
ธ๋ ฅํด๋ณธ ์
ํ๋ก์ ํธ ๊ธฐ๊ฐ๋์ JSON์ ๋ํด ํ์ตํ๊ฒ๋์ด ์์ง Server๋ก ๋ถํฐ API ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ค์ง๋ ๋ชปํ์ง๋ง
FilePath๋ฅผ ํตํด JSON Parshing์ ๊ฒฝํํด๋ณด๊ณ ์ํ์๋ค!
์ฒ์์ Codable์ด ๋์ค๊ธฐ ์ด์ ์ ์ฌ์ฉํ๋ `JSONSerialization`์ ๊ฒฝํํด๋ณด๊ณ `JSONDecoder`๋ฅผ ์ฌ์ฉํ์ฌ ๋ฆฌํํ ๋งํด๋ณด๋ ๊ฒฝํ์ ํด๋ณด์๋ค.
```Swift
// JSONSerialization ์ฌ์ฉ
private func testJSON() {
guard let filePath = Bundle.main.url(forResource: "Dummy", withExtension: "json") else { return }
if let data = try? Data(contentsOf: filePath) {
let json = try! JSONSerialization.jsonObject(with: data, options: []) as! [String: Any]
contactInfomation = json
}
if let contactInfo = contactInfomation["Dummy"] as? [[String: Any]] {
contactInfo.forEach { contactData in
nameArr.append(contactData["name"] as! String)
ageArr.append(contactData["age"] as! String)
phoneNumberArr.append(contactData["phoneNumber"] as! String)
}
}
```
```Swift
// JSONDecoder ์ฌ์ฉ
func loadJSON<T>(_ filename: String) throws -> T where T : Decodable {
let data: Data
guard let filePath = Bundle.main.url(forResource: filename, withExtension: nil) else {
print("\(filename) not found.")
throw JSONErrors.notFoundJSONFile
}
do {
data = try Data(contentsOf: filePath)
} catch {
print("Could not load \(filename): (error)")
throw JSONErrors.notLoadData
}
do {
let JSONDecoder = JSONDecoder()
return try JSONDecoder.decode(T.self, from: data)
} catch {
print("Unable to decode \(filename): (error)")
throw JSONErrors.unableToDecode
}
}
```
## Step3 - ์ฐ๋ฝ์ฒ ์ถ๊ฐ ๊ธฐ๋ฅ ๊ตฌํ
[PR #19 | Step3 - ์ฐ๋ฝ์ฒ ์ถ๊ฐ ๊ธฐ๋ฅ ๊ตฌํ](https://github.com/tasty-code/ios-contact-manager-ui/pull/19)
- ๋ค๋น๊ฒ์ด์
์ปจํธ๋กค๋ฌ ๊ตฌํ
- ์ฐ๋ฝ์ฒ ๋ชฉ๋ก ํ๋ฉด์ ์ฐ์๋จ + ๋ฒํผ์ ํตํด ์ฐ๋ฝ์ฒ ์ถ๊ฐ ํ๋ฉด์ผ๋ก ์ง์
(Modal)
- AutoLayout ๊ณผ StackView๋ฅผ ํ์ฉํ์ฌ ๋ ์ด์์ ๊ตฌ์ฑ
- ๊ฐ ํ๋์ ๋ง๋ ํค๋ณด๋ ์ข
๋ฅ ์ง์
- ์ด๋ฆ: ASCII Capable
- ๋์ด: Number Pad
- ์ฐ๋ฝ์ฒ: Phone Pad
- ์ ์ฐ๋ฝ์ฒ ์ถ๊ฐ ํ๋ฉด์์ ๋ค๋น๊ฒ์ด์
๋ฐ์ ๋ฒํผ์ ๋๋ฅด๋ ๊ฒฝ์ฐ ์๋ฆผ์ฐฝ์ด ๋ํ๋๋๋ก ๊ตฌํ
- ์ทจ์ ๋ฒํผ์ ๋๋ฅด๋ฉด ์ ๋ง ์ทจ์ํ ๊ฒ์ธ์ง ๋ฌป๋๋ก ์๋ฆผ์ฐฝ ๊ตฌํ
- ์ ์ฅ ๋ฒํผ์ ๋๋ฅด๋ฉด ์
๋ ฅํ ์ ๋ณด๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ์
๋ ฅ๋์๋์ง ํ์ธํ๊ณ ์ฌ๋ฐ๋ฅด์ง ์์ ๊ฒฝ์ฐ ํด๋น ์ ๋ณด๋ฅผ,
์ฌ๋ฐ๋ฅธ ๊ฒฝ์ฐ ์ฐ๋ฝ์ฒ ๋ชฉ๋ก ํ๋ฉด์ผ๋ก ์ด๋ํ ํ ์
๋ ฅํ ์ฐ๋ฝ์ฒ๋ฅผ ๋ชฉ๋ก์ ์ถ๊ฐํ๋๋ก ๊ตฌํ
- ๊ฐ ํ๋์ ์
๋ ฅ๋ ๋ฐ์ดํฐ๋ฅผ save ๋ฒํผ์ ๋๋ฅผ ๋ ๊ฒ์ฆํ๋๋ก ๊ตฌํ
- ์ด๋ฆ์ ์ฌ์ฉ์๊ฐ ์ค๊ฐ์ ๋์ด์ฐ๊ธฐ๋ฅผ ํ๋๋ผ๋ ๋์ด์ฐ๊ธฐ๋ฅผ ์ ๊ฑฐ
- ๋์ด๋ ์ซ์๋ก๋ง ์
๋ ฅ, ์ธ ์๋ฆฌ์ ์ดํ
- ์ฐ๋ฝ์ฒ๋ ์ค๊ฐ์ -๋ก ๊ตฌ๋ถ, -๋ ๋ ๊ฐ ์กด์ฌ, -์ ์ ์ธํ๊ณ ์ซ์๋ 9์๋ฆฌ ์ด์
์ ์ฐ๋ฝ์ฒ๋ฅผ ์ถ๊ฐํ ์ ์๋ ํ๋ฉด์ ๊ตฌ์ฑํ๊ณ ์ฐ๋ฝ์ฒ๋ฅผ ์ค์ ๋ก ๋ชฉ๋ก์ ์ถ๊ฐํ ์ ์๋๋ก ํ๋ ๋จ๊ณ์ด๋ค.
ํ๋ก์ ํธ ์๊ตฌ์ฌํญ์ ์ดํด๋ณธ ํ Present Modal ๋ฐฉ์์ผ๋ก ํ๋ฉด์ ๋์ฐ๊ธฐ๋ก ๊ฒฐ์ ํ์๊ณ , ์ฝ๋๋ฅผ ํตํ์ฌ ๊ตฌํํ์๋ค.
data๋ ์ฑ๊ธํค์ ์ฑํํ์ง ์๊ณ ์์ด์ delegate pattern์ ์ฑํํ์ฌ `SendContactDataDelegate`๋ฅผ ํ๋กํ ์ฝ๋ก ๊ตฌํ, ์ฐ๋ฝ์ฒ ๋ชฉ๋ก ๋ทฐ์์ extension์ผ๋ก ํจ์๋ฅผ ๊ตฌํํ์ฌ ๋ฐ์ดํฐ๋ฅผ ์ ๋ฌ๋ฐ๊ณ ํด๋น ๋ฐ์ดํฐ๊ฐ ๋ฐ์๋๋๋ก table view๋ฅผ reloadํ๋๋ก ๊ตฌํํ์๋ค.
---
## **๐ฏ ํธ๋ฌ๋ธ ์ํ
**
### 1๏ธโฃ API Design GuideLine์ ๋ง์ถ Refactoring
์ด๋ป๊ฒ ๋ณด๋ฉด ๊ธฐ๋ฅ์ ๋ํ ๋ฌธ์ ์ ์ ์๋์์ง๋ง ๊ตฌํ ๊ณผ์ ์์ ๋ถํธํจ์ด ์์ด ์ ํญ๋ชฉ์์ ๋ค๋ฃจ์๋ค.
์ด์ ํ๋ก์ ํธ๋ฅผ ์งํํ๋ฉด์ API Design Guide๋ฅผ ์ต๋ํ ๋ฐ๋ผ๊ฐ๋ณด๋ ค ํ์์ง๋ง ์ต์ํ์ง ์๋ค๋ณด๋
์ด๋ ค์์ด ๋ฐ์ํ์๊ณ ์๋์ ์ฝ๋๋ฅผ ์์๋ก ์ข ๋ ๋ฌธ์ฅ์ฒ๋ผ ์ ์ฝํ๋๋ก ๋ฆฌํํ ๋งํ์๋ค.
```Swift
// โ๏ธ
class Converter {
func convertToCharacter(this sentence: String) -> [Character] {
var characterArray = [Character]()
for index in sentence {
characterArray.append(index)
}
return characterArray
}
func convertToString(_ word: [Character]) -> String {
return String(word)
}
}
```
```Swift
// โ
class Converter {
func renderToCharacter(_ sentence: String) -> [Character] {
var characters = [Character]()
for character in sentence {
characters.append(character)
}
return characters
}
func renderToString(_ characters: [Character]) -> String {
return String(characters)
}
}
```
### 2๏ธโฃ ReusableTableViewCell ํ๋กํ ์ฝ์ ์ฑํํ TableViewCell
Step1~2๋ฅผ ์งํํ๋ฉด์ Review๋ฅผ ๋ฐ์ ๋ด์ฉ์์ ์๋์ ๊ฐ์ ์๊ฒฌ์ ๋ฐ๊ฒ๋์ด ๋ฆฌํํ ๋ง์ ํ๊ฒ ๋์๋ค.
prototype cell์ ํ์ฉํ๋ฉด์ `cellIdentifier`๋ฅผ ์ง์ ์ง์ ํด์ฃผ๊ณ , ์ด๋ฅผ ๋งค๊ฐ๋ณ์์ ์ง์ ํ ๋นํ์ฌ `dequeueReusableCell` ํธ์ถ ์ ์ฌ์ฉํ ์ ์๋๋ก ํ์์ผ๋, PR review ์งํ ์ค์ ๋ฆฌ๋ทฐ์ด์ ์ถ์ฒ์ผ๋ก Protocol & Generic์ ์ฌ์ฉํ์ฌ ํด๋จผ ์๋ฌ์ ๋ฐ์์ ์ค์ผ ์ ์๋๋ก ๊ฐ์ ํด ๋ณด์๋ค.
> `cellIdentifier`๋ ๊ฒฐ๊ตญ TableViewCell์ ์ด๋ฆ๊ณผ ๋์ผํ ๊ฒ์ธ๋ฐ ๋งค๋ฒ ์ ๋ ๊ฒ ์ง์ ์์ฑํ๊ฒ๋๋ฉด, ํด๋จผ์๋ฌ๊ฐ ๋ฐ์ํ ํ๋ฅ ๋ ์๊ณ ์ด๋ํ์ชฝ์ด ๋ณ๊ฒฝ๋์์๋ ๋ค๋ฅธ์ชฝ๋ ์ด๋ฆ์ ๊ฐ์ด ๋ณ๊ฒฝํด์ฃผ์ด์ผํ ๊ฒ์
๋๋ค.
์ด๋ฅผ ์ ๋ค๋ฆญ๊ณผ protocol์ ์ฌ์ฉํด์ ๊ฐํธํ๊ฒ ์ฒ๋ฆฌํ๋ ๋ฐฉ๋ฒ์ด ์์ผ๋ ํ ๋ฒ ์ฐธ๊ณ ํด๋ณด์๋ฉด ์ข์ ๊ฒ ๊ฐ๋ค์.
```Swift
// โ๏ธ
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "infoCell"
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath)
var infoContent = cell.defaultContentConfiguration()
infoContent.text = "Name(age)"
infoContent.secondaryText = "contact-number"
cell.contentConfiguration = infoContent
return cell
}
```
```Swift
// โ
// ๊ตฌํ๋ถ
// Protocol์ ์ฑํํ์ฌ ํ์ํ Cell Type๋ง ์ฌ์ฉ
extension UITableViewCell: ReusableTableViewCell {}
// UITableView๋ฅผ ํ์ฅํ์ฌ Custom Method ๊ตฌํ
extension UITableView {
func dequeueReusableCell<T: UITableViewCell>(cellClass: T.Type, for indexPath: IndexPath) -> T {
guard let cell = dequeueReusableCell(withIdentifier: T.reuseIdentifier, for: indexPath) as? T else {
fatalError("Unable Dequeue Reusable")
}
return cell
}
}
// ํธ์ถ๋ถ
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(cellClass: UITableViewCell.self, for: indexPath)
cell.configurationUpdateHandler = { cell, state in
var infoContent = cell.defaultContentConfiguration().updated(for: state)
infoContent.text = "\(self.contactInfomation[indexPath.row].name)(\(self.contactInfomation[indexPath.row].age))"
infoContent.secondaryText = self.contactInfomation[indexPath.row].phoneNumber
cell.accessoryType = .disclosureIndicator
cell.contentConfiguration = infoContent
}
return cell
}
```
1. ํด๋น prototype cell์ด `UITableViewCell`์ ์์๋ฐ๊ฒ ํ๋ค.
2. prototype cell์ identifier๋ฅผ `UITableViewCell`๋ก ์์ ํ๋ค.
์์ ๊ณผ์ ์ผ๋ก ์ธํด ๋ฆฌ๋ทฐ์ด๊ฐ ์ ์ํ ํด๋จผ ์๋ฌ์ ๋ฐ์ ๊ฐ๋ฅ์ฑ์ ์ค์ด๋ค์์ผ๋, ๋งค๋ฒ cell์ ํ์
์ ์ง์ ํด์ฃผ๋ ์์
์ด ๋ถํ์ํ๋ค๊ณ ์๊ฐ๋์ด ๋ฉํ ํ์
์ ํ์ฉํ๋ ๋ฐฉ๋ฒ์ผ๋ก ๋ค์ ํ๋ฒ ๋ฆฌํฉํ ๋ง์ ์งํํ์๋ค.
๋ฉํ ํ์
์ ํ์ฉํ์ฌ ๊ฐ์ ๋ ์ฝ๋๋ `dequeueReusableCell`๋ฅผ ํธ์ถํ ๋ ํด๋น ์
์ ํด๋์ค์ ๋ฉํํ์
์ ๋งค๊ฐ๋ณ์๋ก ์ ๋ฌํ๋ฉด ์
์ ์์ฑํ ๋ ์
์ ํ์
์ ์ง์ ํด ์ฃผ์ด์ผ ํ ํ์๊ฐ ์์ผ๋ฉฐ ์์์ ํ์
์ถ๋ก ์ ํ๊ณ , ๊ทธ์ ๋ง๋ `reuseIdentifier`๋ฅผ ์ ํํ๊ฒ ๊ฐ์ ธ์ฌ ์ ์๊ฒ ๋์๋ค.
---
## ๐ป **์คํ ํ๋ฉด**
<img src="https://user-images.githubusercontent.com/71758542/217981706-6b88c8f3-d8bc-461a-8125-4cb0363cbb56.gif">