# ๐๏ธ ํ๋ก์ ํธ ๋งค๋์
> ์ฌ์ฉ์์ ์ผ์ ์ ํ์ํ๋ iPad ์ฑ์
๋๋ค.
> * ์ฃผ์ ๊ฐ๋
: `MVVM`, `Combine`
>
> ํ๋ก์ ํธ ๊ธฐ๊ฐ: 2023.05.15 ~ 2023.06.02
### ๐ป ๊ฐ๋ฐํ๊ฒฝ ๋ฐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
<img src = "https://img.shields.io/badge/swift-5.8-orange"> <img src = "https://img.shields.io/badge/Minimum%20Diployment%20Target-13.0-blue"> <img src = "https://img.shields.io/badge/CocoaPods-1.11.3-brightgreen">
<img src = "https://img.shields.io/badge/Firebase-10.9.0-red">
## โญ๏ธ ํ์
| Rowan | Brody |
| :--------: | :--------: |
| <Img src = "https://i.imgur.com/S1hlffJ.jpg" height="200"/> |<img height="200" src="https://avatars.githubusercontent.com/u/70146658?v=4" width="200">|<img src="https://github.com/Andrew-0411/ios-diary/assets/45560895/2872b119-d22b-46a7-85c4-d9e0c3dd6da8">
| [Github Profile](https://github.com/Kyeongjun2) |[Github Profile](https://github.com/seunghyunCheon) |
</br>
## ๐ ๋ชฉ์ฐจ
1. [ํ์๋ผ์ธ](#-ํ์๋ผ์ธ)
2. [ํ๋ก์ ํธ ๊ตฌ์กฐ](#-ํ๋ก์ ํธ-๊ตฌ์กฐ)
3. [์คํํ๋ฉด](#-์คํํ๋ฉด)
4. [ํธ๋ฌ๋ธ ์ํ
](#-ํธ๋ฌ๋ธ-์ํ
)
5. [ํต์ฌ๊ฒฝํ](#-ํต์ฌ๊ฒฝํ)
6. [์ฐธ๊ณ ๋งํฌ](#-์ฐธ๊ณ -๋งํฌ)
</br>
# ๐ ํ์๋ผ์ธ
- 2023.05.15: ๊ธฐ์ ์คํ, ์ ์ฅ์ ์ ํ.
- 2023.05.16: MVVM ์ํคํ
์ฒ ํ์ต.
- 2023.05.17: ๋ฉ์ธํ๋ฉด, TodoListViewModel, TodoViewModel ๊ตฌ์ฑ.
- 2023.05.18: Combine๊ฐ๋
ํ์ต.
- 2023.05.19: ํ ์ผ ์ถ๊ฐํ๋ฉด ํ๋ก์ฐ์ฐจํธ, UML, README ์์ฑ.
- 2023.05.22: Detailํ๋ฉด ๊ตฌํ ๋ฐ Combine์ ์ฉ.
- 2023.05.23: Task์ถ๊ฐ ์ ๋ฉ์ธํ๋ฉด์ ์ ์ฉ.
- 2023.05.24: ๋ฉ์ธ ํ๋ฉด ๋ด์ ํ๋์ ์ปฌ๋ ์
๋ทฐ์์ 3๊ฐ์ ์ปฌ๋ ์
๋ทฐ ์ฌ์ฉํ๋๋ก ๋ณ๊ฒฝ.
- 2023.05.25: CollectionViewModel ์ถ์ํํ์ฌ ๋ณด์ผ๋ฌ ํ๋ ์ดํธ ์ฝ๋ ์ญ์ .
- 2023.05.26: ๋ทฐ๋ชจ๋ธ์์ ๋ทฐ๋ฅผ ์ ์ธํ UML ์ฌ์ค๊ณ, README ์์ฑ.
- 2023.05.29: ๋ทฐ - ๋ทฐ๋ชจ๋ธ ๋ฐ์ธ๋ฉ ๋ฆฌํฉํ ๋ง, popOverStyle modal view ์ ์
- 2023.05.30: ๊ฐ์ฒด ๊ฐ ์์กด์ฑ ์ฃผ์
๊ตฌํ, ์๋ฎฌ๋ ์ดํฐ ์๋ ์ ๋ฐ์ํ ์ค๋ฅ ์์
- 2023.05.31: ์๊ตฌ์ฌํญ์ ๋ง๋ UI ๊ตฌํ, ์คํธ๋ฆผ์์ ์ค๋ฅ๋ฅผ ๋์ง ์ ์๋๋ก ๋ฆฌํฉํ ๋ง
- 2023.06.01: ๊ฐ์ฒด ๊ด๊ณ ์ฌ์ค๊ณ ๋ฐ UML ์์ ๋ฐ ๋ฆฌํฉํ ๋ง, ์ปฌ๋ ์
๋ทฐ ์ค๋
์ท ๋ฆฌ๋ก๋ ๊ธฐ๋ฅ ๊ฐ์
- 2023.06.02: ViewModel ๋ด๋ถ ์ด๋ฒคํธ ๋ฐ์ธ๋ฉ์ ์ํ Input / Output Nested Type ์ถ๊ฐ, MainViewModel ๋ฆฌํฉํ ๋ง ๋ฐ README ์์ฑ
</br>
# ๐ณ ํ๋ก์ ํธ ๊ตฌ์กฐ
## UML Class Diagram



</br>
## File Tree
```swift
โโโ Diary
โโโ AppDelegate.swift
โโโ SceneDelegate.swift
โโโ Model
โ โโโ Plan.swift
โ โโโ WorkState.swift
โ โโโ Service
โ โโโ PlanStorageService.swift
โโโ MainView
โ โโโ MainViewController.swift
โ โโโ MainViewModel.swift
โ โโโ PlanCollectionView
โ โ โโโ PlanCollectionViewController.swift
โ โ โโโ TodoViewModel.swift
โ โ โโโ DoingViewModel.swift
โ โ โโโ DoneViewModel.swift
โ โ โโโ PlanListViewModel.swift
โ โโโ PlanCell
โ โ โโโ PlanCell.swift
โ โ โโโ PlanCellViewModel.swift
โ โโโ HeaderView
โ โโโ HeaderView.swift
โ โโโ HeaderViewModel.swift
โโโ DetailView
โ โโโ DetailViewController.swift
โ โโโ DetailViewModel.swift
โโโ Protocols
โ โโโ ReuseableIdentifier.swift
โโโ Extensions
โ โโโ Combine
โ โ โโโ UIControl+Combine.swift
โ โ โโโ UITextView+Combine.swift
โ โ โโโ UITextField+Combine.swift
โ โ โโโ UIDatePicker+Combine.swift
โ โโโ Array.swift
โ โโโ CALayer.swift
โโโ Assets
โโโ LaunchScreen
โโโ Info.plist
โโโ GoogleService-Info.plist
```
</br>
# ๐ฑ ์คํํ๋ฉด
|ํ ์ผ ์ถ๊ฐ|ํ ์ผ ์์ |
|:--:|:--:|
|<img src="https://github.com/seunghyunCheon/Diary/assets/70146658/c186b2c4-ef79-4806-8e4b-2f5c1bef4644" width="700">|<img src="https://github.com/seunghyunCheon/Diary/assets/70146658/b95556ae-4a13-4fa4-9a50-5012de76c84c" width="700">|
|ํ ์ผ ์ญ์ |ํ ์ผ ์ด๋|
|:--:|:--:|
|<img src="https://github.com/seunghyunCheon/Diary/assets/70146658/4720d776-3fdb-44a3-8756-95eae80eee44" width="700">|<img src="https://github.com/seunghyunCheon/Diary/assets/70146658/1bd44cd4-1a2c-4660-bbb0-4ba5c98dc440" width="700">|
|๊ธฐํ์ง๋ ํ ์ผ|ํ์ธ ๋ฒํผ ํ์ฑํ|
|:--:|:--:|
|<img src="https://github.com/seunghyunCheon/Diary/assets/70146658/645b2a79-1b05-4fd9-b707-fa6d5396b0e0" width="700"/>|<img src="https://github.com/seunghyunCheon/Diary/assets/70146658/64a465e1-95f4-4307-9d01-ced02aa163f9" width="700"/>|
</br>
# ๐ ํธ๋ฌ๋ธ ์ํ
## 1๏ธโฃ MVVM ์ํคํ
์ฒ ์ค๊ณ
### ๐ ๋ฌธ์ ์

์ด๊ธฐ ๊ฐ์ฒด ๊ด๊ณ๋ฅผ ์ค๊ณ๋๋ฅผ ์ฐธ๊ณ ํ์ฌ ์ฝ๋๋ฅผ ์์ฑํ ๊ฒฐ๊ณผ ๋ฐ์ํ ๋ฌธ์ ๋ ์๋์ ๊ฐ์ต๋๋ค.
1. ๋ณด์ผ๋ฌ ํ๋ ์ดํธ ์ฝ๋๊ฐ ๋ง์(Todo / Doing / Done List๋ฅผ ๊ด๋ฆฌํ๋ ViewController)
2. ViewModel๊ณผ ViewController์ ์ญํ ์ ๋ถ์ ์ ํ ์ค๊ณ
* DiffableDataSource, Snapshot์ ๊ด๋ฆฌ๋ฅผ ViewModel์ด ํ๊ณ ์๋ ๋ฌธ์
* Todo/Doing/Done List ๊ฐ๊ฐ์ ํ์ฅ์ฑ์ ๊ณ ๋ คํ์ง ์์๋ ๋ฌธ์
3. Protocol์ ํตํ ์ถ์ํ์ ๋ฐ๋ผ ์ฝ๋๊ฐ ๋ ๋ณต์กํ depth / ๊ด๊ณ๋ฅผ ๊ฐ์ ธ ๊ฐ๋
์ฑ / ๊ฐ์ฒด๊ด๊ณ ํ์
์ด ์ด๋ ค์
4. List๋ฅผ ๊ตฌํํ ๋ทฐ๋ฅผ TableView์์ CollectionView๋ก ์ค๊ณ ์์ ์์ด ๋ณ๊ฒฝํ ๋ฌธ์
</br>
### โ๏ธ ํด๊ฒฐ๋ฐฉ์
* MVVM ์ํคํ
์ฒ์ ๋ํ ์ถ๊ฐ ์๋ฃ ์กฐ์ฌ ๋ฐ ํ์ต
* Class Diagram ์ฌ์ค๊ณ


**์ ์ฉ ๊ฒฐ๊ณผ**
1. CollectionView๋ฅผ ๊ด๋ฆฌํ๋ ๋ทฐ์ปจํธ๋กค๋ฌ๋ฅผ ํ๋์ ํ์
(TaskCollectionViewController)์ผ๋ก ๊ฐ์ ํ์ฌ ํด๊ฒฐ
2. DiffableDataSource์ Snapshot ๊ฐ์ฒด์ ์์น๋ฅผ TaskCollectionViewController ๋ด๋ถ๋ก ๋ณ๊ฒฝ
3. ๋ณต์กํ ๊ด๊ณ์ฑ์ ์ต์ํํ๋ ๋ฐฉํฅ์ผ๋ก Protocol ์ถ์ํ ์ค๊ณ(TaskListViewModel Protocol ์ ์, ์ ์ ํ Delegation ํจํด์ ์ํ Protocol ์ ์)
4. UML ์ฌ์ค๊ณ ํ CollectionView ์ฌ์ฉ์ ๋ช
์
</br>
### โ๏ธ ์ถ๊ฐ ๊ฐ์ ์

์ด๋ฒคํธ ์คํธ๋ฆผ์ ๊ด๋ฆฌ๊ฐ ํ์ํ ๋ทฐ ๋ชจ๋ธ์ Input / Output์ ์ ์ํ์ฌ ViewController์์๋ง ๊ตฌ๋
์ด ์ผ์ด๋๋๋ก ์ค๊ณ
**์ ์ฉ ๊ฒฐ๊ณผ**
1. `@Published` attribute๋ฅผ ์ ๊ฑฐํ์ฌ planList fetch ์ดํ ๋ถํ์ํ filter-distribute ๊ณผ์ ์ด ์๋๋ก ๊ฐ์
2. view model์ `transform(input:)` ๋ฉ์๋๋ฅผ ํ์ฉํ์ฌ ViewController์์๋ง ๊ตฌ๋
์งํ
3. DiffableDataSource์ Snapshot apply ์, ๋ถํ์ํ reload๊ฐ ์๋๋ก ๊ฐ์
</br>
## 2๏ธโฃ ์๊ตฌ์ฌํญ์ ๋ง๋ ๋ ์ด์์ ์ค๊ณ.
ํ ํ๋ฉด์ 3๊ฐ์ ํ
์ด๋ธ ๋ทฐ๊ฐ ๋ณด์ฌ์ง๋ ํ๋ฉด์ ๊ตฌํํ๋ ์๊ตฌ์ฌํญ์ด ์กด์ฌํ์ต๋๋ค.
์ด๊ธฐ์๋ ํ๋์ ์ปฌ๋ ์
๋ทฐ์์ ์ปดํฌ์ง์
๋ ๋ ์ด์์์ ์ฌ์ฉํ๋ค๋ฉด ๊ฐ๊ฐ์ ํ๋ฉด์ ์ ๊ทผ์ด ์ฌ์์ง๋ค๊ณ ์๊ฐํ์ฌ ์ด ๋ฐฉ๋ฒ์ ์ฑํํ์ต๋๋ค.
</br>
### ๐ ๋ฌธ์ ์
ํ์ง๋ง ์ด๋ฅผ ๊ตฌํํ๋ฉด์ ๋ค์์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ต๋๋ค.
- section header๊ฐ ์
๋ด์ฉ๊ณผ ๊ฒน์ณ๋ณด์ด๋ ๋ฌธ์ .
- ๊ฐ๊ฐ์ ์น์
์ listConfiguration์ ์ฌ์ฉํ์ฌ ๊ตฌ์ฑํ์ ๋ section์ ํฌ๊ธฐ๋ฅผ ์ก์ง ๋ชปํ๋ ๋ฌธ์ .
- ์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๊ฐ๊ฐ์ ์น์
์ listํ์์ด ์๋๋ผ compositionalLayout์ผ๋ก ๊ตฌ์ฑํ์ง๋ง trailingSwipeActionsConfigurationProvider์ ์ถ๊ฐํ์ง ๋ชปํ๋ ๋ฌธ์ ๊ฐ ๋ ๋ฐ์ํจ.
- deleteํ๋ ๊ฒ์ฒ๋ผ ๋ณด์ฌ์ง๋ ์คํฌ๋กค๋ทฐ๋ฅผ ๋ง๋ค ์๋ ์์ผ๋ ์ ํ์์ ์ง์ํ๋ SwipeAction๊ธฐ๋ฅ์ ์ฌ์ฉํ์ง ์๊ณ ์๊ธฐ์ ์ด๋๋ถํฐ ์ค๊ณ๊ฐ ์๋ชป๋์๋ค๊ณ ์๊ฐ.
- ํ๋์ ๋ทฐ๋ชจ๋ธ์์ ๋ชจ๋ ๊ธฐ๋ฅ์ ๊ด๋ฆฌํ๊ธฐ์ ํ์ฅ์ฑ์ด ๋ฎ์์ง๋ ๋ฌธ์ .
<br/>
```swift
private func collectionViewLayout() -> UICollectionViewLayout {
let layoutConfiguration = UICollectionViewCompositionalLayoutConfiguration()
layoutConfiguration.scrollDirection = .horizontal
let layout = UICollectionViewCompositionalLayout(sectionProvider: { sectionIndex, _ in
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalWidth(0.2))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1/3),
heightDimension: .fractionalHeight(1))
let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize,
subitems: [item])
group.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: nil,
top: .flexible(50),
trailing: nil,
bottom: nil)
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .continuous
// ํค๋ ์ค์ .
// deleteAction ์ปค์คํ
๊ตฌํํด์ผ ํจ.
return section
}, configuration: layoutConfiguration)
return layout
}
```
</br>
### โ๏ธ ํด๊ฒฐ๋ฐฉ์
์ ๋ฌธ์ ๋ค์ ๊ณ ๋ คํ์ ๋ ํ๋์ ์ปฌ๋ ์
๋ทฐ์์ ๊ด๋ฆฌํ๊ธฐ ๋ณด๋ค๋ 3๊ฐ์ ์ปฌ๋ ์
๋ทฐ๋ฅผ ๋ง๋๋ ๊ฒ์ด ์ข๋ค๊ณ ์๊ฐํ์ฌ ์ด ๋ฐฉ๋ฒ์ผ๋ก ๋ณ๊ฒฝํ์ต๋๋ค.
๋ณ๊ฒฝ์ดํ ๋ค์์ ์ฅ์ ์ด ์์์ต๋๋ค.
- ํ์ฅ์ฑ์ ์ฆ๊ฐ์ํค๊ณ ์ ์ง๋ณด์ ์ฉ์ดํ๊ฒ ๋ง๋ค์ด ๊ฐ๋ฐฉํ์์์น ์ค์.
- ๊ฐ๊ฐ์ ๋ทฐ๊ฐ ๋ทฐ๋ชจ๋ธ์ 1:1๊ด๊ณ๋ฅผ ๋งบ๊ณ ์๊ธฐ ๋๋ฌธ์ ๋จ์ผ์ฑ
์์์น์ ์ค์.
- ๋ณต์กํ 1๊ฐ์ ๋ ์ด์์์์ ๊ฐ๋จํ 3๊ฐ์ ๋ ์ด์์ ๊ตฌ์ฑํ์ฌ ๊ฐ๋
์ฑ ์ฆ๊ฐ.
<br/>
```swift
private func collectionViewLayout() -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout { _, layoutEnvironment in
var config = UICollectionLayoutListConfiguration(appearance: .grouped)
// ํค๋ ์ค์
// ์ ํ์ด ์ ์ฉํ๋ UIContextualAction์ฌ์ฉํ์ฌ Delete ๊ตฌํ.
let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: layoutEnvironment)
section.interGroupSpacing = 10
return section
}
return layout
}
```
</br>
## 3๏ธโฃ ์ฌ๋ฌ๊ฐ์ ์์ดํ
์ด ๋ฆฌ๋ก๋ ๋๋ ๋ฌธ์
### ๐ ๋ฌธ์ ์
์ปฌ๋ ์
๋ทฐ์ ๋ฐ์ดํฐ์์ค๋ก ์กด์ฌํ๋ ๋ฐฐ์ด์ด ๋ณ๊ฒฝ๋ ๋๋ง๋ค ์ค๋
์ท์์ ๊ทธ ๋ฐฐ์ด์ ์ ์ฒด id๋ฅผ ๊ฐ์ ธ์ `reloadItems`๋ฅผ ์ฌ์ฉํ์ฌ ์
๋ฐ์ดํธ ํ์ต๋๋ค.
```swift
func bind() {
viewModel.$planList
.sink { isUpdating, planList in
if isUpdating {
self.updateSnapshot(planList)
}
}
}
func updateSnapshot(planList: [Plan]) {
let planIDList = planList.map { $0.id }
let snapshot = Snapshot()
snapshot.appendSection([.main])
snapshot.reloadItems([planIDList])
dataSource.apply(snapshot)
}
```
ํ์ง๋ง ์ ๋ฐฉ๋ฒ์ ์ค๋
์ท์์ `hashable`ํ ๋ชจ๋ธ ์์ฒด๋ฅผ ๋ค๊ณ ์๋ ๊ฒ์ด ์๋๋ผ `id`๋ง์ ๋ค๊ณ ์๊ธฐ ๋๋ฌธ์ `id`์ ํด๋นํ๋ ๋ด์ฉ์ด ๋ณ๊ฒฝ๋๋๋ผ๋ `id`๋ ๋ณ๊ฒฝ๋์ง ์๊ธฐ ๋๋ฌธ์ ๋ฆฌ๋ก๋๋์ง ์๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ต๋๋ค.
๊ทธ๋์ ๋ค์๊ณผ ๊ฐ์ด ๋ณ๊ฒฝํ์ต๋๋ค.
```swift
func updateSnapshot(planList: [Plan]) {
let snapshot = dataSource.snapshot()
let planIDList = planList.map { $0.id }
snapshot.reloadSection([.main])
snapshot.reloadItems([planIDList])
dataSource.apply(snapshot)
}
```
ํ์ง๋ง ์ ์ฝ๋๋ ์
์ ์
๋ฐ์ดํธ๊ฐ ๋ฐ์ํ์ ๋ ์ ์ฒด๋ฅผ ๋ฆฌ๋ก๋ํ๋ ๋ฌธ์ ๊ฐ ์กด์ฌํ๊ณ ์ญ์ ๊ธฐ๋ฅ ๋ํ ๋ง์ฐฌ๊ฐ์ง์์ต๋๋ค.
</br>
### โ๏ธ ํด๊ฒฐ๋ฐฉ์
### ์์ฑ, ์์ , ์ญ์ ์ ๋ํ ์ค๋
์ท ํจ์๋ฅผ ๋ถ๋ฆฌํ์ฌ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ์ต๋๋ค.
`taskList`์์ฒด๋ฅผ ๊ตฌ๋
ํ๊ณ ์๋ค๋ฉด ๋ํ
์ผํ ๊ธฐ๋ฅ์ ์ ์ ํ ์ ์์๊ธฐ ๋๋ฌธ์ `taskList`๋ฅผ `Publisher`๋ก ๋๊ณ ์๋ ์ฝ๋๋ฅผ ์ ๊ฑฐํ์ต๋๋ค.
๋ง์ฝ ์ค๋
์ท์์ ๊ด๋ฆฌํ๊ณ ์๋ ๊ฒ์ด `id`๊ฐ ์๋ `Hashable`ํ ๋ชจ๋ธ์ด๋ผ๋ฉด `dataSource`์ ์ ๋ถ ๊ฐ์ ธ์์ `apply`๋ง ํธ์ถํ์๋ `DiffableDatasource`๊ฐ ์์ฒด์ ์ผ๋ก ์ฐจ์ด๋ฅผ ๋น๊ตํ์ฌ ์
๋ฐ์ดํธ๋ฅผ ํ ๊ฒ์ด๊ธฐ ๋๋ฌธ์ ๋ํ
์ผํ ๊ธฐ๋ฅ์ ์ ์ํ ํ์๊ฐ ์์ ๊ฒ์
๋๋ค.
ํ์ง๋ง ํ์ฌ `DiffableDataSource`์๋ `Hashable`ํ ๋ชจ๋ธ์ ๋ค๊ณ ๋ค๋๋ ๊ฒ์ด ์๋ `UUID`๋ฅผ ๋ค๊ณ ๋ค๋๊ธฐ ๋๋ฌธ์ ๋ํ
์ผํ ๊ธฐ๋ฅ์ ์๊ฐ ํ์ํ๊ธฐ ๋๋ฌธ์ `taskList`์ `Publisher`๋ฅผ ์ ๊ฑฐํ์ต๋๋ค.
๊ทธ๋ฆฌ๊ณ ์์ฑ, ์ญ์ , ์์ ์ด๋ฒคํธ์ ๋ํ `Publisher`๋ฅผ ์ ์ํ์ต๋๋ค.
```swift
protocol PlanListViewModel: AnyObject {
var planList: [Plan] { get set }
var planCountChanged: PassthroughSubject<Int, Never> { get }
var planCreated: PassthroughSubject<Int, Never> { get }
var planUpdated: PassthroughSubject<UUID, Never> { get }
var planDeleted: PassthroughSubject<(Int, UUID), Never> { get }
// ...
}
```
์์ฑ, ์ญ์ , ์์ ์ด ์ผ์ด๋ฌ์ ๋ ๊ฐ๊ฐ์ ์ด๋ฒคํธ ํผ๋ธ๋ฆฌ์
์ `send`๋ฅผ ํตํด ์ฐ๊ด๋ ๊ฐ(ex. ์ญ์ ๋ id, ์
๋ฐ์ดํธ ๋ id ๋ฑ)์ ๋ฐํํจ์ผ๋ก์จ ์ฐ๊ด๋ ์์
์ ๋ง๊ฒ ๋ฐ์ดํฐ์์ค๊ฐ ๋ณ๊ฒฝ๋ ์ ์๋๋ก ํ์ต๋๋ค.
```swift
// PlanCollectionViewController.swift
private func bindState() {
viewModel.planCreated
.sink { [weak self] planListCount in
self?.applyLatestSnapshot()
self?.viewModel.planCountChanged.send(planListCount)
}
.store(in: &cancellables)
viewModel.planUpdated
.sink { [weak self] planID in
self?.reloadItems(id: planID)
}
.store(in: &cancellables)
viewModel.planDeleted
.sink { [weak self] (planListCount, planID) in
self?.deleteItems(id: planID)
self?.viewModel.planCountChanged.send(planListCount)
}
.store(in: &cancellables)
}
private func applyLatestSnapshot() {
let planIDList = viewModel.planList.map { $0.id }
var snapshot = Snapshot()
snapshot.appendSections([.main])
snapshot.appendItems(planIDList)
dataSource?.apply(snapshot)
}
private func reloadItems(id: UUID) {
guard var snapshot = dataSource?.snapshot() else { return }
snapshot.reloadItems([id])
dataSource?.apply(snapshot)
}
private func deleteItems(id: UUID) {
guard var snapshot = dataSource?.snapshot() else { return }
snapshot.deleteItems([id])
dataSource?.apply(snapshot)
}
```
</br>
## 4๏ธโฃ Event Stream์ด ๋์ด์ง๋ ๋ฌธ์
### ๐ ๋ฌธ์ ์
Combine์ ํ์ฉํ์ฌ ๋ทฐ์์ ๋ฐ์ํ Event์ Publisher๋ฅผ ๊ตฌ๋
ํ ๋, Stream์ด ViewModel ๋ด๋ถ์์ ๊ตฌ๋
์ด ์ด๋ฃจ์ด์ ธ ๋ค์๊ณผ ๊ฐ์ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค.
1. ๊ตฌ๋
์ด ViewController์ ViewModel์์ ๋ชจ๋ ์ด๋ฃจ์ด์ง๊ณ ์์ด ์ด๋ฒคํธ ์ฒ๋ฆฌ ๊ณผ์ ์ ํ์
์ด ์ด๋ ต๋ค.
2. ๊ตฌ๋
์ ์๊ฐ ๋์ด๋๋ฉด ์ ์ฅ๋๋ cancellable์ ์ธ์คํด์ค๊ฐ ๋์ด๋ ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋์ด ๋์ด๋ ๊ฒ์ด๋ค.
```swift
// ViewModel ๊ตฌํ๋ถ...
struct Input {
let titleTextEvent: AnyPublisher<String?, Never>
let bodyTextEvent: AnyPublisher<String?, Never>
}
struct Output {
let isEditingDone = PassthroughSubject<Bool, Error>()
}
private var cancellables = Set<AnyCancellable>()
func transform(input: Input) -> Output {
let output = Output()
input.titleTextEvent
.sink(receiveValue: { [weak self] title in
guard let self else { return }
self.title = title
output.isEditingDone.send(true)
})
.store(in: &cancellables)
input.bodyTextEvent
.sink(receiveValue: { [weak self] body in
guard let self else { return }
self.body = body
output.isEditingDone.send(true)
})
.store(in: &cancellables)
return output
}
```
</br>
### โ๏ธ ํด๊ฒฐ๋ฐฉ์
ViewModel์์ cancellables๋ฅผ ์ ๊ฑฐํ๊ณ , operator๋ฅผ ํตํด ์ด๋ฒคํธ๋ฅผ ํตํด ๋ฐ์ publisher๋ฅผ ๊ฐ๊ณตํ์ฌ ๊ด๋ จ๋ publisher๋ฅผ ํ๋์ ์คํธ๋ฆผ์ผ๋ก mergeํ์ต๋๋ค.
merge๋ publisher๋ฅผ Output์ผ๋ก ์ ๋ฌํด ViewController์์๋ง ๊ตฌ๋
์ด 1ํ๋ง ์ผ์ด๋๊ฒ ๋์ด ์ฐ๊ฒฐ๋ ์คํธ๋ฆผ์ ๊ฐ์ง ์ ์๋๋ก ํด๊ฒฐํ์ต๋๋ค.
```swift
struct Input {
let titleTextEvent: AnyPublisher<String?, Never>
let bodyTextEvent: AnyPublisher<String?, Never>
let datePickerEvent: AnyPublisher<Date, Never>
}
struct Output {
let isEditingDone: AnyPublisher<Bool, Error>
}
func transform(input: Input) -> Output {
let titlePublisher = input.titleTextEvent
.tryMap { [weak self] title in
guard let title else { throw EditError.nilText }
guard let self else { throw EditError.nilViewModel }
self.title = title
return true
}
let bodyPublisher = input.bodyTextEvent
.tryMap { [weak self] body in
guard let body else { throw EditError.nilText }
guard let self else { throw EditError.nilViewModel }
self.body = body
return true
}
let datePublisher = input.datePickerEvent
.tryMap { [weak self] date in
guard let self else { throw EditError.nilViewModel}
self.date = date
return true
}
let isEditingDone = titlePublisher
.merge(with: bodyPublisher, datePublisher)
.eraseToAnyPublisher()
return Output(isEditingDone: isEditingDone)
}
```
# โจ ํต์ฌ๊ฒฝํ
<details>
<summary><big>โ
Combine</big></summary>
</br>
## Protocol
### Publisher
์ฑํํ ํ์
์ด ์๊ฐ์ด ์ง๋จ์ ๋ฐ๋ผ ์ผ๋ จ์ ๊ฐ์ ์ ์กํ ์ ์์์ ์ ์ธํ๋ ํ๋กํ ์ฝ.
์ฌ๋ฌ๊ฐ์ง ๊ตฌ๋
๋ฉ์๋๋ฅผ ํตํด Subscriber์ ์์ ์ ๋ณ๊ฒฝ์ฌํญ์ ์๋ฆฐ๋ค.
**Creating Your Own Publishers**
Publisher ํ๋กํ ์ฝ์ ์ง์ ๊ตฌํํ๋ ๋์ Combine ํ๋ ์์ํฌ์์ ์ ๊ณตํ๋ ์ฌ๋ฌ ํ์
์ค ํ๋๋ฅผ ์ฌ์ฉํ์ฌ ๊ณ ์ ํ Publisher๋ฅผ ๋ง๋ค ์ ์๋ค.
* `PassthroughSubject`์ ๊ฐ์ `Subject`์ ๊ตฌ์ฒด์ ์ธ ํ์ ํด๋์ค๋ฅผ ์ฌ์ฉํ์ฌ `send(_:)` ๋ฉ์๋๋ฅผ ํธ์ถํด ํ์์ ๋ฐ๋ผ ๊ฐ์ ๊ฒ์.
* `CurrentValueSubject`๋ฅผ ์ฌ์ฉํ์ฌ subject์ ๊ธฐ๋ณธ ๊ฐ์ ์
๋ฐ์ดํธํ ๋๋ง๋ค ๊ฐ์ ๊ฒ์.
* ์ปค์คํ
ํ์
์ ์์ฑ์ @Published attribute๋ฅผ ์ถ๊ฐํ์ฌ ํ๋กํผํฐ๋ฅผ ๊ฒ์.
### Subscriber
Publisher๋ก๋ถํฐ input์ ์ ๋ฌ ๋ฐ์ ์ ์๋ ํ์
์ ์ ์ธํ๋ ํ๋กํ ์ฝ.
Subscriber ์ธ์คํด์ค๋ Publisher๋ก๋ถํฐ ์คํธ๋ฆผ์ ์์๋ฅผ ์ ๋ฌ ๋ฐ๋๋ค.
Subscriber ์ธ์คํด์ค๋ ๊ด๊ณ ๋ณ๊ฒฝ์ ์ค๋ช
ํ๋ life cycle event์ ํจ๊ป publisher๋ก๋ถํฐ ์คํธ๋ฆผ์ element๋ฅผ ๋ฐ๋๋ค.
Publisher์ `subscribe(_:)` ๋ฉ์๋๋ฅผ ํธ์ถํ์ฌ subscriber์ publisher๋ฅผ ์ฐ๊ฒฐํ๋ค. ํด๋น ๋ฉ์๋์ ํธ์ถ ์ดํ publisher๋ subscriber์ `reveive(subscription:)` ๋ฉ์๋๋ฅผ ํธ์ถํ๋ค. ์ดํ publisher์๊ฒ element๋ฅผ ์์ฒญํ๊ณ ์ ํ์ ์ผ๋ก ๊ตฌ๋
์ ์ทจ์ํ๋ ๋ฐ ์ฌ์ฉํ๋ subscription ์ธ์คํด์ค๊ฐ subscriber์๊ฒ ์ ๊ณต๋๋ค.
Combine์ publisher ํ์
์ ๋ํ ์ฐ์ฐ์๋ก ๋ค์ subscriber๋ฅผ ์ ๊ณตํ๋ค.
* `sink(receiveCompletion:receiveValue:)`: sink๋ ์๋ฃ ์ ํธ๋ฅผ ์์ ํ ๋์ ์ element๋ฅผ ์์ ํ ๋๋ง๋ค ์ ๊ณต๋ ํด๋ก์ ๋ฅผ ์คํํ๋ค.
* `assign(to:on:)`: assign์ ์๋ก ๋ฐ์ ๊ฐ element๋ฅผ ์ง์ ๋ ์ธ์คํด์ค์ key path๋ก ์๋ณ๋๋ ํ๋กํผํฐ์ ํ ๋นํ๋ค.
</br>
### Subject
์ธ๋ถ ํธ์ถ์๊ฐ ์์๋ฅผ ๊ฒ์ํ ์ ์๋ ๋ฉ์๋๋ฅผ ๋
ธ์ถํ๋ Publisher.
`send(_:)` ๋ฅผ ํตํด ์คํธ๋ฆผ์ ์ด๋ค ๊ฐ์ ์ฃผ์
ํ ์ ์๋ค.
</br>
## Property Wrapper
### @Published
ํด๋น ํ๋กํผํฐ ๋ํผ attribute๋ก ํ์๋ ํ๋กํผํฐ๋ ํ์
์ด publishํ๊ฒ ๋๋ค.
* ๊ฒ์๋ ํ๋กํผํฐ์ publisher์๋ ํ๋กํผํฐ ์ด๋ฆ ์์ $ํ์๋ฅผ ์ถ๊ฐํ์ฌ ์ ๊ทผํ ์ ์๋ค.
* @Published ํ๋กํผํฐ ๋ํผ๋ class ํ๋กํผํฐ์๋ง ์ ์ฉํ ์ ์๋ค.
</br>
</details>
<details>
<summary><big>โ
MVVM ํ์ฉ</big></summary>
</br>
## Model
* ๋น์ฆ๋์ค ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ง๊ณ ์๋ ๊ณ์ธต.
* Repository์ ๋ฐ์ดํฐ๋ฅผ CRUDํ๋ ๋ก์ง์ด ์กด์ฌ.
## View
* ๋ทฐ๋ชจ๋ธ๊ณผ ์ฐ๊ฒฐ๋๋ ๋ฐ์ธ๋ฉ์ด ์กด์ฌ.
* ๊ทธ ์ธ ๋ ์ด์์์ ๊ทธ๋ฆฌ๋ ์ฝ๋๋ง ์กด์ฌ.
## ViewModel
* ๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ ๋์์ ์ ๊ณต. ๋ชจ๋ธ์ ์ง์ ๋
ธ์ถํ๊ฑฐ๋ ํน์ ๋ชจ๋ธ ๋ฉค๋ฒ๋ฅผ ๋ํํ๋ ๋ฉค๋ฒ๋ฅผ ์ ๊ณต.
* UIKit๋ฅผ importํ์ง์๊ณ ๋ทฐ์๊ฒ ๋ฐ์ธ๋ฉํด์ฃผ๋ ๋ชจ๋ธ๊ณผ Presentation Logic๋ง ์กด์ฌ.
## MVVM + Combine

Combine ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ์ด๋ฒคํธ ์ฒ๋ฆฌ ์ฐ์ฐ์๋ฅผ ๊ฒฐํฉํ์ฌ ๋น๋๊ธฐ ์ด๋ฒคํธ ์ฒ๋ฆฌ๋ฅผ ์ปค์คํฐ๋ง์ด์งํ ์ ์๋ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ค.
RxSwift ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋์ผํ ๊ธฐ๋ฅ์ ์ ๊ณตํ๊ธฐ ๋๋ฌธ์ ์ด๋ฒคํธ ์คํธ๋ฆผ์ ๋ํ ์ดํด๋ฅผ ์ํด ์ฌ๋ฌ ์๋ฃ๋ค์ ์กฐ์ฌํด๋ณด๊ณ ๊ฐ์ฅ ์ดํด์ ๋์์ด ๋์๋ ์ด๋ฏธ์ง๋ฅผ ์ฒจ๋ถํ๋ค.
MVVM์์ ์ค์ํ ๊ฒ์ View์ ViewModel์ ๋ฐ์ธ๋ฉ์ด๋ฏ๋ก ์คํธ๋ฆผ์ ๋ํ ์ดํด๊ฐ ํ์ํ๋ค.
ํ์ตํ๋ ๋ด์ฉ์์ ์ค์ํ ์ ์ Stream์ ๊ตฌ๋
์ด ViewController์์ ์ผ์ด๋์ผ ํ๋ค๋ ๊ฒ์ด์๋ค. View์์ ๋ฐ์ํ ์ด๋ฒคํธ๋ฅผ View Model ๋ด๋ถ์ cancellable์ ์ ์ฅํ์ฌ ๊ตฌ๋
ํ๊ฒ ๋๋ค๋ฉด stream์ด ๋๊ฒจ ์ด์ํ ์ฝ๋๊ฐ ๋ ์ ์๋ค.
</br>
</details>
<details>
<summary><big>โ
Compositional Layoutํ์ฉ</big></summary>
</br>
## sectionProvider
* Section์์ ์์ ๊ฐ ๊ฐ๊ฒฉ์ ์ฃผ๊ธฐ ์ํด `UICollectionViewCompositionalLayout.list`์ฌ์ฉ์ด ์๋ `sectionProvider`์ ์ฉ.
## layout.scrollDirection
* ์ปฌ๋ ์
๋ทฐ ๋ ์ด์์์ด ์คํฌ๋กค๋๋ ์ถ์ ๊ฒฐ์ ํ๋ ์์ฑ.
## section.orthogonalScrollingBehavior
* ํ์ฌ ๋ ์ด์์๋ฐฉํฅ์ ์์ง๋ฐฉํฅ์ผ๋ก ์คํฌ๋กค ์คํ์ผ์ ์ฃผ๋ ์์ฑ.
</details>
---
</br>
# ๐ ์ฐธ๊ณ ๋งํฌ
* [๐ Apple Docs - Combine](https://developer.apple.com/documentation/combine)
* [๐ Apple Docs - UIContextualAction](https://developer.apple.com/documentation/uikit/uicontextualaction)
* [๐ Apple Docs - associatedtype](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/generics/)
* [๐ WWDC - Combine](https://developer.apple.com/videos/play/wwdc2019/722/)
* [๐ WWDC - Combine in Practice](https://developer.apple.com/videos/play/wwdc2019/721)
* [๊ณฐํ๊น - MVVM](https://www.youtube.com/watch?v=M58LqynqQHc)
* [Github - Combine-MVVM](https://github.com/mcichecki/combine-mvvm)
* [Github - todolist-mvvm](https://github.com/jalehman/todolist-mvvm)
* [Github - CombineCocoa](https://github.com/CombineCommunity/CombineCocoa)
* [Rx-MVVM์ ์ฌ๋ฐ๋ฅธ ์ฌ์ฉ๋ฒ - saebyuck_choom](https://velog.io/@dawn_dancer/iOS-Rx-MVVM%EC%9D%98-%EC%98%AC%EB%B0%94%EB%A5%B8-%EC%82%AC%EC%9A%A9%EB%B2%95-saebyuckchoom)