# ๋ค์ด์ด๋ฆฌ - REAEME
# ๐ Diary
CoreData๋ฅผ ์ด์ฉํด ๊ธฐ๋ก์ ๋จ๊ธฐ๋ ์ผ๊ธฐ์ฅ ์ดํ์
๋๋ค.
- ์๋ก์ด ์ผ๊ธฐ๋ฅผ ์ ์ฅํ ์ ์์ต๋๋ค.
- ์๋ ์ ์ฅ๊ธฐ๋ฅ์ ์ง์ํฉ๋๋ค.
- ์ ์ฅ๋ ์ผ๊ธฐ๋ฅผ ํ์ธ / ์์ / ๊ณต์ / ์ญ์ ํ ์ ์์ต๋๋ค.
- ๊ฐ์ฅ ์ต๊ทผ์์ผ๋ก ์ ์ฅ๋ ์ผ๊ธฐ๋ฆฌ์คํธ๋ฅผ ํ์ธ ํ ์ ์์ต๋๋ค.
- ์ ์ฅ๋ ์ผ๊ธฐ๋ฅผ ํฐ์นํด ํ์ธํ๋ ํ๋ฉด์์ ์์ ์ด ๊ฐ๋ฅํฉ๋๋ค.
- ์์ ๋ด์ฉ์ ์๋ ์ ์ฅํ๋ ๊ธฐ๋ฅ์ ์ง์ํฉ๋๋ค.
- ์ผ๊ธฐ๋ฅผ ๋ณต์ฌํ๊ฑฐ๋ ์ธ๋ถ SNS๋ก ๊ณต์ ํ๋ ๋ด๋ณด๋ด๊ธฐ ์๋น์ค๋ฅผ ์ง์ํฉ๋๋ค.
- ๋ฆฌ์คํธ ๋ชฉ๋ก์์ Swipeํ๊ฑฐ๋ ์์ธํ๋ฉด์ ๋ฒํผ์ ์ด์ฉํด ๊ณต์ /์ญ์ ํ ์ ์์ต๋๋ค.
- ๊ธฐ๊ธฐ์ ์์น์ ๋ง๋ ๋ ์จ๋ฅผ ํ์
ํด ์๋ ๊ธฐ๋กํฉ๋๋ค.
- ์ฌ์ฉ์์ ์์น ์ ๋ณด๋ฅผ ์์ฒญํ๊ณ , ๊ถํ์ด ํ๋ฝ๋๋ฉด ๊ธฐ๊ธฐ์ ์์น๋ฅผ ์๋ํ์
ํฉ๋๋ค.
- ์ ์ฅ๋ ๋ค์ด์ด๋ฆฌ์ ํด๋น ๋ ์ง์ ๋ ์จ ์์ด์ฝ์ ๋ฆฌ์คํธ์ ํจ๊ป ๋ณด์ฌ์ค๋๋ค.
- ๊ฐ๋ก๋ชจ๋๋ฅผ ์ง์ํฉ๋๋ค.
- ํฐ ๊ธฐ๊ธฐ์ ๊ฐ๋ก๋ชจ๋์์๋ ์ฌ์ฉ์๊ฐ ์ฝ๊ธฐ ํธ์ํ ์ปจํ
์ธ ๋ ์ด์์์ ์ ๊ณตํฉ๋๋ค.
## ๐ ๋ชฉ์ฐจ
1. [ํ ์๊ฐ](#-ํ-์๊ฐ)
2. [๊ธฐ๋ฅ ์๊ฐ](#-๊ธฐ๋ฅ-์๊ฐ)
3. [Class Diagram](#-class-diagram)
4. [ํด๋ ๊ตฌ์กฐ](#-ํด๋-๊ตฌ์กฐ)
5. [ํ๋ก์ ํธ์์ ๊ฒฝํํ๊ณ ๋ฐฐ์ด ๊ฒ](#-ํ๋ก์ ํธ์์-๊ฒฝํํ๊ณ -๋ฐฐ์ด-๊ฒ)
6. [ํ์๋ผ์ธ](#-ํ์๋ผ์ธ)
7. [๊ณ ๋ฏผํ ๋ถ๋ถ](#-๊ณ ๋ฏผํ-๋ถ๋ถ)
8. [ํธ๋ฌ๋ธ ์ํ
](#-ํธ๋ฌ๋ธ-์ํ
)
9. [์ฐธ๊ณ ๋งํฌ](#-์ฐธ๊ณ -๋งํฌ)
---
## ๐ฑ ํ ์๊ฐ
|[์จ๋์ฟ ํค](https://github.com/sunny-maeng)|[LJ](https://github.com/lj-7-77)|
|:---:|:---:|
|<img width="180px" img style="border: 2px solid lightgray; border-radius: 90px;-moz-border-radius: 90px;-khtml-border-radius: 90px;-webkit-border-radius: 90px;" src="https://avatars.githubusercontent.com/u/107384230?v=4">| <img width="180px" img style="border: 2px solid lightgray; border-radius: 90px;-moz-border-radius: 90px;-khtml-border-radius: 90px;-webkit-border-radius: 90px;" src="https://i.imgur.com/ggU7PLR.jpg">|
---
## ๐ ๊ธฐ๋ฅ ์๊ฐ
<details>
<summary>[Step1 - ๋ค์ด์ด๋ฆฌ ๋ฆฌ์คํธ ํ๋ฉด, ์์ธํ๋ฉด] </summary>
| **๋ค์ด์ด๋ฆฌ ๋ฆฌ์คํธ ํ๋ฉด** |
| :-------------------------------------------: |
| <img src = "https://i.imgur.com/m8kSEaS.gif"> |
**๋ค์ด์ด๋ฆฌ ์์ธ ํ๋ฉด** |
| <img src = "https://i.imgur.com/G7qLxH3.gif"> |
</details> </br>
[Step2 - ๋ค์ด์ด๋ฆฌ CRUD]
| **์์ฑ** | **์ฝ๊ธฐ** | **์์ ** |
| :---: | :---: | :---: |
| <img src = "https://i.imgur.com/M9rvhco.gif"> | <img src = "https://i.imgur.com/sqZlb0s.gif"> | <img src = "https://i.imgur.com/hic34ow.gif"> |
| **์ญ์ ** | **๊ณต์ ,์ทจ์** | **์ค์์ดํ** |
| :---: | :---: | :---: |
| <img src = "https://i.imgur.com/zb96B1F.gif"> | <img src = "https://i.imgur.com/I7r5Ysy.gif"> | <img src = "https://i.imgur.com/biRoxz9.gif"> |
**Step3 - ๋ ์จ API ํ์ฉ**
| **Lightweight Migration** | **์์น์ ๋ณด ๊ถํ ์์ฒญ** | **๋ ์จ ์์ด์ฝ ํ์** |
| :---: | :---: | :---: |
| <img width = 280, src = "https://i.imgur.com/vpsaRkh.png"> | <img width = 300, src = "https://i.imgur.com/d4pWZX7.png"> | <img width = 310, src = "https://i.imgur.com/rT75YW4.png"> |
---
## ๐ Class Diagram
| **Model** |
| :-------------------------------------------: |
||
| **ViewController** |
| :-------------------------------------------: |
||
---
## ๐ ํด๋ ๊ตฌ์กฐ
```
Diary
โโโ Model
โย ย โโโ DiaryInfo
โย ย โโโ WeatherInfo
โย ย โโโ Weather
โย ย โย ย โโโ LocationManager
โย ย โย ย โโโ WeatherManager
โย ย โย ย โโโ WeatherURL
โย ย โโโ CoreData
โย ย โย ย โโโ CoreDataManager
โย ย โย ย โโโ MappingModelV1ToV2.xcmappingmodel
โย ย โโโ URLSession
โย ย โย ย โโโ URLSessionProvider
โย ย โโโ Support
โย ย โย ย โโโ CacheManager
โย ย โย ย โโโ DecodeManager
โย ย โโโ Extension
โย ย โย ย โโโ Date+Extension
โโโ View
โย ย โโโ DiaryListView
โย ย โโโ DiaryCell
โย ย โโโ DiaryDetailView
โย ย โโโ CustomUI
โโโ ViewController
โ โโโ DiaryListViewController
โ โโโ DiaryDetailViewController
โ โโโ RegisterDiaryViewController
โ โโโ CustomActivityViewController
โโโ AppDelegate
โโโ SceneDelegate
โโโ Info.plist
โโโ Diary.xcdatamodeld
โย ย โโโ Diary 2.xcdatamodel
โย ย โโโ Diary.xcdatamodel
Pods
โโโ SwiftLint
```
---
## ํ๋ก์ ํธ์์ ๊ฒฝํํ๊ณ ๋ฐฐ์ด ๊ฒ
- CoreData ํ์ฉํ๊ธฐ
- [X] CoreData ๋ชจ๋ธ์ ์ฑ๊ธํค์ผ๋ก ์์ฑํ์ต๋๋ค.
- [X] CRUD ๋ฉ์๋๋ฅผ ๊ตฌํํ์ต๋๋ค.
- CoreData Migration ์ ์ฉํ๊ธฐ
- [X] ๋ ์จ์ ๋ณด๋ฅผ ์ถ๊ฐํ๋ฉด์ ์ฝ์ด๋ฐ์ดํฐ์ ๋ชจ๋ธ ๋ณ๊ฒฝ์ ์ํด Lightweight Migration ํ์ต๋๋ค.
- Open API์ ํ์ฉ
- [X] [Open Weather](https://openweathermap.org/current) ์๋น์ค๋ฅผ ์ฌ์ฉํ์ต๋๋ค.
- [X] URLSessionDataTask๋ฅผ ํ์ฉํด ๊ธฐ๊ธฐ์ ์์น๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ํ์ฌ ๋ ์จ์ ๋ณด์ ๋ ์จ ์์ด์ฝ์ ์์ฒญํด์ ๋ฐ์์ต๋๋ค.
- Core Location ํ๋ ์์ํฌ์ ํ์ฉ
- [X] ๊ธฐ๊ธฐ์ ์์น ์ ๋ณด์ ๋ํ ๊ถํ์ ์์ฒญํฉ๋๋ค.
- [X] `CLLocationManager` ๋ฅผ ํ์ฉํ์ฌ ๊ธฐ๊ธฐ์ ์์น๋ฅผ ํ์
ํ์ต๋๋ค.
- ์คํ ๋ฆฌ๋ณด๋์์ด ์ฝ๋๋ก๋ง View๊ตฌํํ๊ธฐ
- [X] ์คํ ๋ฆฌ๋ณด๋๋ฅผ ์ญ์ ํ๊ณ SceneDelegate๋ฅผ ์์ ํด navigationController๊ฐ embeded๋ View๋ฅผ ๊ตฌํํ์ต๋๋ค.
- swiftLint ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉํ๊ธฐ
- [X] pod ์ ์ด์ฉํด ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ ์ฉํ์ต๋๋ค.
- [X] `swiftLint` ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก ์ฝ๋์ปจ๋ฒค์
์ ํต์ผํ์ต๋๋ค.
- modernCollectionView๋ฅผ ์ด์ฉํ List๊ตฌํ
- [X] `UICollectionViewCompositionalLayout.list`๋ฅผ ์ด์ฉํด ListView ๊ตฌํํ์ต๋๋ค.
- [X] DiffableDataSource, snapShot์ ์ดํดํ๊ณ ์ ์ฉํ์ต๋๋ค.
- readableContentGuide ์ฌ์ฉํ๊ธฐ
- [X] app adaptivity ๋ฐ ๊ฐ๋ก๋ชจ๋์ ๋ ์ด์์์ readableContentGuide๋ฅผ ์ฌ์ฉํ์ต๋๋ค.
- ์์์ผ๋ก ๋ฑ๋ก/์์ ๊ณผ์ ์ ๊ณตํต๊ธฐ๋ฅ์ ๊ตฌํ
- [X] VC1์ ๊ณตํต๊ธฐ๋ฅ์ ๊ฐ๋ VC2๋ฅผ Class์์์ ํตํด ๊ตฌํํ์ต๋๋ค.
- ์ปฌ๋ ์
๋ทฐ ๋ฆฌ์คํธ์์ ์ค์์ดํ๋ฅผ ์ก์
๊ตฌํ
- [X] Cell ์ค์์ดํ ์, ๋ฐ์ดํฐ๋ฅผ ๊ณต์ /์ญ์ ํ๋ ๊ธฐ๋ฅ์ ๊ตฌํํ์ต๋๋ค.
- Text View Delegate์ ํ์ฉ
- [X] ์ฌ์ฉ์ ์
๋ ฅ์ PlaceHolder๋ฅผ ์ง์ฐ๊ธฐ์ํด Text View Delegate๋ฅผ ์ฌ์ฉํ์ต๋๋ค.
---
## โฐ ํ์๋ผ์ธ
| ๐ Step 1| |
| :--------: | -------- |
| 1 | `SwiftLint` ํ์ฉ |
| 2 | DTO๊ตฌํ - JSON ๋ฐ์ดํฐ์ ๋งคํํ `Diary` ํ์
๊ตฌํ |
| 3 | ์ผ๊ธฐ์ฅ ๋ชฉ๋กํ๋ฉด ๊ตฌํ, ์ผ๊ธฐ์ฅ ์์ญํ๋ฉด UI๊ตฌํ, readableContentGuide ์ ์ฉ |
<details>
<summary>[Details - Step1 ํ์
๋ณ ๊ธฐ๋ฅ ์ค๋ช
] </summary>
#### 1๏ธโฃ DTO - `Diary` ๊ตฌ์กฐ์ฒด
- JSONํ์ผ(sample.json)์ decodeํ๊ธฐ์ํ DTO(๋ฐ์ดํฐ์ ์ก๊ฐ์ฒด) ์
๋๋ค.
#### 2๏ธโฃ `DiaryListViewController` ํด๋์ค
- ์ผ๊ธฐ์ฅ ๋ชฉ๋กํ๋ฉด์ธ DiaryListView๋ฅผ ๋ด๋นํ๋ ๋ทฐ์ปจํธ๋กค๋ฌ์
๋๋ค.
- collectionView(collectionView:didSelectItemAt:) : UICollectionViewDelegateํ๋กํ ์ฝ์ ์ฑํํ์ฌ ๋ชฉ๋ก ์ค ํ๋์ ํญ๋ชฉ์ ์ ํํ์ ๋ ๋ค์ํ๋ฉด์ผ๋ก ์ ํํ๋ ๋ฉ์๋์
๋๋ค.
#### 3๏ธโฃ `DiaryDetailViewController` ํด๋์ค
- ์ผ๊ธฐ์ฅ ๋ชฉ๋ก ์ค ํ๋์ ์ผ๊ธฐ์์ธํ๋ฉด์ธ DiaryDetailView๋ฅผ ๋ด๋นํ๋ ๋ทฐ์ปจํธ๋กค๋ฌ์
๋๋ค.
-`configureDiary()`๋ฉ์๋
์ผ๊ธฐ์์ธํ๋ฉด์ ์ ๋ชฉ,๋ด์ฉ,์ผ์ ๊ฐ๊ฐ ํญ๋ชฉ๋ณ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ธ๋ฉํ๋ ๋ฉ์๋์
๋๋ค.
- `setupNotification()`๋ฉ์๋
ํค๋ณด๋๊ฐ ๋ณด์ด๊ฑฐ๋ ์จ๊ฒจ์ง ๋์ ๋
ธํฐํผ์ผ์ด์
์ Observer๋ฅผ ์ถ๊ฐํด์ controlKeyboard() ๋ฉ์๋๋ฅผ ํธ์ถํฉ๋๋ค.
</details> </br> </br>
| ๐ Step 2| |
| :--------: | -------- |
| 1 | CoreData ๋ชจ๋ธ ์์ฑ |
| 2 | ์ผ๊ธฐ์ฅ CRUD๊ธฐ๋ฅ(์๋์ ์ฅ, ์ฝ๊ธฐ, ์์ , ์ญ์ ) ๊ตฌํ |
| 3 | ์ปฌ๋ ์
๋ทฐ ๋ฆฌ์คํธ์์ ์ค์์ดํ๋ฅผ ์ก์
๊ตฌํ |
| 4 | Text View Delegate์ ํ์ฉ |
<details>
<summary>[Details - Step2 ํ์
๋ณ ๊ธฐ๋ฅ ์ค๋ช
] </summary>
#### 1๏ธโฃ `CoreDataManager` ๊ตฌ์กฐ์ฒด
- CRUD ๊ธฐ๋ฅ๋ณ ๋ฉ์๋ ๊ตฌํ
- `deleteAllNoDataDiaries()`๋ฉ์๋
์๋ก์ด ์ผ๊ธฐ ์์ฑ ํ ์๋ฌด๊ฒ๋ ์
๋ ฅํ์ง ์์์ ๊ฒฝ์ฐ, ์ ์ฅ ํ fetchํด์ฌ ๋ ์ญ์ ํฉ๋๋ค.
- `searchDiary(id:)`๋ฉ์๋
์์ ์ ํด๋น ์ผ๊ธฐ๋ง ์ฐพ์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ต๋๋ค.
#### 2๏ธโฃ `RegisterDiaryViewController` ํด๋์ค
- ์๋ก์ด ์ผ๊ธฐํ๋ฉด์ผ๋ก ๋ด์ฉ์์ด PlaceHolder๋ง ์
๋ ฅ๋์ด์๋ ์ํ์ `DiaryDetailView`๋ฅผ ๋ด๋นํฉ๋๋ค.
#### 3๏ธโฃ `DiaryDetailViewController` ํด๋์ค
- `RegisterDiaryViewController` ํด๋์ค๋ฅผ ์์๋ฐ์ต๋๋ค.
- ์ด๋ฏธ ์์ฑํ ์ผ๊ธฐ๋ด์ฉ์ด ์
๋ ฅ๋์ด์๋ ์ํ์ `DiaryDetailView`๋ฅผ ๋ด๋นํฉ๋๋ค.
#### 4๏ธโฃ `CustomActivityViewController` ํด๋์ค
- ๊ณต์ ์ ๋ทฐ ํ๋จ์์ ์ฌ๋ผ์ค๋ `ActivityView`๋ฅผ ๋ด๋นํ๋ ์ปจํธ๋กค๋ฌ์
๋๋ค.
</details> </br> </br>
| ๐ Step 3| |
| :--------: | -------- |
| 1 | ์์น์ ๋ณด, ๋ ์จ์ ๋ณด ํ๋์ ์ํ ํด๋์ค ๋ด๋ถ ๊ตฌํ |
| 2 | URLSession ํ์ฉ ๋ฐ ์๋ฒ์ ํต์ ํ๋ ๊ด๋ จ ํด๋์ค ๊ตฌํ |
| 3 | CoreData ๋ง์ด๊ทธ๋ ์ด์
|
<details>
<summary>[Details - Step3 ํ์
๋ณ ๊ธฐ๋ฅ ์ค๋ช
] </summary>
#### 1๏ธโฃ `DiaryInfo` DTO
- ์ฝ์ด๋ฐ์ดํฐ์ ๋ค์ด์ด๋ฆฌ ๋ด์ฉ์ ์ ๋ฌํด์ฃผ๋ ๋ฐ์ดํฐ์ ์ก๊ฐ์ฒด ์
๋๋ค.
#### 2๏ธโฃ `WeatherInfo` DTO
- ๋ ์จAPI์ ์๋ json ์ค์ฒฉํ์
๋ฐ์ดํฐ๋ฅผ ์ ์ญํ์
์ผ๋ก ๋ณํํ์ฌ ์ ์ฅํ๊ณ , `DiaryInfo`ํ์
์ ์์ฑ ์ค ํ๋์ ํ์
์ ๋ด๋นํ๋ ๊ตฌ์กฐ์ฒด์
๋๋ค.
#### 3๏ธโฃ `WeatherManager` ํด๋์ค
- fetchWeatherInfo(completion:) : ์๋ฒ๋ก๋ถํฐ ๋ ์จ์ ๋ณด๋ฅผ ๋ฐ์์ค๊ธฐ์ํด URL์ ๋ง๋ค๊ณ , `URLSessionProvider()`๋ฅผ ํตํด ๋ฐ์ดํฐ๋ฅผ ์ป์ต๋๋ค.
- fetchWeatherIcon(icon:,completion:) : ๋งค๊ฐ๋ณ์๋ก ๋ฐ์ `icon`์ ๋ฌธ์์ด์ ํด๋นํ๋ ๋ ์จ์์ด์ฝ์ด ์์นํ URL์ ์ด์ฉํ์ฌ `URLSessionProvider()`๋ก๋ถํฐ ์ด๋ฏธ์ง๋ฅผ ์ป์ต๋๋ค.
#### 4๏ธโฃ `WeatherURL` ์ด๊ฑฐํ
- ์์น์ ๋ณด๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ๋ ์จ์ ๋ง๋ ๋ ์จ์์ด์ฝ์ ๊ฐ์ ธ์ฌ ๋ ํด๋น ์์ด์ฝ์ด ์์นํ URL์ ์์ฑํ๋ ์ด๊ฑฐํ์
๋๋ค.
#### 5๏ธโฃ `LocationManager` ํด๋์ค
- ์์น์ ๋ณด ์ ๊ทผ์ ๋ด๋นํ๋ ํด๋์ค์
๋๋ค. ์์น ์ ํ๋๊ฐ ์ง์ ๋ ๊ฐ์ฒด์ธ `manager`๋ฅผ `CLLocationManager`ํ์
์ผ๋ก ์์ฑํ์ฌ ๊ด๋ จ๋ ์ผ๋ค์ ์ฒ๋ฆฌํฉ๋๋ค.
- ์์น๊ฐ์ ๋ด์ `location` ๋ณ์๋ ์๋์ ๊ฒฝ๋๋ก ๋ฐ์์ค๋ฏ๋ก ํํํ์
์ผ๋ก ์ ์ํ์์ต๋๋ค.
- fetchLocation() : ์ฌ์ฉ์์ ํ์ฌ์์น ํ๋์ ๋ฐฐํฐ๋ฆฌํจ์จ์ ์ํด `requestLocation()`๋ก ํ๋ฒ๋ง ์์น๋ฅผ ์์ฒญํฉ๋๋ค.
- CLLocationManagerDelegate ๊ด๋ จ ๋ฉ์๋
- locationManagerDidChangeAuthorization(manager:) : ์ฑ์ ๋ํ ์์น ์ ๊ทผ๊ถํ ์ค์ ์ด ๋ณ๊ฒฝ๋๋ฉด ํธ์ถ๋ฉ๋๋ค. ์ฌ์ฉ์์ ์ ํ์ ๋ฐ๋ผ ์์น ์ ๊ทผ๊ถํ์ ํ๋ํ๊ฑฐ๋ ๊ฒฐ์ ํด๋ฌ๋ผ๋ ์์ฒญ์ ๋ณด๋
๋๋ค.
- locationManager(manager:didUpdateLocations:) : ์ฌ์ฉ์์ ์์น๋ฅผ ์ฑ๊ณต์ ์ผ๋ก ๊ฐ์ ธ์์ ๊ฒฝ์ฐ ํธ์ถ๋ฉ๋๋ค.
- locationManager(manager:didFailWithError:) : ์ฌ์ฉ์์ ์์น๋ฅผ ๊ฐ์ ธ์ค์ง ๋ชปํ์ ๊ฒฝ์ฐ ํธ์ถ๋ฉ๋๋ค.
#### 6๏ธโฃ `URLSessionProvider` ๊ตฌ์กฐ์ฒด
- `URLSession`์ ์ด์ฉํ์ฌ ๋งค๊ฐ๋ณ์๋ก ๋ฐ์ `url`์ ์๋ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ต๋๋ค.
</details> </br> </br>
---
## ๐ญ ๊ณ ๋ฏผํ ๋ถ๋ถ
### 1๏ธโฃ ํค๋ณด๋๊ฐ ๋ํ๋๊ณ ์ฌ๋ผ์ง ๋, ์ปจํ
์ธ ๋ฅผ ๊ฐ๋ฆฌ์ง์๋๋ก ํ๋ ๋ฐฉ๋ฒ
- 1. **ScrollView ์ด์ฉ**
- ํค๋ณด๋๊ฐ ๋ํ๋ ๋, ์ข์์ง ํ๋ฉด์ผ๋ก๋ ์คํฌ๋กค ํด์ ๊ฐ๋ ค์ง ํ๋ฉด์ ๋ณผ ์ ์๋๋ก `ScrollView`์ ์ปจํ
์ธ ๋ค์ ๋ด์์ต๋๋ค.
- 2. **ScrollView์ inset, offset ์กฐ์ **
- ํค๋ณด๋๊ฐ ๋ํ๋๊ณ ํ๋ฉด์ ์คํฌ๋กค ํ ๋ ์ปจํ
์ธ ์ ๊ฐ์ฅ ๋ฐ๋ถ๋ถ์ด ํค๋ณด๋ ๋ค์ ๊ฐ๋ ค์ง๋ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด, ์คํฌ๋กค๋ทฐ๊ฐ ๋ํ๋ ๋์ Notification์ ๋ฐ์ ScrollView์ Bottom๋ฐฉํฅ์ผ๋ก ํค๋ณด๋ ๋์ด๋งํผ์ `Inset`์ ๋ํ๋๋ก ํ์ต๋๋ค. ๋๋ถ์ด ์ฌ์ฉ์๊ฐ ๋ณด๊ณ ์๋ ํ๋ฉด์ ์ ์งํ๊ธฐ์ํด `offset.y`๋ ํค๋ณด๋์ ๋์ด๋งํผ ๋นผ์ค๋๋ค.
- ํค๋ณด๋๊ฐ ์ฌ๋ผ์ง ๋๋ ๋ฐ๋๋ก `Inset`์ ์ญ์ ํฉ๋๋ค.
- 3. **์
๋ ฅ์ด ๋๋๊ณ ํค๋ณด๋๋ฅผ ๋ด๋ฆฌ๊ธฐ์ํ `keyboardDismissMode` ์์ฑ ์ฌ์ฉ**
- ์ฌ์ฉ์๊ฐ ์
๋ ฅ์ ๋๋ด๊ณ ํค๋ณด๋๋ฅผ ๋ด๋ฆฌ๋ ๋ฐฉ๋ฒ์ผ๋ก UIScrollView์ `keyboardDismissMode` ์์ฑ์ ์ฌ์ฉํด ์ฌ์ฉ์๊ฐ ์คํฌ๋กคํ์ฌ ํค๋ณด๋๋ฅผ ๋ค์ ๋ด๋ฆด ์ ์๋๋ก ๊ตฌํํ์ต๋๋ค.
### 2๏ธโฃ ์์์ผ๋ก ๊ณตํต๊ธฐ๋ฅ ๊ตฌํ์, ์ฃผ์ ํ๋กํผํฐ์ ์ ๊ทผ์ ์ด(private)๋ฅผ ์ค์ ํด์ฃผ๊ธฐ ์ํด ์ด๋์
๋ผ์ด์ ์์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์๋ผ์ฐ๋ ๋ก์ง ๊ตฌํ
- `RegisterDiaryViewController`(๋ค์ด์ด๋ฆฌ ๋ฑ๋กํ๋ฉด VC)๋ฅผ ์์๋ฐ๋ `DiaryDetailViewController`(๋ค์ด์ด๋ฆฌ ์์ ํ๋ฉด VC)๋ฅผ ๊ตฌํํ๋ฉด์ ํ์๋ก `private`์ผ๋ก ์ค์ ํด์ฃผ์ด์ผ ํ ํ๋กํผํฐ๋ค์ ์ ๊ทผ์ ์ด๋ฅผ ์ค์ ํ ์ ์๋ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค. VC ๋ ๊ฐ๊ฐ ๋๊ฐ์ด `diaryPageView`, `diaryPage` ๋ฐ์ดํฐ๋ฅผ ๊ฐ๊ณ ์๋๋ฐ ์ธ๋ถ์์ ์ ๊ทผํ ์ ์๊ฒ `Private`์ค์ ์ ํด์ฃผ์ด์ผ ํ์ต๋๋ค.
- ์์๋ฐ์ ํ๋กํผํฐ๋ฅผ ์ฌ์ฉํ์ง ์๊ณ ์๋ก์ด ํ๋กํผํฐ๋ฅผ ์์ฑํด์ ์ฌ์ฉํ๊ฒ๋๋ฉด, ์ด ํ๋กํผํฐ๋ค์ ์ฌ์ฉํ๋ ๋ฉ์๋๋ค์ ๋ชจ๋ `override`ํด์ ์๋ก ์์ฑํ ํ๋กํผํฐ๋ฅผ ์ฌ์ฉํ ์ ์๋๋ก ๋ชจ๋ `override`ํ ์ฌ๊ตฌํ ํด์ผ ํ์ต๋๋ค.
- ์ด๋์
๋ผ์ด์ ์์ ํด๋ต์ ์ป์์ต๋๋ค. ์ด๋์
๋ผ์ด์ ์์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์๋ผ์ฐ๋ ๋ก์ง์ ๊ตฌํํ๋ ํ๋กํผํฐ๋ ์ธ์คํด์ค์์ฑ์ ๋ฐ๋๊ธฐ ๋๋ฌธ์ Private ์ ๊ทผ์ ์ด๋ฅผ ์ค์ ํ ์ ์๊ณ , ์์๋ฐ์ ๋ฉ์๋๋ค์ override์์ด ์ฌ์ฉํ ์ ์์์ต๋๋ค.
### 3๏ธโฃ ๋ฐ์ดํฐ๊ฐ ์๋ ํ์ด์ง๋ ์๋ ์ญ์ ๋๋ ๋ก์ง
- ์ฒ์์ ๋ค์ด์ด๋ฆฌ ๋ฑ๋ก ์ ์์ฑ๋ด์ฉ์ด ์๋์ง ํ์ธํ ํ ์ ์ฅ์ ๊ฒฐ์ ํ๋ ๋ก์ง์ ๊ตฌํํ๋ คํ๋๋ฐ, ์ ์ฅํ ๋๋ ํ์ธํด์ผํ๊ณ , ํ๊ธ์๋ฅผ ์จ์ ์ด๋ฏธ ์ ์ฅ์ด ๋ ๋ฐ์ดํฐ๋ค์ ๋ํด์๋ ์
๋ฐ์ดํธ์ ํ์ธ ํ ์ญ์ ํด์ผํ๊ณ , ๋ฑ๋กํ๋ฉด ๋ฟ ์๋๋ผ ์ด๋ฏธ ์ ์ฅ๋ ๋ค์ด์ด๋ฆฌ๋ฅผ ๋ณด๋ ํ๋ฉด์์๋ ๋ด์ฉ์ ๋ชจ๋ ์ง์ฐ๋ฉด ์
๋ฐ์ดํธ๊ฐ ์๋ ์ญ์ ๋ฅผ ํด์ผํด์ผํ๋ ๋ฒ๊ฑฐ๋ก์์ด ์์ด ์ผ๋จ ๋ฐ์ดํฐ๊ฐ ์๋ ํ์ด์ง๋ ์๋์ ์ฅ ๋ ํ `fetch`ํด ์ฌ ๋(์ ์ฅ๋ ๋ฆฌ์คํธ๋ฅผ ๋ณด์ฌ์ค ๋) ๋น ๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ์์ฒด์ ์ผ๋ก ์ญ์ ํ ํ `fetch`ํ๋ ๋ก์ง์ผ๋ก ๊ตฌํํ์ต๋๋ค.
### 4๏ธโฃ ๋์ฝ๋ฉ์ ํ์ํ DTOํ์
๊ตฌํ์์ ์ค์ฒฉํ์
์์ด ๊ตฌํํ๊ธฐ
<img width = 600, src = "https://i.imgur.com/hqcn0qA.png">
- [Open Weather](https://openweathermap.org/current) ์๋น์ค์ ์์ฒญํด ๊ฐ์ ธ์จ ํ์ฌ ๋ ์จ์ ๋ณด๋ฅผ ๋ด์ JSON๋ฐ์ดํฐ ์ค ์ค์ ๋ก ํ์ํ ์ ๋ณด๋ ์ฌ์ง์ ํ์ํ `weather` ๋ด๋ถ์ `main`, `icon` ๋ฐ์ดํฐ ์
๋๋ค.
- ๋์ฝ๋ฉ์ ์ํด DTO๋ฅผ ๋ง๋ค๋, ์ฒ์์๋ ์๋์๊ฐ์ด `WeatherInfo` ํ์
์์ `Weather`ํ์
์ ์ค์ฒฉ์ผ๋ก ๊ฐ๊ณ , ์ด `Weather`ํ์
์ด `main`, `icon` ์ ๊ฐ๋ ํ์์ผ๋ก ๋ง๋ค์์ต๋๋ค.
```swift
// ์ค์ฒฉํ์
์ ์ฌ์ฉํ DTO
struct WeatherInfo: Decodable {
struct Weather: Decodable {
let main: String
let icon: String
}
let weather: Weather
}
```
- ํ์ง๋ง ์๋ฏธ์๋ ์ค์ฒฉ์ธ ๊ฒ ๊ฐ์ ์ค์ฒฉํ์
์ ์ฌ์ฉํ์ง ์๊ณ , `CodingKey`์, Decodable์ init๋ด๋ถ์์ `KeyedDecodingContainer` ๋ฅผ ํ์ฉํด `WeatherInfo`ํ์
์ด `main`, `icon`์ ๊ฐ๋๋ก ํ์ต๋๋ค.
```swift
// ์ค์ฒฉํ์
์ ์ฌ์ฉํ์ง ์๋ DTO
struct WeatherInfo {
let main: String
let icon: String
}
extension WeatherInfo: Decodable {
enum CodingKeys: String, CodingKey {
case main, icon, weather
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
var weatherContainer = try container.nestedUnkeyedContainer(forKey: .weather)
let nestedContainer = try weatherContainer.nestedContainer(keyedBy: CodingKeys.self)
main = try nestedContainer.decode(String.self, forKey: .main)
icon = try nestedContainer.decode(String.self, forKey: .icon)
}
}
```
---
## ๐ ํธ๋ฌ๋ธ ์ํ
### 1๏ธโฃ DiffableDataSource์ฌ์ฉํ ๋, Value๊ฐ์ด ๋๊ฐ์ ๋ฐ์ดํฐ๋ก ์ธํ ๋ฌธ์ ํด๊ฒฐ
- ํ๋ก์ ํธ์ ํฌํจ๋ ๊ฒฌ๋ณธ JSON ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉํด List Cell์ ๊ตฌ์ฑํ ๋, ๊ฐ์ด ๊ฐ์ ๋ฐ์ดํฐ๊ฐ์์ด ์ ์์๋ ๋์ง์๋ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค.
- ์์ธ: Diffable datasource๋ฅผ ์ฌ์ฉํ๋ ค๋ฉด `ItemIdentifier`๊ฐ ๊ฐ๊ฐ ๊ณ ์ ํ Hash๊ฐ์ ๊ฐ๊ณ ์์ด์ผํ๋๋ฐ ์๋ ์บก์ณ ํ๋ฉด ์ฒ๋ผ value๊ฐ ์์ ํ ๊ฐ์ ๋๋ ๋ ๊ฐ๊ฐ ๊ฐ์ Hash๊ฐ์ ๊ฐ๊ธฐ ๋๋ฌธ์
๋๋ค.

</br>
- **โ
์์ : UUID ์ฌ์ฉ**
- DTO์ `UUID`๋ฅผ ์์ฑํด ๊ฐ value๋ค์ด ๊ฐ๊ฐ ๊ณ ์ ํ Hash๊ฐ์ ๊ฐ๋๋ก ํ์ต๋๋ค.
```swift
struct Diary: Decodable, Hashable {
let title: String
let body: String
let createdAt: Double
let id = UUID() // ๐
// ...
}
```
### 2๏ธโฃ ์ผ๊ธฐ์ฅ ๋ชฉ๋ก์ ์
๋์ด ์กฐ์ - ์คํ ๋ ์ด์์ ๋ฌธ์ ํด๊ฒฐ
- `DiaryCell`์ `configureDiaryCellLayout()`๋ฉ์๋์์ AutoLayout Constraints ์ค์ ์ ์ผ๊ธฐ ์ ๋ชฉ๊ณผ ์์ฑ์ผ ๋ฑ์ Label์ ๋ด์ `totalStackView`์ `Constraints`๋ฅผ `self`๋ฅผ ๊ธฐ์ค์ผ๋ก ํ์ ๋ ์๋ํ๋๋ก ์
๋์ด๊ฐ ์กฐ์ ๋์ง ์์์ต๋๋ค.
| ์์ ์ | ์์ ํ |
| :--------: | :--------: |
| <img height = 400 src = "https://i.imgur.com/I73XrwP.png"> | <img height = 400 src = "https://i.imgur.com/S6m4APA.png"> |
- **โ
์์ : `LayoutConstraint`์ `contentView`์ ๊ฑธ์ด์ฃผ๊ธฐ**
- `LayoutConstraint`์ `self(DiaryCell)`์ด ์๋ `contentView`๋ฅผ ๊ธฐ์ค์ผ๋ก `topAnchor`์ `bottomAnchor`์ `constant`๊ฐ์ ์ฃผ์ด ํด๊ฒฐํ์์ต๋๋ค.
### 3๏ธโฃ ์ผ๊ธฐ์ฅ ๋ชฉ๋ก์ ์
๋์ด ์กฐ์ - ํด๋ฆญ ์ ์ ์๋์ด๊ฐ ๋๋ ๋ฌธ์ ํด๊ฒฐ
- ๋ค์ด์ด๋ฆฌ ๋ฆฌ์คํธํ๋ฉด์์ `DiaryCell`์ ๋์ด๊ฐ ์๋GIF์ฒ๋ผ ์์๋ค๊ฐ ํด๋ฆญ ์ ์ ์๋์ด๋ก ๋์์ค๋ ๋ฌธ์ ๊ฐ ์์ต๋๋ค.
- ์์ธ์ ์
์ ๋ ์ด์์์ ์ก์์ฃผ๋ `configureDiaryCellLayout()` ๋ฉ์๋๊ฐ Cell์ ์ ๋ชฉ, ๋ ์จ์์ด์ฝ, ๋ด์ฉ์ ๋ด์์ฃผ๋ `configureCell()` ๋ฉ์๋ ๋ด๋ถ์์ ํธ์ถ์ด ๋๋๋ฐ, ์ด `configureCell()` ๋ฉ์๋์ ํธ์ถ ์์ ์ด ์ฐ๋ ๋๊ด๋ จ ๋ฌธ์ ๋ก ๊ฐ์ฅ ๋ง์ง๋ง์ ํธ์ถ๋๋ ๋ฌธ์ ์์ต๋๋ค.
- ์๋ฒ์์ ๊ฐ์ ธ์ค๋ ๋ ์จ์์ด์ฝ์ ๋ด์์ฃผ๊ธฐ ์ํด์ URLSessionDataTask๋ฅผ ํ์ฉํ ๋ ๋ค๋ฅธ์ฐ๋ ๋์์ ์์
์ด ์งํ๋๊ณ , ์์
์ด ๋๋ ํ์ ๊ฐ์ ธ์จ ์์ด์ฝ์ ๊ฐ๊ณ `DispatchQueue.main.async`๋ก ๋ณด๋ด main์ฐ๋ ๋์์ `configureCell()`๋ฉ์๋๊ฐ ์งํ๋๋๋ก ํ์์ต๋๋ค. ์ด ๋ ๋ฌธ์ ๋, ๋ค์ main์ผ๋ก ๋ณด๋ด์ง ์์
์ด ์๋ main์ ๋ชจ๋ ์์
์ด ๋๋ ํ์ ์งํ์ด ๋๋ฏ๋ก ์ปฌ๋ ์
๋ทฐ๋ฆฌ์คํธ๊ฐ ๋ชจ๋ ๊ทธ๋ ค์ง ํ, `configureCell()`์ด ๋ถ๋ฆฌ๊ณ , ์ฌ๊ธฐ์ ํฌํจ๋ ๋ ์ด์์์ ์ก์์ฃผ๋`configureDiaryCellLayout()`๋ ๋์ค์ ๋ถ๋ ค์ง๋๊ฒ ์์ธ์ด์์ต๋๋ค.
| ์์ ์ | ์์ ํ |
| :--------: | :--------: |
| <img height = 400 src = "https://i.imgur.com/OewIbcp.gif"> | <img height = 400 src = "https://i.imgur.com/zeNeua7.gif"> |
- **โ
์์ : ๋ ์จ์์ด์ฝ์ ๋ด์์ฃผ๋ ์์
๋ง `DispatchQueue.main.async`๋ก ์ฒ๋ฆฌ**
- `configureCell()`,`configureDiaryCellLayout()` ๋ฉ์๋๋ main์ฐ๋ ๋์์ ํธ์ถํด์ ๋จผ์ ๋ ์ด์์์์ ์ก๊ณ ์ ๋ชฉ๊ณผ ๋ด์ฉ๋ง ๋ด๊ฒจ์๊ฒ ๋๊ณ , ์๋ฒ ํต์ ์ ์ํด ๋ค๋ฅธ ์ฐ๋ ๋์์ ์์
์ด ํ์ํ ๋ ์จ์์ด์ฝ fetch ์์
๋ง ๊ธฐ๋ฅ์ ๋ถ๋ฆฌํด ์์
์ด ๋๋๋ฉด ๋ค์ main์ฐ๋ ๋๋ก ๋ณด๋ด๋ `DispatchQueue.main.async`๋ก ์ฒ๋ฆฌํด ์ ์์ ์ผ๋ก ๋ ์ด์์์ด ์กํ์๋ Cell์ icon์ ๋ฃ์ ์ ์์์ต๋๋ค.
### 4๏ธโฃ ๋ค์ด์ด๋ฆฌ ์ ์ฅ ์ 2๊ฐ์ฉ ์ ์ฅ๋๋ ๋ฌธ์ ํด๊ฒฐ
- ๋ค์ด์ด๋ฆฌ ํ ๊ฐ ์ ์ฅ ์ ๋ ๊ฐ๊ฐ ์ ์ฅ๋์ด ๋ชฉ๋ก์ ๋์ค๋ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค. ํ๋๋ ์ฌ์ฉ์๊ฐ ์
๋ ฅํ ๋ด์ฉ์ ๋ค์ด์ด๋ฆฌ์ด๊ณ , ๋ค๋ฅธ ํ๋๋ ๋ด์ฉ์๋ ๋ค์ด์ด๋ฆฌ๊ฐ ์์ฑ๋์ด ๋ชฉ๋ก์ ๋ณด์์ต๋๋ค.
- ์์ธ์ CoreData์์ `Weather`์ `Diary` ์ํฐํฐ์ Relationship์ค์ ์ค์ ์ `Parent Entity`๋ก `Diary`๋ฅผ ์ค์ ํด ๋์๊ฒ ๋ฌธ์ ์์ต๋๋ค.
- **โ
์์ : No Parent Entity ์ค์ **
- `Weather` ์์ฑ ์ค์ ์ Xcode ์ฐ์ธก์ ์๋ `Data Model Inspector` - `Entity` - `Parent Entity`์ต์
์์ `No Parent Entity`๋ก ์ค์ ํ์ฌ ํด๊ฒฐํ์์ต๋๋ค.
### 5๏ธโฃ TextView PlaceHolder ์ค์ ๋ฐฉ๋ฒ
- PlaceHolder๋ฅผ TextView.text ์์ฑ์ผ๋ก "์ผ๊ธฐ ์ ๋ชฉ", "์ผ๊ธฐ ๋ด์ฉ" ๊ณผ๊ฐ์ PlaceHolder๋ฅผ ๋ง๋ค๊ณ `textViewDidBeginEditing`๋ฉ์๋๋ก ๊ฐ์ด ๋ฐ๋์์ ๋, PlaceHolder๊ฐ ์ง์์ง๋ ๋ก์ง์ ๊ตฌํํ์ ๋ ์๋์ ๊ฐ์ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค.
- ์ฝ์ด๋ฐ์ดํฐ์ ๋ฐ์ดํฐ ์ ์ฅ ์ ์
๋ ฅ๊ฐ์ด ์์ ๋๋ ์
๋ ฅ๋์ด์๋ PlaceHolder์ธ "์ผ๊ธฐ ์ ๋ชฉ", "์ผ๊ธฐ ๋ด์ฉ"์ Text๋ก ์ธ์ํด ์ ์ฅ๋๋ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค.
- ํ๋ฉด์ ํ ์ ํค๋ณด๋๋ฅผ ๋ฐ๋ก ๋์ฐ๊ธฐ ์ํด TitleTextView๋ฅผ FirstResponder๋ก ์ง์ ํด๋์ผ๋ `textViewDidBeginEditing` ๋ฉ์๋๊ฐ ํธ์ถ๋์ด PlaceHolder๊ฐ ๋ณด์ด์ง ์๋ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค.
- `textViewDidBeginEditing`๋์ , `textViewDidChange`๋ก ๊ฐ์ด ํ๋๋ผ๋ ์ ํ ๋ PlaceHolder๊ฐ ์ ๊ฑฐ๋๋ ๋ก์ง์ผ๋ก ๋ณ๊ฒฝํ๋ฉด PlaceHolder๋ ํ๋ฉด์ ๋ณด์ด์ง๋ง ํ์์ปค์๊ฐ PlaceHolder ๋ค์ชฝ์ ์์นํ๋ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค.
- **โ
์์ : Label์ฌ์ฉ**
- UILabel์ธ `titlePlaceHolder`์ `bodyPlaceHolder`๋ฅผ ๋ง๋ค์ด TextView์ ์ถ๊ฐํด ๋ณด์ด๊ฒ ํ๊ณ UITextViewDelegate์ `textViewDidChange`๋ฉ์๋๋ก ์ถ๊ฐ๋ Label์ด ์ญ์ ๋๋๋ก ์์ ํด ํด๊ฒฐํ์ต๋๋ค.
- Label ์ฌ์ฉ์, ์
๋ ฅ๊ฐ์ด ์์ ๋์ ์ ์ฅ๋๋ ๋ฐ์ดํฐ์ ๊ฐ๋ ๋น๊ฐ์ผ๋ก ์ ์ ์ ์ฅ๋๊ณ , ํ๋ฉด์ ํ์ FirstResponder์ฌ๋ PlaceHolder๊ฐ ์ ๋ณด์ด๊ณ , ์ปค์์ ์์น๋ ์ ์์ ์ผ๋ก(์ ์ผ ์์ชฝ์) ์์นํฉ๋๋ค.
---
## ๐ ์ฐธ๊ณ ๋งํฌ
[๊ณต์๋ฌธ์]
- [Implementing Modern Collection Views](https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/implementing_modern_collection_views)
- [UICollectionViewDiffableDataSource](https://developer.apple.com/documentation/uikit/uicollectionviewdiffabledatasource/)
- [Setting Up a Core Data Stack](https://developer.apple.com/documentation/coredata/setting_up_a_core_data_stack)
- [readableContentGuide](https://developer.apple.com/documentation/uikit/uiview/1622644-readablecontentguide/)
- [DateFormatter](https://developer.apple.com/documentation/foundation/dateformatter)
- [UITextViewDelegate](https://developer.apple.com/documentation/uikit/uitextviewdelegate/)
- [UISwipeActionsConfiguration](https://developer.apple.com/documentation/uikit/uiswipeactionsconfiguration/)
- [Core Location](https://developer.apple.com/documentation/corelocation)
- [๋ ์จ API](https://openweathermap.org/current)