
## Ground Rules
### ๊ท์น
- TIL, ์ผ์ผ ํ๊ณ ์์ฑ ์๊ฐ(๋งค์ผ 00์๋ถํฐ 1์๊ฐ ์์ฑ ์งํ)
- ํ์ดํ๋ก๊ทธ๋๋ฐ ์๊ฐ ์ ํ ์ต๋ 30๋ถ
### ์คํฌ๋ผ
- ์ค์ 11์ ๋์ค์ฝ๋์์ ์งํ
- ๊ธ์ผ ์งํ ์ฌํญ ๊ณต์ ํ๊ธฐ(์ค๋์ ํ ์ผ)
### ํ๋ก์ ํธ ๊ท์น
- ๋ค์ด๋ฐ ์ค์ํ๊ธฐ(๊ฐ์ด๋ ๋ผ์ธ)
- ์ปค๋ฐ ๋ฉ์์ง ๊ท์น ์งํ
- ์ฝ๋์ ๋ํ ๊ธฐ๋ก ๊ทธ๋๊ทธ๋ ํ๊ธฐ
## ์ผ์ผ ์คํฌ๋ผ
### ๐ 06/26
- ์ค๋์ ์ปจ๋์
- idinaloq: ๋น๊ฐ์์ ๋๋ฌด ์ตํด์ ๐ฆ
- hoon: ๋ฐฉํ์ด ํ๋ณตํ์ต๋๋ค.๐
- ํน์ด์ฌํญ
- idinaloq: ๋ฐฉํ๋ ๊ณต๋ถ๋ฅผ ์ํ์ด์๐ญ
- hoon: ๋ฐฉํ์ด ๋ ํ์ํด์...๐คฃ
- ์ค๋ ํ ์ผ
- [x] ํ๋ํ์ต ์์ต
- [x] JSON ํ์ผ ๋ถ์
- [x] STEP 1 ๋ชจ๋ธ ํ์
๊ตฌํ
### ๐ 06/27
- ์ค๋์ ์ปจ๋์
- idinaloq: ์์ด ์์ข์์๐ฆ๐ฆ๐ฆ
- hoon: ํน ์ค์ต๋๋ค.
- ํน์ด์ฌํญ
- idinaloq: ์ค๋์ 2~3์์ ์ ์๋๊ฑธ๋ก...!
- hoon: ์คํ์ ์ด๋์ ๋ค๋
์ค๊ฒ ์ต๋๋ค. ๐๐ป
- ์ค๋ ํ ์ผ
- [x] STEP 1 PR ์ฝ๋ฉํธ ๋ต๋ณ ๋ฌ๊ธฐ
- [ ] ์คํ ๋ ์ด์์ ์ ๋ณตํ๊ธฐ ๊ณต๋ถ
- [ ] ์ฝ๋๋ฒ ์ด์ค๋ก UI ๊ทธ๋ฆฌ๊ธฐ ๊ณต๋ถ
### ๐ 06/28
- ์ค๋์ ์ปจ๋์
- idinaloq: ํน์์ ์ปจ๋์
์ด ์ข์์!
- hoon: ์กฐ๊ธ ํผ๊ณคํฉ๋๋ค๐คฃ
- ํน์ด์ฌํญ
- idinaloq: ๋ฐ๋ฆฐ TIL ์ฐ๊ธฐ
- hoon: ์คํ ๋ ์ด์์ ๋ ๊ณต๋ถํด์ผ ํด์....
- ๊ณตํต: ์คํ 8์ ๋ฆฌ๋ทฐ์ด ๋ง๋จ ๐
- ์ค๋ ํ ์ผ
- [x] STEP2 ์ฒซ ๋ฒ์งธ ํ๋ฉด ๊ตฌํํ๊ธฐ
- [x] ๋ฆฌ๋ทฐ์ด ๋ฉด๋ด
### ๐ 06/29
- ์ค๋์ ์ปจ๋์
- idinaloq: ์ปจ๋์
๋ง ์ข์ต๋๋ค..
- hoon: ๋น๊ฐ ์ค๋ ๊ฟ๊ฟํด์โ๏ธ
- ํน์ด์ฌํญ
- idinaloq: ์ง๊ฐํ์ต๋๋ค..์ฃฝ๊ฒ ์ต๋๋ค ๐ญ๐โโ๏ธ
- hoon: ๊ฐ๋ฅํ๋ค๋ฉด ๋ฎ์ ์...๐คฃ
- ์ค๋ ํ ์ผ
- [x] STEP2 TableView ๊ตฌํํ๊ธฐ
- [x] ์ธ ๋ฒ์งธ ๋ทฐ ๊ตฌํํ๊ธฐ
### ๐ 06/30
- ์ค๋์ ์ปจ๋์
- idinaloq: ์ต๊ณ ์
๋๋ค๐
- hoon: ๋ซ๋ฐฐ๋~๐
- ํน์ด์ฌํญ
- idinaloq: ์ผ์ฐ PRํ ์ ์๋ค๋ฉด ์๊ฒฝ์ ์๋ก ๋ง์ถ๊ธฐ!
- hoon: ํ ์์คํฐ๋ ์์ต ๋ฐ ํ์๋ค๊ณผ ์ ๊น ์คํฌ๋ผ์ ํ ์ ์์ต๋๋ค
- ์ค๋ ํ ์ผ
- [x] ๋ฆฌํฉํ ๋ง
- [x] STEP2 PR
- [x] README ์์ฑ
### ๐ 07/03
- ์ค๋์ ์ปจ๋์
- idinaloq: 2์ผ๋์ ์ ์ฌ์ด์ ์ข์์ ใ
ใ
ใ
- hoon: ์ฃผ๋ง์ด ๋๋ฌด ํ๋ณตํ์ต๋๋ค๐
- ํน์ด์ฌํญ
- idinaloq: ์ ๊น ๋ณผ์ผ ๋ณด๋ฌ ๋ค๋
์ค๊ฒ ์ต๋๋ค
- hoon: ํ๋ ํ์ต ์์ต๐
- ์ค๋ ํ ์ผ
- [ ] PR ์ฝ๋ฉํธ ๋ต๋ณ ๋จ๊ธฐ๊ธฐ
- [x] ํ๋ ํ์ต ์์ต ๋ฐ ๊ฐ์ธ ๊ณต๋ถ
### ๐ 07/04
- ์ค๋์ ์ปจ๋์
- idinaloq: ์์ฆ ๋๋ฌด ์ ์ฌ๋๊ฑฐ๊ฐ์์...์ข๋ค์ ใ
ใ
๐
- hoon: ์ฅ๋ง๋ผ ์ตํด์๐ง๏ธ
- ํน์ด์ฌํญ
- idinaloq: PR์์ ์๋ฃํ๊ณ ์คํ ๋ ์ด์์ ๊ณต๋ถํ๊ธฐ
- hoon: ๋น๊ฐ ์ค์ง ์๋๋ค๋ฉด ์ ๋
์ ์ด๋์ ๋ค๋
์ค๊ฒ ์ต๋๋ค๐คฃ
- ์ค๋ ํ ์ผ
- [x] PR ์ฝ๋ฉํธ ๋ต๋ณ ๋จ๊ธฐ๊ธฐ
### ๐ 07/05
- ์ค๋์ ์ปจ๋์
- idinaloq: ๊ทธ์ ๊ทธ๋์๐
- hoon: ํผ๊ณคํฉ๋๋ค๐ญ
- ํน์ด์ฌํญ
- idinaloq: ์ค๋ํ ์ผ ๋๋ด๊ธฐ
- hoon: ๊ฐ๋ฅํ๋ค๋ฉด ๋ฎ์ ๊ณผ ์ด๋์...
- ์ค๋ ํ ์ผ
- [x] STEP 3 ๊ตฌํ
- [ ] STEP 3 PR ๋ณด๋ด๊ธฐ
### ๐ 07/06
- ์ค๋์ ์ปจ๋์
- idinaloq: ์กธ๋ฆฌ๋ค์..
- hoon: ์์นจ์ด ํ๋ค์ด์๐คฃ
- ํน์ด์ฌํญ
- idinaloq: ๋นจ๋ฆฌ๋๋ด๊ณ ๋ฎ์ ์๊ธฐ!!(์คํ5์์ดํ ์์)
- hoon: ๋ฎ์ ์ด ํ์ํฉ๋๋ค๐ฅบ
- ์ค๋ ํ ์ผ
- [x] STEP 3 PR ๋ณด๋ด๊ธฐ
- [x] ํ๋ ํ์ต ์์ต
### ๐ 07/07
- ์ค๋์ ์ปจ๋์
- idinaloq: ๋นจ๋ฆฌ...๋นจ๋ฆฌ๋ด์ผ...๐ต
- hoon: ๋๋ด๊ณ ์ ์๊ฐ์ ํ๋ณตํฉ๋๋ค๐ฅณ
- ํน์ด์ฌํญ
- idinaloq: ํ๋ก์ ํธ ๋ง๋ฌด๋ฆฌํ๊ธฐ!
- hoon: ์ค๋์ด ๋ง์ง๋ง ๋ ๐
- ์ค๋ ํ ์ผ
- [x] STEP 3 PR ๋ต๋ณ
- [x] README ์์ฑ
# ๐ช ๋ง๊ตญ๋ฐ๋ํ
## ๐ ๋ชฉ์ฐจ
1. [์๊ฐ](#-์๊ฐ)
2. [ํ์](#-ํ์)
3. [ํ์๋ผ์ธ](#-ํ์๋ผ์ธ)
4. [์๊ฐํ๋ ํ๋ก์ ํธ ๊ตฌ์กฐ](#-์๊ฐํ๋-ํ๋ก์ ํธ-๊ตฌ์กฐ)
5. [์คํ ํ๋ฉด](#-์คํ-ํ๋ฉด)
6. [ํธ๋ฌ๋ธ ์ํ
](#-ํธ๋ฌ๋ธ-์ํ
)
7. [์ฐธ๊ณ ๋งํฌ](#-์ฐธ๊ณ -๋งํฌ)
8. [ํ ํ๊ณ ](#-ํ-ํ๊ณ )
</br>
## ๐ ์๊ฐ
ํ, ์ด๋๋๋กํฌ(`hoon`, `idinaloq`) ํ์ด ๋ง๋ ๋ง๊ตญ๋ฐ๋ํ ๊ฐ์ด๋ ์ฑ์
๋๋ค. 1900.04.14 ~ 1900.11.12 ๊น์ง ๊ฐ์ต๋ ํ๋ฆฌ ๋ง๊ตญ๋ฐ๋ํ ์ค ํ๊ตญ ์ถํ์์ ์๊ฐํฉ๋๋ค. ๊ฐ๊ฐ์ ํ
์ด๋ธ ๋ทฐ ์
์ ํด๋ฆญํ๋ฉด ์์ธํ ์ถํ์ ๋ด์ฉ์ ํ์ธํ ์ ์์ต๋๋ค.
* ์ฃผ์ ๊ฐ๋
: `UITableView`, `UITableViewCell`, `JSON`, `JSONDecoder`, `Dynamic Type`, `AutoLayout`, `appDelegate`, `sceneDelegate`
</br>
## ๐จโ๐ป ํ์
| idinaloq | hoon |
| :--------: | :--------: |
| <Img src = "https://user-images.githubusercontent.com/109963294/235301015-b81055d2-8618-433c-b680-58b6a38047d9.png" width = "200" height="200"/> |<Img src="https://i.imgur.com/zXoi5OC.jpg" width="200" height="200"> |
|[Github Profile](https://github.com/idinaloq) |[Github Profile](https://github.com/Hoon94) |
</br>
## โฐ ํ์๋ผ์ธ
|๋ ์ง|๋ด์ฉ|
|:--:|--|
|2023.06.26.| - `ExpositionUniverselle`, `Item` ํ์
์์ฑ |
|2023.06.27.| - `ExpositionUniverselle`, `Item` ํ์
๋ฆฌํฉํ ๋ง |
|2023.06.28.| - `ExpositionUniverselleViewController UI` ๊ตฌํ <br> - `JSON` ํ์ผ ๋์ฝ๋ฉ <br> - `touchUpExpositionItemListButton` ๋ฉ์๋ ์ถ๊ฐ |
|2023.06.29.| - `ExpositionItemListViewController UI` ๊ตฌํ <br> - `itemListTableView` ์ถ๊ฐ <br> - `ItemUITableViewCellStyleSubtitle` ํด๋์ค ์์ฑ <br> - `ExpositionItemViewController UI` ๊ตฌํ <br> - ๋ฆฌํฉํ ๋ง |
|2023.06.30.| - ๋ฆฌํฉํ ๋ง <br> - README ์์ฑ |
|2023.07.01.| - `lazy var`๋ก ์ ์ธ๋ ํ๋กํผํฐ ์์ <br> - `ExpositionUniverselle` ํ์
์ฐ์ฐ ํ๋กํผํฐ ์ถ๊ฐ <br> - `viewDidLoad` ์์ <br> - ๋งค์ง๋ฆฌํฐ๋ด ์์ , `JSONFile` ํ์
์ถ๊ฐ |
|2023.07.03.| - `configureCell` ๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ฌ `cell`์ ์ ๊ทผํ๋๋ก ์์ <br> - `itemImage` ํด๋ก์ ์ ๊ฑฐ, `expositionItem` ์์๋ก ๋ณ๊ฒฝ <br> - `addArrangedSubviews` ๋งค๊ฐ๋ณ์์ธ `subviews` ํ์
์์ <br> - `JSONFile` ํ์
์ด `CustomStringConvertible` ์ฑํํ๋๋ก ์์ |
|2023.07.04.| - `viewDidLoad` ์ถ์ํ ์์ค ์์ <br> - `AlertController` ์ถ๊ฐ |
|2023.07.05.| - `applicaion(_:supportedInterfaceOrientationsFor:)` ๋ฉ์๋ ์ถ๊ฐ <br> - `ExpositionUniverselleViewController` ์คํ ๋ ์ด์์ ์ ์ฝ ์ถ๊ฐ <br> - `configureItemImageConstraint` ๋ฉ์๋ ์ถ๊ฐ <br> - ๊ธฐ์กด์ ์
์ ์ปค์คํ
์
๋ก ๋ณ๊ฒฝ <br> - ์คํ ๋ ์ด์์ ์ ์ฝ์กฐ๊ฑด ์์ |
|2023.07.06.| - `String+Extension` ์ถ๊ฐ <br> - `dynamic type` ์ ์ฉ๋๋๋ก ๋ ์ด๋ธ ์์ <br> - `addTarget`์ `addAction`์ผ๋ก ์์ <br> |
|2023.07.07.| - `cell imageView constraints` ์์ <br> - `itemImage`์ `contentCompressionResistance priority` ๋ณ๊ฒฝ <br> - `changeOrientation` ๋ค์ด๋ฐ ์์ ๋ฐ ๊ฐ๋
์ฑ ๊ฐ์ <br> - `configureCell` ๋ฉ์๋ ์์ <br> - README ์์ฑ |
</br>
## ๐ ์๊ฐํ๋ ํ๋ก์ ํธ ๊ตฌ์กฐ
### โน๏ธ File Tree
โโโ Expo1900
โย ย โโโ Expo1900
โย ย ย ย โโโ Model
โย ย ย ย โย ย โโโ ExpositionUniverselle
โย ย ย ย โย ย โโโ Item
โย ย ย ย โย ย โโโ JSONFile
โย ย ย ย โโโ View
โย ย ย ย โย ย โโโ LaunchScreen
โย ย ย ย โย ย โโโ ExpositionItemCell
โย ย ย ย โโโ Controller
โย ย ย ย โย ย โโโ AppDelegate
โย ย ย ย โย ย โโโ SceneDelegate
โย ย ย ย โย ย โโโ ExpositionUniverselleViewController
โย ย ย ย โย ย โโโ ExpositionItemListViewController
โย ย ย ย โย ย โโโ ExpositionItemViewController
โย ย ย ย โย ย โโโ AlertController
โย ย ย ย โโโ Extension
โย ย ย ย โ โโโ JSONDecoder+Extension
โย ย ย ย โ โโโ String+Extension
โย ย ย ย โโโ Error
โย ย ย ย โย ย โโโ NSDataAssetError
โย ย ย ย โโโ Assets
โย ย ย โโโ Info
โ
โโโ ํํ๊ณ .md
โโโ README.md
### ๐ Class Diagram
<p align="center">
<img width="1000" src="https://hackmd.io/_uploads/r1CkYErY3.jpg">
</p>
</br>
## ๐ป ์คํ ํ๋ฉด
| ์คํ ํ๋ฉด(์ธ๋ก) | ๋์ฝ๋ฉ ์ค๋ฅ | ํฐํธ ํฌ๊ธฐ ๋ณ๊ฒฝ |
|:-------------:|:--------:| :--------: |
|<Img src="https://github.com/Hoon94/SwiftAlgorithm/assets/43189761/b9701fea-486c-405c-b15e-8531946e3777" width="250">|<Img src="https://github.com/Hoon94/SwiftAlgorithm/assets/43189761/24187f3c-0085-42f6-be1c-9a6c750605c7" width="250">|<Img src="https://github.com/Hoon94/SwiftAlgorithm/assets/43189761/3df7f44a-1bfb-41b1-b616-fad2d825ca7f" width="250">|
| ์คํ ํ๋ฉด(๊ฐ๋ก) |
|:-------------:|
|<Img src="https://github.com/Hoon94/SwiftAlgorithm/assets/43189761/a7630713-0a66-4555-8bbc-cd8059b4c388" height="250">|
</br>
## ๐งจ ํธ๋ฌ๋ธ ์ํ
1๏ธโฃ **JSON ํฌ๋ฉง์ ๋ฐ์ดํฐ์ ๋งค์นญํ ๋ชจ๋ธ ํ์
** <br>
-
๐ **๋ฌธ์ ์ ** <br>
`exposition_universelle_1900.json`๊ณผ `items.json`์์ ์ฌ์ฉํ๋ `JSON` ํฌ๋งท์ ๋ฐ์ดํฐ๋ ๋ค์ด๋ฐ์ snake case ํํ๋ก ์ฌ์ฉํ์์ต๋๋ค. Swift์์ ์ฌ์ฉํ๋ camel case ํํ์ ๋ฌ๋๊ณ , ์ถ์ฝ์ด๋ฅผ ์ฌ์ฉํ๋ ๋ค์ด๋ฐ๋ ์์์ต๋๋ค. `JSONDecoder`๋ฅผ ์ฌ์ฉํ์ฌ ๋์ฝ๋ฉ์ ํ๋ฉด JSON ํ์ผ์ ์ ์ฅ๋ ๋ค์ด๋ฐ์ ์ ํฌ๊ฐ ์์ฑํ ํ์
์ ํ๋กํผํฐ์ ๋ค์ด๋ฐ๊ณผ ์ผ์น์์ผ์ผ ํ์ง๋ง, Swift์์ ์ฌ์ฉํ๋ [๋ค์ด๋ฐ ๊ฐ์ด๋๋ผ์ธ](https://www.swift.org/documentation/api-design-guidelines/#naming)์์ ํ์
๊ณผ ํ๋กํ ์ฝ์ **UpperCamelCase**๋ก, ๋๋จธ์ง๋ **lowerCamelCase**๋ก ์์ฑํด์ผ ๋๋ค๋ ๊ฐ์ด๋๋ผ์ธ๊ณผ ๋ค๋ฅด๋ค๋ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค.
๐ **ํด๊ฒฐ๋ฐฉ๋ฒ** <br>
`CodingKeys`๋ฅผ ์ฌ์ฉํ์ฌ `JSON` ํ์ผ์ ์ ์ฅ๋ ๋ค์ด๋ฐ์ ๋ค์๊ณผ ๊ฐ์ด ๊ฐ๊ฐ์ ํ๋กํผํฐ์ ์ผ์ด์ค์ ๋ง๊ฒ ์ง์ ํ์์ต๋๋ค.
```swift
struct Item: Decodable {
let name: String
let imageName: String
let shortDescription: String
let totalDescription: String
enum CodingKeys: String, CodingKey {
case name
case imageName = "image_name"
case shortDescription = "short_desc"
case totalDescription = "desc"
}
}
```
<br>
2๏ธโฃ **์ฝ๋๋ฒ ์ด์ค๋ก `UI` ๊ตฌํํ๊ธฐ** <br>
-
๐ **๋ฌธ์ ์ ** <br>
์ง๊ธ๊น์ง์ ํ๋ก์ ํธ๋ ์คํ ๋ฆฌ๋ณด๋๋ฅผ ์ด์ฉํด์ `UI`๋ฅผ ๊ตฌํํ์ต๋๋ค. ์ง๊ธ์ ์๊ท๋ชจ ํ๋ก์ ํธ๋ ๊ด์ฐฎ์ง๋ง, ๋ง์ฝ ํ๋ก์ ํธ๊ฐ ๋ณต์กํด์ง๋ค๋ฉด ํ์
์ ํ ๋ ์คํ ๋ฆฌ๋ณด๋์์์ ์ถฉ๋์ด ์์ ๊ฐ๋ฅ์ฑ์ด ์๊ธฐ ๋๋ฌธ์ ์ฝ๋๋ฒ ์ด์ค๋ก `UI`๋ฅผ ๊ตฌํํ๊ธฐ๋ก ํ์ง๋ง, `View`๋ฅผ ์ถ๊ฐํด๋ ์๋ฎฌ๋ ์ดํฐ์ ์๋ฌด๊ฒ๋ ํ์๋์ง ์๋ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค.
๐ **ํด๊ฒฐ๋ฐฉ๋ฒ** <br>
ํด๋น ๋ฌธ์ ๋ ๊ฐ๊ฐ์ ๋ทฐ์ ์ ์ฝ์กฐ๊ฑด์ ์ค์ ํ์ง ์์๋ ๋ฌธ์ ๋ก, ์คํ ๋ฆฌ๋ณด๋ ์์์ `UI`๋ฅผ ์ถ๊ฐํ ๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์์ ๊ฐ์ธ `constant`๋ก ์ง์ ๋์ด์ `UI`๊ฐ ๋ณด์ด์ง๋ง, ์ฝ๋๋ฒ ์ด์ค๋ก `UI`๋ฅผ ๊ตฌํํ ๋๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์๋ฌด ๊ฐ๋ ์๊ธฐ ๋๋ฌธ์ `UI`๊ฐ ํ์๋์ง ์์๋ ๋ฌธ์ ์์ต๋๋ค. `topAnchor`, `leadingAnchor`, `trailingAnchor`, `bottomAnchor`, `heightAnchor`, `widthAnchor`๋ฑ์ ์ ์ฝ์กฐ๊ฑด์ ์ฌ์ฉํด์ ํ๋ฉด์ ์ด๋ ์์น์ ๋ทฐ๋ฅผ ๋์ธ์ง์ ๋ํ ์คํ ๋ ์ด์์ ์ ์ฝ์กฐ๊ฑด์ ์ค์ ํ์ต๋๋ค.
<br>
3๏ธโฃ **๋ทฐ ์
๋ฐ์ดํธ** <br>
-
๐ **๋ฌธ์ ์ ** <br>
๋ทฐ ์ปจํธ๋กค๋ฌ์์ ์ฐ์ฐ ํ๋กํผํฐ๋ฅผ ์ฌ์ฉํ์ฌ ๋ทฐ๋ฅผ ์์ฑํ ๋ `UILabel`์ ๊ฒฝ์ฐ ์
๋ ฅ๋์ด์ผ ํ๋ ๋ด์ฉ๋ค์ ํจ๊ป ์
๋ ฅํ์ฌ ์์ฑํ์์ต๋๋ค.
```swift
private var expositionUniverselle: ExpositionUniverselle?
private lazy var titleLabel: UILabel = {
let label: UILabel = UILabel()
label.text = = expositionUniverselle?.title
label.textAlignment = .center
return label
}()
```
์์ ๊ฐ์ด ์ฌ์ฉํ๋ฉด `expositionUniverselle`์ด ์ด๊ธฐํ ์ ์ ๊ฐ์ ์ ๊ทผํ์ฌ `title` ๊ฐ์ ๋ฐ์์ค๋ ค๊ณ ํ๊ธฐ ๋๋ฌธ์ `lazy var`๋ฅผ ์ฌ์ฉํด์ผ ํ์ต๋๋ค.
๐ **ํด๊ฒฐ๋ฐฉ๋ฒ** <br>
๋ทฐ๋ฅผ ์์ฑํ๊ณ ์ดํ์ `updateLabel()` ๋ฉ์๋์์ `label.text`์ ๊ฐ์ ํ ๋นํ๋ ์์
๋ค์ ๋ชจ์์ ์งํํ๋๋ก ํ์์ต๋๋ค.
```swift
private var expositionUniverselle: ExpositionUniverselle?
private let titleLabel: UILabel = {
let label: UILabel = UILabel()
label.textAlignment = .center
return label
}()
...
private func updateLabel() {
navigationItem.title = "๋ฉ์ธ"
titleLabel.text = expositionUniverselle?.titleForLabel
visitorsLabel.text = expositionUniverselle?.visitorsForLabel
locationLabel.text = expositionUniverselle?.locationForLabel
durationLabel.text = expositionUniverselle?.durationForLabel
totalDescriptionLabel.text = expositionUniverselle?.totalDescription
}
```
<br>
4๏ธโฃ **๋งค์ง๋ฆฌํฐ๋ ์ต์ํ** <br>
-
๐ **๋ฌธ์ ์ ** <br>
`exposition_universelle_1900`, `items`๊ฐ์ `JSON` ํ์ผ์์ ์ง์ ์ ์ผ๋ก ํ์ผ์ ์ฝ์ด์ฌ ๋ ์๋์ ๊ฐ์ด ํ์ผ ์ด๋ฆ์ ์ง์ ์ง์ ํ์ฌ ์ฝ์ด์ค๋๋ก ํ์์ต๋๋ค.
```swift
private func decodeExpositionUniverselle() {
do {
expositionUniverselle = try JSONDecoder().decode(ExpositionUniverselle.self, from: "exposition_universelle_1900")
} catch {
print(error)
}
}
```
์ถํ ํ์ผ์ ์ด๋ฆ์ด๋ ํ์ผ ์์ฒด๊ฐ ๋ฐ๋๋ ๊ฒฝ์ฐ ํ์ผ ์ด๋ฆ์ ๊ณ์ ๋ณ๊ฒฝํด์ผ ๋๋๋ฐ, ๋ง์ฝ ๋ทฐ ์ปจํธ๋กค๋ฌ๊ฐ ์ปค์ง๊ฑฐ๋ ํ์ผ์ด ๋ง์์ง๋ ๊ฒฝ์ฐ ์ผ์ผ์ด ํ์ผ๋ช
์ ์
๋ ฅํ๋ ์ฝ๋๋ฅผ ์ฐพ์ ๋ณ๊ฒฝํ๊ธฐ ์ด๋ ค์ธ ๊ฒ ๊ฐ๋ค๊ณ ์๊ฐํ์ต๋๋ค.
๐ **ํด๊ฒฐ๋ฐฉ๋ฒ** <br>
์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด `JSONFile.swift` ํ์ผ์ ๋ง๋ค๊ณ ํ์ผ ์ด๋ฆ์ ๊ด๋ฆฌํ ์ ์๋๋ก ํ์ต๋๋ค.
```swift
enum JSONFile: CustomStringConvertible {
case exposition
case items
var description: String {
switch self {
case .exposition:
return "exposition_universelle_1900"
case .items:
return "items"
}
}
}
```
์ด๋ ๊ฒ ํ๋์ ํ์ผ์์ `JSON` ํ์ผ์ ๊ด๋ฆฌํ๊ธฐ ๋๋ฌธ์ ํ์ผ์ ์์ ์ด ํ์ํ ๊ฒฝ์ฐ ํ์ผ๋ช
์ ์
๋ ฅํ๋ ์ฝ๋๋ฅผ ์ฐพ์๊ฐ ํ์ ์์ด `JSONFile.swift` ํ์ผ์์ ๋ชจ๋ ํด๊ฒฐํ ์ ์์์ต๋๋ค. ์์ `JSONFile` ํ์
์ ์ ์ฉํ์ฌ ์๋์ ๊ฐ์ด ์ฝ๋๋ฅผ ์์ ํ์์ต๋๋ค.
```swift
private func decodeExpositionUniverselle() {
do {
expositionUniverselle = try JSONDecoder().decode(ExpositionUniverselle.self, from: JSONFile.exposition.name)
} catch {
print(error)
}
}
```
<br>
5๏ธโฃ **๋ค์ ๋ทฐ ์ปจํธ๋กค๋ฌ์ ๋ฐ์ดํฐ ์ ๋ฌ** <br>
-
๐ **๋ฌธ์ ์ ** <br>
๋ ๋ฒ์งธ ๋ทฐ ์ปจํธ๋กค์ธ `ExpositionItemViewController`์์ `JSON` ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉํ ๋ `decodeItems()` ๋ฉ์๋๋ฅผ ํตํด ๋์ฝ๋ฉ ๋ ๋ฐ์ดํฐ๋ฅผ `itemList`์ ํ ๋นํฉ๋๋ค. ์ธ ๋ฒ์งธ ๋ทฐ ์ปจํธ๋กค์ธ `ExpositionItemListViewController`์์๋ ์์ ๊ฐ์ด ๋์ฝ๋ฉ์ ํ๋ฉด ๊ฐ์ ๊ฒฐ๊ณผ๊ฐ ๋์ค์ง๋ง, ๋์ผํ ํ์ผ์ ๋ ๋ฒ ๋์ฝ๋ฉ ํ๋ ์ค๋ณต๋ ๋์์ ์ํํฉ๋๋ค.
๐ **ํด๊ฒฐ๋ฐฉ๋ฒ** <br>
๋ ๋ฒ์งธ ๋ทฐ ์ปจํธ๋กค์์ ๋ค์ ๋ทฐ ์ปจํธ๋กค๋ฌ์ ๋ฐ์ดํฐ๋ฅผ ๋๊ฒจ์ฃผ์ด ๋์ผํ ์์
์ ์ฌ๋ฌ ๋ฒ ๋ฐ๋ณตํ์ง ์๋๋ก ํ์์ต๋๋ค. ํนํ ์ธ ๋ฒ์งธ ํ๋ฉด์ ๊ฐ ์์ดํ
์ ๋ํ ์์ธ ์ ๋ณด๋ฅผ ๋ณด์ฌ์ฃผ๋ ํ๋ฉด์
๋๋ค. ์ธ ๋ฒ์งธ ํ๋ฉด์์ ์ ์ฒด ์์ดํ
๋ชฉ๋ก์ ๋์ฝ๋ฉ ํ์ฌ ์ฌ์ฉํ๊ธฐ๋ณด๋ค๋ ๊ฐ๋ณ ์์ดํ
์ ๋ํ ๋ฐ์ดํฐ๋ง์ ์ด์ ํ๋ฉด์์ ์ ๋ฌ๋ฐ์ ์ฌ์ฉํ๋๋ก ์๋์ ๊ฐ์ด ๊ตฌํํ์ต๋๋ค.
```swift
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let expositionItemViewController: ExpositionItemViewController = ExpositionItemViewController(item: itemList[indexPath.row])
navigationController?.pushViewController(expositionItemViewController, animated: true)
}
```
<br>
6๏ธโฃ **`JSONDecoder Extension`** <br>
-
๐ **๋ฌธ์ ์ ** <br>
๊ฐ์ `decode()` ๋ฉ์๋๋ฅผ ํ์ฌ ๋ ๋ฒ ์ฌ์ฉํ๊ณ ์์ต๋๋ค. `do - try - catch` ๊ณผ์ ์์ `NSDataAsset`์ ์ต์
๋ ๋ฐ์ธ๋ฉ์ ํด์ ์ฌ์ฉํ๊ณ ์์์ต๋๋ค. `decode()` ๋ฉ์๋๋ฅผ ์ฌ์ฉํ๋ ํ์๊ฐ ๋์ด๋๋ค๋ฉด, `NSDataAsset`์ ๋ํ ์ต์
๋ ๋ฐ์ธ๋ฉ ์ฝ๋๊ฐ ์ค๋ณต๋์์ต๋๋ค.
๐ **ํด๊ฒฐ๋ฐฉ๋ฒ** <br>
`Extension`์ผ๋ก `decode`์ ๊ธฐ๋ฅ์ ๋ค์๊ณผ ๊ฐ์ด ํ์ฅํ์ฌ ์ฌ์ฉํ์ต๋๋ค.
```swift
extension JSONDecoder {
func decode<T: Decodable>(_ type: T.Type, from assetName: String) throws -> T {
guard let json = NSDataAsset(name: assetName) else {
throw NSDataAssetError.invalidDataAsset
}
return try JSONDecoder().decode(type, from: json.data)
}
}
```
<br>
7๏ธโฃ **์ฌ์ฉ์์๊ฒ ์๋ฌ ์๋ฆฌ๊ธฐ** <br>
-
๐ **๋ฌธ์ ์ ** <br>
๊ธฐ์กด ์ฝ๋์์ ์๋ฌ๊ฐ ๋ฐ์ํ๋ ๊ฒฝ์ฐ `print(error)`์ ๊ฐ์ด ํฐ๋ฏธ๋์ ์๋ฆฌ๋๋ก ๊ตฌํ์ ํ์ต๋๋ค. ๋ง์ฝ ์ฑ์์ ์๋ฌ๊ฐ ๋ฐ์ํ์ ๋ ์ฌ์ฉ์๋ค์ ์๋ฌ ๋ฐ์ ์ํฉ์ ์ธ์งํ์ง ๋ชปํ๋ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค.
```swift
do {
itemList = try JSONDecoder().decode([Item].self, from: JSONFile.items.description)
} catch NSDataAssetError.invalidDataAsset {
...
} catch {
print(error)
}
```
๐ **ํด๊ฒฐ๋ฐฉ๋ฒ** <br>
์ฑ์ ์ฌ์ฉํ๋ ์ฌ์ฉ์๋ค์๊ฒ ์๋ฌ ๋ฐ์๊ณผ ๊ฐ์ ํฌ๋ฆฌํฐ์ปฌํ ์ํฉ์ ์๋ฆฌ๊ณ ์ถ์ ๋, [UIAlertController](https://developer.apple.com/documentation/uikit/uialertcontroller)๋ฅผ ์ฌ์ฉํด์ ์ฌ์ฉ์์๊ฒ [Alerts](https://developer.apple.com/design/human-interface-guidelines/alerts)์ ํ์ํ๋ ๋ฐฉ๋ฒ์ด ์๋ฌ ๋ฐ์ ์ํฉ์ ์๋ฆฌ๋ ๊ฐ์ฅ ์ข์ ๋ฐฉ๋ฒ์ด๋ผ๊ณ ์๊ฐํ๊ณ , ์ผ๋ฐ์ ์ผ๋ก๋ ์ด๋ฌํ ๋ฐฉ๋ฒ์ ์ฌ์ฉํ๊ณ ์๊ธฐ ๋๋ฌธ์ ์ผ๋ฟ์ผ๋ก ์๋ฌ ๋ฐ์์ ์๋ฆฌ๋๋ก ์ถ๊ฐํ์ต๋๋ค.
```swift
do {
itemList = try JSONDecoder().decode([Item].self, from: JSONFile.items.description)
} catch NSDataAssetError.invalidDataAsset {
let alert: UIAlertController = AlertController().configureAlert(errorName: NSDataAssetError.invalidDataAsset.localizedDescription)
present(alert, animated: true)
} catch {
...
}
```
<br>
8๏ธโฃ **`NavigationCotroller` ์ฌ์ฉ** <br>
-
๐ **๋ฌธ์ ์ ** <br>
ํ๋ฉด ๊ฐ์ ์ ํ์์ `navigation` ๋ฐฉ์์ ์ฌ์ฉํ์ฌ `push`๋ฅผ ํ์ฌ ๋ค์ ํ๋ฉด์ผ๋ก ๋์ด๊ฐ๋๋ก ๊ตฌ์ฑํ์์ต๋๋ค. ์ฝ๋๋ฒ ์ด์ค๋ก `UI`๋ฅผ ์์ฑํ๋ฉด์ `NavigationController`๋ฅผ ์ด๊ธฐํ๋ฅผ ์ฝ๋๋ก ์ง์ ํด์ฃผ์ด์ผ ํ์ต๋๋ค.
๐ **ํด๊ฒฐ๋ฐฉ๋ฒ** <br>
`SceneDelegate.swift` ํ์ผ์์ ์๋์ ์ฝ๋๋ฅผ ์
๋ ฅํ์์ต๋๋ค
```swift
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
//UIWindowScene์ scene์ ๊ด๋ฆฌํ๊ณ , UIScene์ ์ฑ์์ ์ฌ์ฉ์ ์ธํฐํ์ด์ค๋ฅผ ํ์ํ๋ ์ญํ
//UIScene์ ์์๋ฐ์ UIWindowScene์ผ๋ก Scene์ ๊ด๋ฆฌํ๊ธฐ ์ํด ๋ค์ด ์บ์คํ
์ ์ฌ์ฉ
guard let windowScene = (scene as? UIWindowScene) else { return }
// SceneDelegate์ ํ๋กํผํฐ์ ํ ๋น
// windowScene์ด ๊ด๋ฆฌํ window ์์ฑ
window = UIWindow(windowScene: windowScene)
// ๋งจ ์ฒ์ ๋ณด์ฌ์ค ViewController ํ ๋น
let mainViewController: ExpositionUniverselleViewController = ExpositionUniverselleViewController()
// mainViewController๋ฅผ ์ฌ์ฉํ์ฌ UINavigationController ์ด๊ธฐํ
// ๋ค๋น๊ฒ์ด์
์ ์ฒซ ํ๋ฉด์ mainViewController๋ก ์ง์
let navigationController: UINavigationController = UINavigationController(rootViewController : mainViewController)
// ํ๋ฉด์ ์ฒซ ์์ ์ง์ ์ ํ ๋น
window?.rootViewController = navigationController
// window๋ฅผ ํ๋ฉด์ ํ์ํ๊ณ key window๋ก ์ฌ์ฉ
window?.makeKeyAndVisible()
}
```
์์์ `UINavigationController`์ `init`์ ํตํด ๋ค๋น๊ฒ์ด์
์ ์ฒซ ํ๋ฉด์ ์ง์ ํ์์ต๋๋ค.
<br>
9๏ธโฃ **ํ๋ฉด์ ํ์ ๊ณ ์ ** <br>
-
๐ **๋ฌธ์ ์ ** <br>
์ฒซ ํ๋ฉด์์๋ `portrait mode`๋ง ์ง์ํ๊ณ ์ถ์์ต๋๋ค. ์ด ์ธ์ ํ๋ฉด์๋ ๋ชจ๋ ํ์ ์ ๋ํด ์ง์ํ๊ณ ์ถ์์ต๋๋ค. ์ ์ฒด ์ฑ์ ๋์์ ๋ํ ํ๋ฉด์ ํ๋ก์ ํธ์ `Deployment Info`์์ ์ค์ ํ ์ ์์์ต๋๋ค. ํ์ง๋ง ๊ฐ๊ฐ์ ํ๋ฉด์ ๋ฐ๋ผ ํ์ ์ ๋ํ ์ ํ์ ๊ฑธ๊ธฐ๋ ํ๋ค์์ต๋๋ค.
๐ **ํด๊ฒฐ๋ฐฉ๋ฒ** <br>
์ฒซ ๋ฒ์งธ ํ๋ฉด์ ์ธ๋ก๋ก ๊ณ ์ ๋์ด์ผ ํ๊ณ , ์์๋ณด๋ ์ค `appDelegate`๋ฅผ ์ด์ฉํ ๋ฐฉ๋ฒ์ ์ฌ์ฉํ๊ฒ ๋์์ต๋๋ค. `appDelegate`๋ ์ฑ์ ๋ผ์ดํ ์ฌ์ดํด๊ณผ ์ด๋ฒคํธ๋ฅผ ์ฒ๋ฆฌํ๋ ๊ฐ์ฒด๋ก์จ ์ฑ์ ์์, ์ข
๋ฃ ๋ฐ ์ํ๋ณํ ๋ฑ๊ณผ ๊ด๋ จ๋ ์์
์ ์ํํฉ๋๋ค. `AppDelegate.swift`์ ๋ค์ ์ฝ๋๋ฅผ ์์ฑํ์์ต๋๋ค.
```swift
// AppDelegate.swift
var isOnlyPortrait: Bool = true
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
if isOnlyPortrait {
return [.portrait]
} else {
return [.all]
}
}
// ExpositionUniverselleViewController.swift
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
delegate?.isOnlyPortrait = true
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
delegate?.isOnlyPortrait = false
}
```
๋ง์ฝ `isOnlyPortrait`๊ฐ `true`์ผ ๋ ์ธํฐํ์ด์ค ๋ฐฉํฅ์ ๋ํ๋ด๋`UIInterfaceOrientationMask`๊ฐ ์ธ๋ก ๋ฐฉํฅ๋ง์ ํ์ฉํ๋๋ก `[.portrait]`์ ๋ฐํํ๊ณ , false์ธ ๊ฒฝ์ฐ ๋ชจ๋ ๋ฐฉํฅ์ ํ์ฉํ๋๋ก `[.all]`์ ๋ฐํํ๋๋ก ํ์์ต๋๋ค. `isOnlyPortrait`๋ฅผ ํ๋ฉด ๊ณ ์ ์ ์ํ๋ ์ฒซ ๋ฒ์งธ ๋ทฐ์์ ๋ทฐ๊ฐ ๋ํ๋ ๋ viewWillAppear์์ true๋ก ์ค์ ํด์ ์ธ๋ก๋ก ๊ณ ์ ๋๊ฒ ํ๊ณ , ๋ค๋ฅธ ๋ทฐ๋ก ๋์ด๊ฐ ๋ viewWillDisappear์์ false๋ก ์ค์ ํด์ ํ๋ฉด์ด ๋ชจ๋ ๋ฐฉํฅ์ ํ์ฉํ๋๋ก ํ์์ต๋๋ค.
<br>
๐ **`Dynamic Type`** <br>
-
๐ **๋ฌธ์ ์ ** <br>
1. `Dynamic Type`์ ์ฌ์ฉ์๊ฐ ์ํ๋ ํ
์คํธ ์ฌ์ด์ฆ๋ก ์ปจํ
์ธ ๋ฅผ ๋ณผ ์ ์๋ ์ ์ฐ์ฑ์ ์ ๊ณตํ๋ ๊ธฐ๋ฅ์
๋๋ค. ์ด ๊ธฐ๋ฅ์ ์ฌ์ฉํ์ฌ ํ
์คํธ์ ํฌ๊ธฐ๋ฅผ ํค์ฐ๊ฒ ๋๋ฉด `...`์ผ๋ก ํ์๋๋ ๋ฌธ์ ๊ฐ ์์๊ณ , ์ด๋ก ์ธํด ์ฌ์ฉ์๊ฐ ํ
์คํธ์ ํฌ๊ธฐ๋ฅผ ํฌ๊ฒ ํ์ ๋ ๋ด์ฉ์ ์ ์ ์๋ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค.
2. `Dynamic Type`์ ์ฌ์ฉํ์ฌ ํ
์คํธ์ ํฌ๊ธฐ๋ฅผ ํค์ธ ๋, ๋ฒํผ์ ํ
์คํธ๋ ์ปค์ง์ง๋ง ๋ฒํผ ์์ฒด์ ํฌ๊ธฐ์ ๋ฒํผ์ด ๋ด๊ธด ์คํ๋ทฐ์ ํฌ๊ธฐ๊ฐ ์ปค์ง์ง ์๋ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค.
๐ **ํด๊ฒฐ๋ฐฉ๋ฒ** <br>
1. `UILabel`์ `numberOfLines`๋ `UILabel`์์ ํ
์คํธ๋ฅผ ํ์ํ ์ค์ ์ต๋ ๊ฐ์๋ฅผ ์ง์ ํ๋ ๋ฐ ์ฌ์ฉ์ด ๋๊ณ , ์๋ฌด๊ฒ๋ ์ง์ ํ์ง ์์ผ๋ฉด ๊ธฐ๋ณธ๊ฐ์ด `1`๋ก ์ง์ ๋๋ ๊ฒ์ ์๊ฒ ๋์์ต๋๋ค. ์ด ๊ฐ์ด `1`์ผ ๋ ํ
์คํธ๊ฐ ํ ์ค์๋ง ํ์๋๊ธฐ ๋๋ฌธ์ `numberOfLines = 0`์ผ๋ก ์ค์ ์ ํด์ ๋ง์ฝ ๋์ด๊ฐ ์ด๊ณผํ๋ ๊ฒฝ์ฐ ์ค๋ฐ๊ฟ์ด ๋๋๋ก ๋ณ๊ฒฝํ์ต๋๋ค.
2. `์ด๋ฏธ์ง - ๋ฒํผ - ์ด๋ฏธ์ง`์ ํํ๋ก ํ๋์ ์คํ๋ทฐ์ ๋ฌถ์ฌ์์๊ณ , ์ด๋ฏธ์ง์ ์ ์ฝ์กฐ๊ฑด์ด ๋ฒํผ์ ์ฐธ์กฐํ๋๋ก ํ์ต๋๋ค. ์ด๋ฐ ๊ฒฝ์ฐ ์ด๋ฏธ์ง์ ์ฐ์ ์์๊ฐ ๋์์ ธ ๋ฒํผ ํฌ๊ธฐ๊ฐ ๋ณ๊ฒฝ๋์ง ์์ ๋ฌธ์ ์์ต๋๋ค. ์ด๋ฏธ์ง์ ์ ์ฝ์กฐ๊ฑด์ ๋ฌธ์ ๊ฐ ์์๊ธฐ ๋๋ฌธ์ ์ด๋ฅผ ์ ๊ฑฐํ๊ณ , `setContentCompressionResistancePriority(_:for:)`๋ฉ์๋๋ฅผ ์ฌ์ฉํด์ ์ด๋ฏธ์ง๋ทฐ์ `CompressionResistance Priority`๋ฅผ ์กฐ์ ํ์ต๋๋ค.
```swift
private let rightItemImage: UIImageView = {
let imageName: String = "flag"
let image: UIImage? = UIImage(named: imageName)
let imageView: UIImageView = UIImageView(image: image)
imageView.contentMode = .scaleAspectFit
imageView.setContentCompressionResistancePriority(.fittingSizeLevel, for: .vertical)
return imageView
}()
private let leftItemImage: UIImageView = {
let imageName: String = "flag"
let image: UIImage? = UIImage(named: imageName)
let imageView: UIImageView = UIImageView(image: image)
imageView.contentMode = .scaleAspectFit
imageView.setContentCompressionResistancePriority(.fittingSizeLevel, for: .vertical)
return imageView
}()
```
`setContentCompressionResistancePriority(_:for:)` ๋ฉ์๋๋ ๋ทฐ๊ฐ ์ ํฉํ ํฌ๊ธฐ๋ฅผ ์ ์งํ๊ธฐ ์ํด ์ฐ์ ์์๋ฅผ ์ค์ ํ๋ ๋ฉ์๋๋ก, ์ฒซ ๋ฒ์งธ ๋งค๊ฐ๋ณ์๋ ๋ทฐ๊ฐ ์ ํฉํ ํฌ๊ธฐ๋ฅผ ์ ์งํ๊ธฐ ์ํ ์ฐ์ ์์๋ฅผ ๋ํ๋ด๊ณ ์ ๋ฌ์ธ์๋ฅผ ๊ธฐ๋ณธ ๊ฐ ๋ณด๋ค ๋ฎ์ `.fittingSizeLevel`์ผ๋ก ์ค์ ํด์ ์ด๋ฏธ์ง๋ทฐ์ ํฌ๊ธฐ์ ์คํ๋ทฐ์ ์ฐ์ ์์๊ฐ ๋ฎ์์ง์ง ์๋๋ก ํ์ต๋๋ค.
๋ ๋ฒ์งธ ๋งค๊ฐ๋ณ์๋ ์ํ ๋ฐฉํฅ, ์์ง ๋ฐฉํฅ ๋ฑ ํฌ๊ธฐ ์กฐ์ ์ ๋ํด ์ฐ์ ์์๋ฅผ ์ค์ ํ๋ ๊ฒ์ผ๋ก, ์ฒซ ๋ฒ์งธ ํ์ด์ง๋ ์์ง์ผ๋ก๋ง ํํ๋๋ฉด ๋๊ธฐ ๋๋ฌธ์ ์ ๋ฌ์ธ์๋ฅผ`.vertical`๋ก ์ค์ ํด์ ๋ทฐ๊ฐ ์์ง ๋ฐฉํฅ์ผ๋ก ์์ถ๋๋ ๊ฒฝ์ฐ ๋ด์ฉ์ด ์๋ฆฌ์ง ์๋๋ก ํ์ต๋๋ค.
<br>
</br>
## ๐ ์ฐธ๊ณ ๋งํฌ
- [๐Apple Docs: JSONDecoder](https://developer.apple.com/documentation/foundation/jsondecoder)
- [๐Apple Docs: UITableView](https://developer.apple.com/documentation/uikit/uitableview)
- [๐Apple Docs: Filling a table with data](https://developer.apple.com/documentation/uikit/views_and_controls/table_views/filling_a_table_with_data)
- [๐Apple Docs: Encoding and Decoding Custom Types](https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types)
- [๐Apple Docs: Typography - Dynamic Type](https://developer.apple.com/design/human-interface-guidelines/typography#iOS-iPadOS-Dynamic-Type-sizes)
- [๐Apple Docs: loadView()](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621454-loadview)
- [๐stackOverflow: NSAttributedString](https://stackoverflow.com/questions/43723345/nsattributedstring-change-the-font-overall-but-keep-all-other-attributes)
- [๐blog: NavigationController๋ฅผ rootViewController๋ก ์ง์ ํ๊ธฐ](https://vanillacreamdonut.tistory.com/277)
- [๐blog: Hugging, Compression Resistance Priority](https://ios-development.tistory.com/673)
- [๐blog: ์ถ์ํ ์์ค](https://velog.io/@mincho920/%ED%81%B4%EB%A6%B0%EC%BD%94%EB%93%9C-%ED%95%A8%EC%88%98%EC%9D%98-%EC%B6%94%EC%83%81%ED%99%94-%EC%88%98%EC%A4%80-%EA%B7%B8%EB%A6%AC%EA%B3%A0-Switch-%EB%AC%B8)
</br>
## ๐ฅ ํ ํ๊ณ
- [ํ ํ๊ณ ๋งํฌ](https://github.com/idinaloq/ios-exposition-universelle/wiki/%ED%8C%80-%ED%9A%8C%EA%B3%A0)
# ํ ํ๊ณ
## ์ฐ๋ฆฌํ์ด ์ํ ์
- ํ๋ก์ ํธ ์งํ ๊ธฐ๊ฐ์์ ์ต์ํ์ง ์์ ์ฝ๋๋ฒ ์ด์ค๋ก STEP3๊น์ง ๊ตฌํํ ์
- ๋ฐ์ํ๋ ์๋ฌ์ ์์ธ๊ณผ ํด๊ฒฐ ๋ฐฉ๋ฒ์ ๋น ๋ฅด๊ฒ ์ฐพ์๋ธ ์
## ์๋ก์๊ฒ ์ข์๋ ์ ํผ๋๋ฐฑ
- to. idinaloq: ๊ผผ๊ผผํ๊ฒ ์ดํดํ์ง ๋ชปํ ๋ถ๋ถ์ ๋ํด ์ง๊ณ ๋์ด๊ฐ๋ ๋ชจ์ต์์ ๋ฐฐ์ธ ์ ์ด ๋ฌด์ฒ ๋ง์์ต๋๋ค. ํนํ ์ด๋ ํ ๊ธฐ๋ฅ์ ์ถ๊ฐํ๊ฑฐ๋ ์ฌ์ฉํ ๋ ์ด๊ฒ ์ค์ ์ ์ฉ์ ํ์ ๋ ์ฅ๋จ์ ์ด ๋ฌด์์ธ์ง ๋น๊ตํ์ฌ ํ์ํ ๋ถ๋ถ๋ง์ ์ ์ฉํ๋ ๊ฒ์ด ๋ง์ด ๋์ ๋์์ต๋๋ค.
- to. hoon: ์ฝ๋๋ฒ ์ด์ค๋ก ์์ฑํ๋ ๊ฒ์ด ์ฒ์์ด์๋๋ฐ, ์ด๋ฐํ ์๊ฐ์๋ ๋ถ๊ตฌํ๊ณ ํ๋ฃจ ์ ๋ ๊ณต๋ถํ๋ ์๊ฐ์ ๊ฐ์ ๊ฒ์ด ๋์์ด ๋ง์ด ๋์์ต๋๋ค.
## ์๋ก์๊ฒ ํ๊ณ ์ถ์ ๋ง
- to. idinaloq: ํ์ผ๋ก ๋ง๋ ์ ๋ง ์ค๋ ์๊ฐ๊ฐ์ด ํ๋ก์ ํธ๋ฅผ ์งํํ ๊ฒ ๊ฐ์ต๋๋ค. ๊ณต๋ถํ๋ฉด์ ์๋ก์ด ๊ธฐ๋ฅ์ ์ถ๊ฐํ ๋ ์ด๊ฑธ ์ ์ฌ์ฉํด์ผ์ง๋ผ๊ณ ์๊ฐํ๋ ๋ชจ์ต์ ๋ณด๋ฉฐ ๊ผญ ๋ฐฐ์ฐ๊ณ ์ถ์ ๋ถ๋ถ์ด๋ผ๊ณ ์๊ฐํ์ต๋๋ค. ๋ค์์ ๋ ๋ค๋ฅธ ๋ฏธ์
์ผ๋ก ๊ฐ์ด ํ๋ฉด ์ข๊ฒ ์ต๋๋ค.๐
- to. hoon: ๋ค์๋ถํด ๋ฆ์ง ์๊ฒ ์ต๋๋ค.๐โโ๏ธ
# PR: STEP 1
## ๊ณ ๋ฏผ๋์๋ ์
### Assets import ํด๋ ๊ตฌ์ฑ
- ์ ๊ณต๋ฐ์ expo_assets ํด๋๋ ๋ด๋ถ์ ๊ฐ๊ฐ์ ํด๋๋ฅผ ๊ฐ์ง๊ณ ์๋ ํํ๋ก ํด๋๊ฐ ๊ตฌ์ฑ๋์ด ์์์ต๋๋ค. expo_assets ํด๋ ์์ฒด๋ฅผ import ํ๋ฉด ํ์ ์ด๋ฏธ์ง๋ฅผ ๋ถ๋ฌ์ฌ ๋์ ํ์ผ๋ช
์ผ๋ก ์ ๊ทผ ์ ํด๋๋ช
๊ณผ ํจ๊ป ํ์ผ๋ช
์ ์
๋ ฅํด์ผ ํ๋ ๋ถ๋ถ์ด ์์ด ๋ด๋ถ ํด๋๋ง์ ๊ฐ๋ณ์ ์ผ๋ก import ํ์์ต๋๋ค.
### Content.json
- `Asset`์ ์ถ๊ฐํ ๋ ๋ฐ์ดํฐ ํ์ผ ์ธ์ ์ด๋ฏธ์ง ํ์ผ์ `Content.json`๋ผ๋ `json`ํฌ๋ฉง์ ํ์ผ์ด ์์์ต๋๋ค. ํด๋น ํ์ผ๋ ์ถ๊ฐ๋ฅผ ํด์ผ ๋ ์ง ๊ณ ๋ฏผ์ ํ์๋๋ฐ, ์ฌ๋ฌ ๊ฐ์ง๋ก ๊ฒ์ํด ๋ณธ ๊ฒฐ๊ณผ `Contents.json` ํ์ผ์ `Xcode`์์ `Asset Catalog`์ ํฌํจ๋ ๋ฆฌ์์ค์ ์ด๋ฆ, ์ ํ, ํฌ๊ธฐ ๋ฐ ๊ธฐํ ์์ฑ๊ณผ ๊ด๋ จ๋ ์ ๋ณด๋ฅผ ํฌํจํ๋ ํ์ผ์ธ ๊ฒ์ ์๊ฒ ๋์์ต๋๋ค. ๋ฐ๋ผ์ ๋ฐ๋ก ์ถ๊ฐํ์ง๋ ์์์ต๋๋ค.
### Decodable
- `typealias Codable`์๋ `Encodable`, `Decodable` ๊ธฐ๋ฅ์ด ์์ง๋ง, ์ด๋ฒ ํ๋ก์ ํธ์์๋ ์ธ์ฝ๋ฉ์ ํ ํ์๊ฐ ์๋ค๊ณ ์๊ฐํด์ `Decodable`์ ์ฌ์ฉํ์์ต๋๋ค.
### CodingKeys ์ฌ์ฉ
- Json ํฌ๋ฉง ๋ค์ด๋ฐ์ `_`๊ฐ ์์์ง๋ง, `camelCase` ์ฌ์ฉ์ ์ํด `enum CodingKeys: String, CodingKey{...}`๋ฅผ ์ถ๊ฐํด์ `camelCase`์ ๋ง๋ ์ด๋ฆ์ผ๋ก ๋ณ๊ฒฝํ์์ต๋๋ค.
## ์กฐ์ธ์ ์ป๊ณ ์ถ์ ๋ถ๋ถ
### MVC ํด๋ ๊ทธ๋ฃนํ
- ํ์ฌ MVC ํจํด์ ๋ง์ถ์ด ํ์ผ์ ๊ทธ๋ฃนํํ์์ต๋๋ค. ์ฌ๋ผํ๋ผ๋ฉด ์ด๋ป๊ฒ ํ์ผ์ ๊ตฌ๋ถํ์ฌ ๋๋์ง ๊ถ๊ธํฉ๋๋ค!
MVC ๊ธฐ์ค
- Model: ์ฑ์์ ์ฌ์ฉํ๋ ๋ฐ์ดํฐ์ ๋น์ง๋์ค ๋ก์ง์ ๋ด๋นํฉ๋๋ค.
- View: ๋ฐ์ดํฐ๋ฅผ ์๊ฐ์ ์ผ๋ก ํํํ๋ ์ญํ ์ ๋ด๋นํฉ๋๋ค.
- Controller: Model๊ณผ View ์ฌ์ด์ ์ด๋ฒคํธ ์ฒ๋ฆฌ๋ฅผ ๋ด๋นํฉ๋๋ค.
Model: `ExpositionUniverselle.swift` ํ์ผ๊ณผ `Item.swift` ํ์ผ์ ๋ฐ์ดํฐ์ ๋น์ง๋์ค ๋ก์ง์ ๋ด๋นํ๋ ๋ถ๋ถ์ด๋ผ๊ณ ์๊ฐํ์ต๋๋ค.
View: `Main.storyboard`๋ ์ฌ์ฉ์์๊ฒ ์๊ฐ์ ์ผ๋ก ๋ณด์ด๋ ๋ถ๋ถ์ ๋ด๋นํ๊ณ ์๋ค๊ณ ์๊ฐํ์ต๋๋ค.
Controller: `AppDelegate.swift`์ `SceneDelegate.swift`๊ฐ์ ๊ฒฝ์ฐ๋ ์ง์ ์ ์ผ๋ก `Model`๊ณผ `View` ์ฌ์ด์ ์ค๊ณ์ ์ญํ ์ ํ์ง๋ ์์ง๋ง `MVC`ํจํด์ผ๋ก ๊ตฌ๋ถํ๋ค๋ฉด ์ปจํธ๋กค๋ฌ ์ญํ ์ ์ข ๋ ๊ฐ๊น๋ค๊ณ ์๊ฐํฉ๋๋ค.
๊ทธ๋ ๊ฒ ์๊ฐํ ์ด์ ๋ `AppDelegate`๊ฐ์ ๊ฒฝ์ฐ ํน์ ํ `scenes`, `views`, `view controllers`์ ํ์ ๋์ง ์๊ณ ์ฑ ์์ฒด๋ฅผ ํ๊ฒํ๋ ์ด๋ฒคํธ์ ๋์ํ๋ ์ญํ ์ ์ํํ๋ฉฐ, `SceneDelegate`์ `UI` ์ํ์ ๋ฐ๋ฅธ ์ด๋ฒคํธ ์ฒ๋ฆฌ๋ฅผ ๋ด๋นํ๊ธฐ ๋๋ฌธ์
๋๋ค.
`let`์ผ๋ก ์์ ํ์์ต๋๋ค.
๊ณต์ ๋ฌธ์์์ ์์๋ก ์๋ ์ํ ์ฝ๋๋ `var`๋ฅผ ์ฌ์ฉํ์์ต๋๋ค. ํ์ง๋ง ์ ํฌ๊ฐ ์ฐพ์๋ณธ ๋ด์ฉ์ผ๋ก๋ ํ์ ๊ฐ์ด ๋ณ๊ฒฝ๋ ์ผ์ด ์๋ ๊ฒฝ์ฐ์๋ `let`์ ์ฌ์ฉํ์ฌ ์์ ์ฑ์ ๋์ผ ์ ์๋ค๊ณ ์๊ฐํ์์ต๋๋ค. ๊ณต์ ๋ฌธ์์ ์์ ์ฝ๋์๋ ์ดํ์ ๋ณ๊ฒฝ์ ๋ํ ์์๋ ์์ง๋ง `var`๋ฅผ ์ฌ์ฉํด ๋ณ์๋ก ์ฌ์ฉํ๊ณ ์๋๋ฐ ์ฌ๊ธฐ์ ์ด๋ ๊ฒ ์ฌ์ฉํ๋ ์ด์ ๊ฐ ์์๊น์?
์์ ์ฝ๋: [JSONDecoder](https://developer.apple.com/documentation/foundation/jsondecoder)
์๋
ํ์ธ์ ์ฌ๋ผํ(@1Consumption)๐ hoon, idinaloq์
๋๋ค.
๋ง๊ตญ๋ฐ๋ํ STEP 2 PR ๋๋ฆฝ๋๋ค. ์ฒ์์ผ๋ก ์ฝ๋ ๋ฒ ์ด์ค๋ก UI๋ฅผ ๊ตฌํํ๋ฉฐ ๋ง์ ์ด๋ ค์์ด ์์๊ณ ์ฌ์ ํ ๋ถ์กฑํ ์ ์ด ๋ง์ ๊ฒ ๊ฐ์ต๋๋ค.
์ฒ์ฒํ ๋ณด์๊ณ ์ฝ๋ฉํธ ๋จ๊ฒจ์ฃผ์๋ฉด ์ด์ฌํ ๊ณต๋ถํด์ ๋ค์ ๋ต๋ณ ๋จ๊ธฐ๋๋ก ํ๊ฒ ์ต๋๋ค. ๊ฐ์ฌํ๊ฒ ์ต๋๋ค.๐
์ข์ ์ฃผ๋ง ๋ณด๋ด์ธ์.๐ฅณ
# PR: STEP 2
## ๊ณ ๋ฏผ๋์๋ ์
### ์คํ ๋ ์ด์์ ์ฌ์ฉ
- ์คํ ๋ฆฌ๋ณด๋๋ฅผ ์ฌ์ฉํ์ง ์๊ณ ์ฝ๋ ๋ฒ ์ด์ค๋ก ๋ทฐ๋ฅผ ์์ฑํ์ฌ `addSubview`๋ฅผ ํตํด ๋ทฐ๋ฅผ ์ถ๊ฐํ๋ฉด ์๋ฎฌ๋ ์ดํฐ์ ๋์ค์ง ์์์ต๋๋ค. ์์ฑํ ๋ทฐ๋ฅผ ํ๋ฉด์ ์ด๋ ์์น์ ๋์์ฃผ์ด์ผ ํ๋์ง ํ๋จํ์ง ๋ชปํ์ฌ ์๊ธด ๋ฌธ์ ์์ต๋๋ค. ์ด๋ฅผ ํด๊ฒฐํด ์ฃผ๊ธฐ ์ํด ์คํ ๋ ์ด์์์ ์ฌ์ฉํ์ฌ ํ๋ฉด์ ์ด๋ ์์น์ ๋ทฐ๋ฅผ ๋์ธ์ง์ ๋ํ ์ ์ฝ์กฐ๊ฑด์ ์ค์ ํ์์ต๋๋ค.
```swift
private func configureExpositionUniverselleScrollViewConstraint() {
NSLayoutConstraint.activate([
expositionUniverselleScrollView.topAnchor.constraint(equalTo: view.topAnchor),
expositionUniverselleScrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
expositionUniverselleScrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
expositionUniverselleScrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
}
private func configureContentStackViewConstraint() {
NSLayoutConstraint.activate([
contentStackView.topAnchor.constraint(equalTo: expositionUniverselleScrollView.topAnchor),
contentStackView.bottomAnchor.constraint(equalTo: expositionUniverselleScrollView.bottomAnchor),
contentStackView.widthAnchor.constraint(equalTo: expositionUniverselleScrollView.widthAnchor)
])
}
```
### ๋ทฐ ์
๋ฐ์ดํธ ์์
- ๋ทฐ ์ปจํธ๋กค๋ฌ์์ ์ฐ์ฐ ํ๋กํผํฐ๋ฅผ ์ฌ์ฉํ์ฌ ๋ทฐ๋ฅผ ์์ฑํ ๋ `UILabel`์ ๊ฒฝ์ฐ ์
๋ ฅ๋์ด์ผ ํ๋ ๋ด์ฉ๋ค์ ํจ๊ป ์
๋ ฅํ์ฌ ์์ฑํ์์ต๋๋ค.
```swift
private var expositionUniverselle: ExpositionUniverselle?
private lazy var titleLabel: UILabel = {
let label: UILabel = UILabel()
label.text = = expositionUniverselle?.title
label.textAlignment = .center
return label
}()
```
์์ ๊ฐ์ด ์ฌ์ฉํ๋ฉด `expositionUniverselle`์ด ์ด๊ธฐํ ์ ์ ๊ฐ์ ์ ๊ทผํ์ฌ `title` ๊ฐ์ ๋ฐ์์ค๋ ค๊ณ ํ๊ธฐ ๋๋ฌธ์ `lazy var`๋ฅผ ์ฌ์ฉํด์ผ ํ์ต๋๋ค. ์ด๋ฌํ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ทฐ๋ฅผ ์์ฑํ๊ณ ์ดํ์ `updateLabel()` ๋ฉ์๋์์ `label.text`์ ๊ฐ์ ํ ๋นํ๋ ์์
๋ค์ ๋ชจ์์ ์งํํ๋๋ก ํ์์ต๋๋ค.
```swift
private var expositionUniverselle: ExpositionUniverselle?
private let titleLabel: UILabel = {
let label: UILabel = UILabel()
label.textAlignment = .center
return label
}()
...
private func updateLabel() {
navigationItem.title = "๋ฉ์ธ"
titleLabel.text = expositionUniverselle?.titleForLabel
visitorsLabel.text = expositionUniverselle?.visitorsForLabel
locationLabel.text = expositionUniverselle?.locationForLabel
durationLabel.text = expositionUniverselle?.durationForLabel
totalDescriptionLabel.text = expositionUniverselle?.totalDescription
}
```
### `addArrangedSubview()` ๋ฉ์๋ ์ค๋ณต ์ ๊ฑฐ
- `ExpositionUniverselleViewController`์ ์ฒซ ํ๋ฉด์์๋ ๋ค์ํ ๋ทฐ๋ค์ ์ฌ์ฉํ์ฌ ์ ์ฒด ํ๋ฉด์ ๊ตฌ์ฑํ์์ต๋๋ค. ๋ง์ ๋ทฐ๋ค์ `contentStackView`์ ์ถ๊ฐํ์ฌ ๊ด๋ฆฌํ๊ธฐ ์ํด ์๋์ ๊ฐ์ด ์ฌ๋ฌ ๋ฒ์ ๋์ผํ `addArrangedSubview()` ๋ฉ์๋๋ฅผ ์์ฑํด์ผ ํ์ต๋๋ค.
```swift
override func viewDidLoad() {
super.viewDidLoad()
decodeExpositionUniverselle()
horizontalStackView.addArrangedSubview(leftFlagImage)
horizontalStackView.addArrangedSubview(expositionItemListButton)
horizontalStackView.addArrangedSubview(rightFlagImage)
verticalStackView.addArrangedSubview(titleLabel)
verticalStackView.addArrangedSubview(posterImage)
verticalStackView.addArrangedSubview(visitorsLabel)
verticalStackView.addArrangedSubview(durationLabel)
verticalStackView.addArrangedSubview(locationLabel)
verticalStackView.addArrangedSubview(totalDescriptionLabel)
verticalStackView.addArrangedSubview(horizontalStackView)
expositionUniverselleScrollView.addSubview(verticalStackView)
view.addSubview(expositionUniverselleScrollView)
}
```
`addArrangedSubview()` ๋ฉ์๋์ ๋งค๊ฐ๋ณ์๋ก ์ฃผ์ด์ง๋ ๋ทฐ๋ง ๋ณ๊ฒฝํ๋ฉด ์ค๋ณต๋ ์ฝ๋๋ฅผ ์ค์ผ ์ ์์ด ๊ฐ๋
์ฑ์ ๋์ผ ์ ์์์ต๋๋ค.
```swift
private func configureView() {
addArrangedSubviews(to: buttonStackView, elements: [leftItemImage,
expositionItemListButton,
rightItemImage
])
addArrangedSubviews(to: contentStackView, elements: [titleLabel,
posterImage,
visitorsLabel,
locationLabel,
durationLabel,
totalDescriptionLabel,
buttonStackView])
expositionUniverselleScrollView.addSubview(contentStackView)
view.addSubview(expositionUniverselleScrollView)
}
private func addArrangedSubviews(to superview: UIStackView, elements subviews: [AnyObject]) {
subviews.forEach {
guard let subview = $0 as? UIView else { return }
superview.addArrangedSubview(subview)
}
}
```
์์ ๊ฐ์ด `addArrangedSubviews()` ๋ฉ์๋์ ์ฌ๋ฌ ๋ทฐ๋ค์ `Array`๋ก ๋ฌถ์ด ๋งค๊ฐ๋ณ์๋ก ๋๊ธฐ๋ฉด ์์ฐจ์ ์ผ๋ก `superview`์ `subview`๋ค์ด ์ถ๊ฐ๋๋๋ก ํ์์ต๋๋ค.
### ๋งค์ง๋ฆฌํฐ๋ ์ต์ํ
- `exposition_universelle_1900`, `items`๊ฐ์ `JSON` ํ์ผ์์ ์ง์ ์ ์ผ๋ก ํ์ผ์ ์ฝ์ด์ฌ ๋ ์๋์ ๊ฐ์ด ํ์ผ ์ด๋ฆ์ ์ง์ ์ง์ ํ์ฌ ์ฝ์ด์ค๋๋ก ํ์์ต๋๋ค.
```swift
private func decodeExpositionUniverselle() {
do {
expositionUniverselle = try JSONDecoder().decode(ExpositionUniverselle.self, from: "exposition_universelle_1900")
} catch {
print(error)
}
}
```
์ถํ ํ์ผ์ ์ด๋ฆ์ด๋ ํ์ผ ์์ฒด๊ฐ ๋ฐ๋๋ ๊ฒฝ์ฐ ํ์ผ ์ด๋ฆ์ ๊ณ์ ๋ณ๊ฒฝํด์ผ ๋๋๋ฐ, ๋ง์ฝ ๋ทฐ ์ปจํธ๋กค๋ฌ๊ฐ ์ปค์ง๊ฑฐ๋ ํ์ผ์ด ๋ง์์ง๋ ๊ฒฝ์ฐ ์ผ์ผ์ด ํ์ผ๋ช
์ ์
๋ ฅํ๋ ์ฝ๋๋ฅผ ์ฐพ์ ๋ณ๊ฒฝํ๊ธฐ ์ด๋ ค์ธ ๊ฒ ๊ฐ๋ค๊ณ ์๊ฐํ์ต๋๋ค. ์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด `JSONFile.swift` ํ์ผ์ ๋ง๋ค๊ณ ํ์ผ ์ด๋ฆ์ ๊ด๋ฆฌํ ์ ์๋๋ก ํ์ต๋๋ค.
```swift
enum JSONFile {
case exposition
case items
var name: String {
switch self {
case .exposition:
return "exposition_universelle_1900"
case .items:
return "items"
}
}
}
```
์ด๋ ๊ฒ ํ๋์ ํ์ผ์์ JSON ํ์ผ์ ๊ด๋ฆฌํ๊ธฐ ๋๋ฌธ์ ํ์ผ์ ์์ ์ด ํ์ํ ๊ฒฝ์ฐ ํ์ผ ๋ช
์ ์
๋ ฅํ๋ ์ฝ๋๋ฅผ ์ฐพ์๊ฐ ํ์ ์์ด `JSONFile.swift` ํ์ผ์์ ๋ชจ๋ ํด๊ฒฐํ ์ ์์์ต๋๋ค. ์์ `JSONFile` ํ์
์ ์ ์ฉํ์ฌ ์๋์ ๊ฐ์ด ์ฝ๋๋ฅผ ์์ ํ์์ต๋๋ค.
```swift
private func decodeExpositionUniverselle() {
do {
expositionUniverselle = try JSONDecoder().decode(ExpositionUniverselle.self, from: JSONFile.exposition.name)
} catch {
print(error)
}
}
```
### ์ธ ๋ฒ์งธ ๋ทฐ๋ก ๋ฐ์ดํฐ ๋๊ธฐ๊ธฐ
- ๋ ๋ฒ์งธ ๋ทฐ ์ปจํธ๋กค์ธ `ExpositionItemViewController`์์ `JSON` ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉํ ๋ `decodeItems()` ๋ฉ์๋๋ฅผ ํตํด ๋์ฝ๋ฉ ๋ ๋ฐ์ดํฐ๋ฅผ `itemList`์ ํ ๋นํฉ๋๋ค. ์ธ ๋ฒ์งธ ๋ทฐ ์ปจํธ๋กค์ธ `ExpositionItemListViewController`์์๋ ์์ ๊ฐ์ด ๋์ฝ๋ฉ์ ํ๋ฉด ๊ฐ์ ๊ฒฐ๊ณผ๊ฐ ๋์ค์ง๋ง, ๋์ผํ ํ์ผ์ ๋ ๋ฒ ๋์ฝ๋ฉ ํ๋ ๊ฒ๋ณด๋ค ๋ ๋ฒ์งธ ๋ทฐ ์ปจํธ๋กค์์ ๋ค์ ๋ทฐ ์ปจํธ๋กค๋ฌ์ ๋ฐ์ดํฐ๋ฅผ ๋๊ฒจ์ฃผ๋ ํธ์ด ๋์ผํ ์์
์ ์ฌ๋ฌ ๋ฒ ๋ฐ๋ณตํ์ง ์์ ์ข๋ค๊ณ ์๊ฐํ๊ธฐ ๋๋ฌธ์ ์๋์ ๊ฐ์ด ๊ตฌํํ์ต๋๋ค.
```swift
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let expositionItemViewController: ExpositionItemViewController = ExpositionItemViewController(item: itemList[indexPath.row])
navigationController?.pushViewController(expositionItemViewController, animated: true)
}
```
### `JSONDecoder Extension`
- ๊ฐ์ `decode()` ๋ฉ์๋๋ฅผ ํ์ฌ ๋ ๋ฒ ์ฌ์ฉํ๊ณ ์๋๋ฐ, `do - try - catch` ๊ณผ์ ์์ `NSDataAsset`์ ์ต์
๋ ๋ฐ์ธ๋ฉ์ ํด์ ์ฌ์ฉํ๊ณ ์์์ต๋๋ค. `decode()` ๋ฉ์๋๋ฅผ ์ฌ์ฉํ๋ ํ์๊ฐ ๋์ด๋๋ค๋ฉด, `NSDataAsset`์ ๋ํ ์ต์
๋ ๋ฐ์ธ๋ฉ ์ฝ๋๊ฐ ์ค๋ณต๋๋ ๊ฒ ๊ฐ๋ค๊ณ ์๊ฐํ์ฌ `Extension`์ผ๋ก `decode`์ ๊ธฐ๋ฅ์ ๋ค์๊ณผ ๊ฐ์ด ํ์ฅํ์ฌ ์ฌ์ฉํ์ต๋๋ค.
```swift
extension JSONDecoder {
func decode<T: Decodable>(_ type: T.Type, from assetName: String) throws -> T {
guard let json = NSDataAsset(name: assetName) else {
throw NSDataAssetError.invalidDataAsset
}
return try JSONDecoder().decode(type, from: json.data)
}
}
```
### `CustomCell` ์ฌ์ฉ
- ๋ ๋ฒ์งธ ๋ทฐ์์ ๊ฐ๊ฐ์ ์
์ด `subtitle`์ ํ์๋ก ํ์ต๋๋ค.
```swift
cell.detailTextLabel?.text = "Subtitle"
```
์์ ๊ฐ์ด ์ ์ธํด์ `subtitle`์ ์ฌ์ฉํ๋๋ฐ, ๋ทฐ์๋ ์ ์ฉ๋์ง ์๋ ๋ฌธ์ ๊ฐ ์์๊ณ , ์ฝ๋ ๋ฒ ์ด์ค๋ก ์์ฑํด์ ์ด๋ฌํ ๋ฌธ์ ๊ฐ ์๋ค๊ณ ์๊ฐํ์ต๋๋ค. ์ปค์คํ
์
์ ์ฌ์ฉํด์ `style`์ `subtitle`๋ก ๋ค์๊ณผ ๊ฐ์ด ์ค์ ํ์ต๋๋ค.
```swift
final class ItemUITableViewCellStyleSubtitle: UITableViewCell {
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: .subtitle, reuseIdentifier: reuseIdentifier)
accessoryType = .disclosureIndicator
}
...
}
```
## ํด๊ฒฐ์ด ๋์ง ์์ ์
### `massive viewController`
- ์คํ ๋ฆฌ๋ณด๋๋ก `UI`๋ฅผ ๊ตฌํํ ๋ ์์ด์ฝ์ ์คํ ๋ฆฌ๋ณด๋์ ์์ฑํ๋ ์๊ฐ `UI`๊ฐ ์์ฑ๋๊ณ , ๊ทธ๊ฒ์ `ViewController`์ ๊ฐ๊ฐ์ `UI`๋ค์ ์ฐ๊ฒฐํด ์ฃผ๋ ์์
์ ์งํํ์์ต๋๋ค. ์ฝ๋ ๋ฒ ์ด์ค๋ก ๋ณ๊ฒฝ๋ ์ง๊ธ์ ์ฝ๋๋ฅผ ๋ณด๋ฉด, `ViewController`์์ `UI`๋ฅผ ์์ฑํ๊ณ ์ฐ๊ฒฐ๊น์ง ํ๋ ๋ ๊ฐ์ ๋์์ ํ๊ธฐ ๋๋ฌธ์ UI๋ฅผ ์์ฑํ๋ ๋ถ๋ถ์ `View`์ ๋ค์ด๊ฐ์ผ ๋๋ค๊ณ ์๊ฐํฉ๋๋ค. ์ด๋ฌํ ๊ณผ์ ๋ค์ด ์ด๋ป๊ฒ ๋๋ ์ง๊ณ , ์คํ๋์ด์ผ ํ๋์ง์ ๋ํด ์ ์์ง ๋ชปํ์ฌ `ViewController`์ ๋ชจ๋ ๊ตฌํํ ์ํ๋ก ์ด ๋ถ๋ถ์ ์์ง ํด๊ฒฐํ์ง ๋ชปํ์์ต๋๋ค.
- ๊ณ ๋ฏผํ์ ์ ์ ์ ์ด์ฃผ์ ์์ด์ฝ์ ์คํ ๋ฆฌ๋ณด๋์ ์์ฑํ๋ ์๊ฐ UI๊ฐ ์์ฑ๋๊ณ , ๋ผ๋ ๋ถ๋ถ์ด ์ด๋ค ๊ฒ์ ๋ปํ๋์ง ์ ๋ชจ๋ฅด๊ฒ ์ต๋๋ค. ์ด ๋ถ๋ถ์ ์กฐ๊ธ ๋ ์์ธํ๊ฒ ์ค๋ช
ํด ์ฃผ์๊ฒ ์ด์?
- ์คํ ๋ฆฌ๋ณด๋๋ก UI๋ฅผ ๊ตฌํํ ๋ ์์ด์ฝ์ ์คํ ๋ฆฌ๋ณด๋์ ์ถ๊ฐํ๋ฉด XML์๋ ์ถ๊ฐ๊ฐ ๋์ด UI์ ๋ํ ์ ๋ณด๊ฐ ๊ธฐ๋ก๋๋ค๊ณ ์๊ฐํ์ต๋๋ค.
-
- ์ฌ๊ธฐ ์๋ ์ฝ๋๋ค์ ์ถ์ํ ์์ค์ด ์๋ง๋ค์. ์ญํ ๋ณ๋ก ๋ฉ์๋๋ก ๋ถ๋ฆฌํด๋ณด์๊ฒ ์ด์?
- clean code์์ ์ถ์ํ ๋ ๋ฒจ ๊ณต๋ถํด๋ณด๊ธฐ
- print๋ฅผ ํ๋ฉด ์ฑ์ ์ฌ์ฉํ๋ ์ฌ์ฉ์๋ค์ ์๋ฌ ๋ฐ์ ์ํฉ์ ์ธ์งํ์ง ๋ชปํฉ๋๋ค. ์ด๋ค ๋ฐฉ๋ฒ์ด ์์๊น์?
- ์ผ๋ฟ์ผ๋ก ํ์ํ๋๋ก ์์ ํ๊ธฐ
- ์ด ์ฝ๋์ ์ญํ ์ ๋ญ๊ฐ์? ์ ํด์ฃผ์
จ๋์?
- `translatesAutoresizingMaskIntoConstraints`์ [๊ณต์ ๋ฌธ์](https://developer.apple.com/documentation/uikit/uiview/1622572-translatesautoresizingmaskintoco)๋ฅผ ๋ณด๋ฉด ๋ทฐ์ `Autoresizing mask`๋ฅผ `Auto Layout constraints`๋ก ๋ฐ๊ฟ์ง๋ฅผ ๊ฒฐ์ ํ๋ `Boolean` ๊ฐ์ด๋ผ๊ณ ์ค๋ช
ํฉ๋๋ค. `translatesAutoresizingMaskIntoConstraints`์ ๊ฐ์ด `true` ์ผ ๋ ์์คํ
์ ๋ทฐ์ `Autoresizing mask`์ ์ํด ์ง์ ๋ ์ ์ฝ ์กฐ๊ฑด์ ์์ฑํฉ๋๋ค. ์ด๋ฌํ ๊ฒฝ์ฐ์ ์๋ก์ด ์ ์ฝ ์กฐ๊ฑด์ ์ถ๊ฐํ๋ฉด ์ด์ ์ ์ฝ ์กฐ๊ฑด๊ณผ ์ถฉ๋์ด ๋ฐ์ํ ์ ์์ต๋๋ค. ์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด `translatesAutoresizingMaskIntoConstraints`์ ๊ฐ์ `false`๋ก ์ค์ ํฉ๋๋ค.
- ํด๋ก์ ์์ฑ์ ํตํด ์ธ์คํด์ค๋ฅผ ๋ง๋ค์ด์ค ์ด์ ๋ ๋ญ๊ฐ์? ์ด๋ ๊ฒํ๋ฉด ์ด๋ค ์ฅ์ ์ด ์๋๊ฐ์?
- ํด๋ก์ ๋ฅผ ์ฌ์ฉํ์ฌ ์ธ์คํด์ค๋ฅผ ์์ฑํจ์ผ๋ก์จ ๊ด๋ จ๋ ์ฝ๋๋ฅผ ๋ธ๋กํํ์ฌ ๊ฐ๋
์ฑ์ ๋์ผ ์ ์์์ต๋๋ค. ๋ทฐ๋ฅผ ์์ฑํ๊ณ ์ด์ ํ์ํ ํ๋กํผํฐ๋ค์ ํจ๊ป ์ค์ ํจ์ผ๋ก์จ ํ๋์ ์ฝ๋ ๋ธ๋ก ์์์ ํ์ํ ๋ชจ๋ ๋์์ ์ํํด ๊ฐ๋
์ฑ์ ๋์์ต๋๋ค.
- ํ๋ํ๋ ์ ๊ทผํ์ง๋ง๊ณ ๋ฉ์๋ ํ๋๋ก ์ ๊ทผ์ ์ ์ดํด๋ณด๋๊ฑด ์ด๋จ๊น์?
- `configureCell(with:item:)` ๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ฌ ์
์ ์ ๊ทผํ๋๋ก ์์ ํ์์ต๋๋ค.
- ๋ด๋ถ์์ ์
์ ๊ฐ๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๋๋ก ์์ ํ์์ต๋๋ค.
- final์ ๋ถ์ฌ์ค ์ด์ ๊ฐ ๊ถ๊ธํฉ๋๋ค. ์ด ํค์๋๋ ์ด๋ค ์ญํ ์ ํ๋์?
- [final](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/inheritance/#Preventing-Overrides) ํค์๋๋ ๋ ์ด์ ์์์ด ํ์ ์์์ ๋ช
์์ ์ผ๋ก ํํํ๋ฉฐ, ์ค๋ฒ๋ผ์ด๋ ํ ์ ์ปดํ์ผ ์๋ฌ๋ฅผ ๋ฐ์์ํฌ ์ ์์ต๋๋ค.
```swift
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
```
- ์ด ์ด๋์
๋ผ์ด์ ๋ ์ธ์ ๋ถ๋ฆฌ๋์? fatalError๋ฅผ ํด์ค ์๋๊ฐ ๊ถ๊ธํฉ๋๋ค.
- ์์๋ฐ์ `init()` ๋ฉ์๋ ๋์ `ExpositionItemViewController`์์ ์ง์ ์์ฑ์๋ฅผ ์ง์ ๊ตฌํํ์ต๋๋ค. ํ์ฌ ์ฝ๋๋ฒ ์ด์ค๋ฅผ ํตํด ์ด๊ธฐํ๋ฅผ ์งํํ๊ณ ์๊ณ , ์คํ ๋ฆฌ๋ณด๋๋ xib ํ์ผ์ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ๋ ์๊ธฐ ๋๋ฌธ์ `required init?` ๋ฉ์๋๊ฐ ์ง์ ํธ์ถ๋ ์ผ์ ์๋ค๊ณ ์๊ฐํฉ๋๋ค. ๋ง์ฝ ํธ์ถ๋๋ค๋ฉด ๋น์ ์์ ์ธ ์๋์ด๊ธฐ ๋๋ฌธ์ `fatalError`๋ฅผ ํตํด ์ฑ์ด ์คํ ์ข
๋ฃ๋๋๋ก ํ์ต๋๋ค.
- ์ด ์ฝ๋์ ์ญํ ์ด ๊ถ๊ธํฉ๋๋ค. ํ์คํ์ค ์ค๋ช
ํด์ฃผ์๊ฒ ์ด์?
- ์ด ์ฝ๋๋ ๋ค์ด๊ฒ์ด์
์ปจํธ๋กค๋ฌ๋ฅผ ๋ฃจํธ๋ทฐ ์ปจํธ๋กค๋ฌ๋ก ์ง์ ํด์ฃผ๋ ์ญํ ์
๋๋ค.
```swift
//UIWindowScene์ scene์ ๊ด๋ฆฌํ๊ณ , UIScene์ ์ฑ์์ ์ฌ์ฉ์ ์ธํฐํ์ด์ค๋ฅผ ํ์ํ๋ ์ญํ
//UIScene์ ์์๋ฐ์ UIWindowScene์ผ๋ก Scene์ ๊ด๋ฆฌํ๊ธฐ์ํด ๋ค์ด์บ์คํ
์ ์ฌ์ฉ
guard let windowScene = (scene as? UIWindowScene) else { return }
// SceneDelegate์ ํ๋กํผํฐ์ ํ ๋น
// windowScene์ด ๊ด๋ฆฌํ window ์์ฑ
window = UIWindow(windowScene: windowScene)
// ๋งจ ์ฒ์ ๋ณด์ฌ์ค ViewController ํ ๋น
let mainViewController = ExpositionUniverselleViewController()
// mainViewController๋ฅผ ์ฌ์ฉํ์ฌ UINavigationController ์ด๊ธฐํ
// ๋ค๋น๊ฒ์ด์
์ ์ฒซ ํ๋ฉด์ mainViewController๋ก ์ง์
let navigationController = UINavigationController(rootViewController : mainViewController)
// ํ๋ฉด์ ์ฒซ ์์์ง์ ์ ํ ๋น
window?.rootViewController = navigationController
// window๋ฅผ ํ๋ฉด์ ํ์ํ๊ณ key window๋ก ์ฌ์ฉ
window?.makeKeyAndVisible()
}
```
์๋
ํ์ธ์ ์ฌ๋ผํ()๐ ๋ง์ง๋ง PR ๋๋ฆฝ๋๋ค.
2์ฃผ๊ฐ ์ ์ ์์ด ์ ๋ง ๋น ๋ฅด๊ฒ ์๊ฐ์ด ํ๋ฌ๊ฐ ๊ฒ ๊ฐ์ต๋๋ค.
์ฝ๋ฉํธ ๋จ๊ฒจ์ฃผ์๋ฉด ๋ง์ง๋ง๊น์ง ์ด์ฌํ ๋ง๋ฌด๋ฆฌํด ๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.๐ฅ
์งง์ ์๊ฐ์ด์ง๋ง ์ฌ๋ผํ์๊ฒ ์ ๋ง ๋ง์ด ๋ฐฐ์ธ ์ ์์์ต๋๋ค.
๋ค์์ ๋ค๋ฅธ ๋ฏธ์
์ผ๋ก ๋ ๋ง๋ฌ์ผ๋ฉด ์ข๊ฒ ์ต๋๋ค. ๊ฐ์ฌํฉ๋๋ค.๐
# PR: STEP 3
## ๊ณ ๋ฏผ๋์๋ ์
### ์ด๋ฏธ์ง ๋น์จ ๋ฌธ์
- ์ฒ์์ ์ด๋ฏธ์ง์ ์ ์ฝ์กฐ๊ฑด์ ์ฃผ์๊ณ , ๊ทธ์ ๋ฐ๋ผ ์ด๋ฏธ์ง์ ํฌ๊ธฐ๊ฐ ๋ณํ์์ต๋๋ค. ํ์ง๋ง ์ด๋ฏธ์ง์ ํฌ๊ธฐ๊ฐ ๊ฐ์ ๋ก ์ก์ ๋๋ฆฐ ๊ฒ์ฒ๋ผ ๋์์ต๋๋ค. `contentMode`์ ๊ธฐ๋ฅ์ ์ด๋ฏธ์ง ๋น์จ์ ๋ฐ๋ผ ๋๋ ค์ฃผ๋ ๊ธฐ๋ฅ์ธ `scaleAspectFit`์ ์ฌ์ฉํด์ ์ด๋ฏธ์ง์ ํฌ๊ธฐ๋ฅผ ์กฐ์ ํ ๋ ๋น์จ์ ๋ง์ถฐ ์กฐ์ ๋๋๋ก ์ค์ ํ์์ต๋๋ค.
### ์ปค์คํ
์
์ฌ์ฉ
- `ItemUITableViewCellStyleSubtitle`์ ์ฌ์ฉํ์ฌ `subtitle` ์คํ์ผ์ ๊ธฐ๋ณธ ์
์ ์ฌ์ฉํ์์ต๋๋ค. ํ์ง๋ง ์
์ ์์ดํ
๋ค์ ๊ฐ๊ฐ ๋ค๋ฅธ ์ด๋ฏธ์ง ํฌ๊ธฐ๋ฅผ ๊ฐ์ง๊ณ ์๊ธฐ ๋๋ฌธ์ ์
์ `imageView`์ ์ ์ฝ์กฐ๊ฑด์ ์ถ๊ฐํ์ฌ์ผ ํ์ต๋๋ค. ๊ธฐ๋ณธ ์
์ ๊ฒฝ์ฐ ๊ฐ๊ฐ์ ๋ทฐ์ ๋ํ ์ ์ฝ์ ์ค์ ํ๊ธฐ๊ฐ ์ด๋ ค์ ์ต๋๋ค. ๊ฐ๊ฐ์ ๋ทฐ์ ๋ํด ์ ์ฝ์ ์ถ๊ฐํ๊ธฐ ์ํด ์ปค์คํ
์
์ ์ฌ์ฉํ์ฌ contentView์ ๋ค์ด๊ฐ๋ ๋ทฐ๋ค์ ์ ์ฝ์กฐ๊ฑด์ ์ถ๊ฐํ์์ต๋๋ค.
- `itemImageView.heightAnchor`์ ๋์ด๋ฅผ ๊ธฐ์กด์๋ ์
์ `itemImageView.widthAnchor`์ ํฌ๊ธฐ์ ๋ง์ถ์ด ์ ์ฝ์กฐ๊ฑด์ ์ถ๊ฐํ์์ต๋๋ค.
```swift
private func configureItemImageViewConstraint() {
NSLayoutConstraint.activate([
itemImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8),
itemImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
itemImageView.widthAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: 0.2),
itemImageView.heightAnchor.constraint(equalTo: itemImageView.widthAnchor),
itemImageView.topAnchor.constraint(greaterThanOrEqualTo: contentView.topAnchor, constant: 8),
itemImageView.bottomAnchor.constraint(lessThanOrEqualTo: contentView.bottomAnchor, constant: -8)
])
}
```
์ด๋ฐ ๊ฒฝ์ฐ์ ํ๋ฉด์ด ํ์ ํ๋ฉด `itemImageView`์ ๋์ด๊ฐ ์ฌ์ค์ ๋๋ฉฐ `topAnchor`์ `bottomAnchor`์ ์ ์ฝ์ ํตํด ์๊ธด `imageItemView`์ ๋์ด์ ์ถฉ๋์ด ๋ฐ์ํ์์ต๋๋ค. ์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํ์ฌ ์ ์ฝ์กฐ๊ฑด์ ์ฐ์ ์์๋ฅผ ์ ์ ํ์์ต๋๋ค.
```swift
private func configureItemImageViewConstraint() {
NSLayoutConstraint.activate([
itemImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8),
itemImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
itemImageView.widthAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: 0.2),
itemImageView.topAnchor.constraint(greaterThanOrEqualTo: contentView.topAnchor, constant: 8),
itemImageView.bottomAnchor.constraint(lessThanOrEqualTo: contentView.bottomAnchor, constant: -8)
])
let itemImageViewHeightConstraint: NSLayoutConstraint = itemImageView.heightAnchor.constraint(equalTo: itemImageView.widthAnchor)
itemImageViewHeightConstraint.priority = .defaultHigh
itemImageViewHeightConstraint.isActive = true
}
```
### ํ๋ฉด์ ํ์ ๊ณ ์
- ์ฒซ ๋ฒ์งธ ํ๋ฉด์ ์ธ๋ก๋ก ๊ณ ์ ๋์ด์ผ ํ๊ณ , ์์๋ณด๋ ์ค `appDelegate`๋ฅผ ์ด์ฉํ ๋ฐฉ๋ฒ์ ์ฌ์ฉํ๊ฒ ๋์์ต๋๋ค. `appDelegate`๋ ์ฑ์ ๋ผ์ดํ ์ฌ์ดํด๊ณผ ์ด๋ฒคํธ๋ฅผ ์ฒ๋ฆฌํ๋ ๊ฐ์ฒด๋ก์จ ์ฑ์ ์์, ์ข
๋ฃ ๋ฐ ์ํ๋ณํ ๋ฑ๊ณผ ๊ด๋ จ๋ ์์
์ ์ํํฉ๋๋ค.
- `appDelegate`์ ๋ค์ ์ฝ๋๋ฅผ ์์ฑํ์์ต๋๋ค.
```swift
var changeOrientation: Bool = true
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
if !changeOrientation {
return [.all]
} else {
return [.portrait]
}
}
```
๋ง์ฝ `changeOrientation`๊ฐ `true`์ผ ๋ ์ธํฐํ์ด์ค ๋ฐฉํฅ์ ๋ํ๋ด๋`UIInterfaceOrientationMask`๊ฐ ๋ชจ๋ ๋ฐฉํฅ์ ํ์ฉํ๋๋ก `[.all]`์ ๋ฐํํ๊ณ , `false`์ธ ๊ฒฝ์ฐ ์ธ๋ก ๋ฐฉํฅ๋ง์ ํ์ฉํ๋๋ก `[.portrait]`๋ฅผ ๋ฐํํ๋๋ก ํ์ต๋๋ค.
`changeOrientation`์ ํ๋ฉด ๊ณ ์ ์ ์ํ๋ ์ฒซ ๋ฒ์งธ ๋ทฐ์์ ๋ทฐ๊ฐ ๋ํ๋ ๋ `viewWillAppear`์์ `true`๋ก ์ค์ ํด์ ์ธ๋ก๋ก ๊ณ ์ ๋๊ฒ ํ๊ณ , ๋ค๋ฅธ ๋ทฐ๋ก ๋์ด๊ฐ ๋ `viewWillDisappear`์์ `false`๋ก ์ค์ ํด์ ํ๋ฉด์ด ๋ชจ๋ ๋ฐฉํฅ์ ํ์ฉํ๋๋ก ํ์ต๋๋ค.
### Dynamic type
- Dynamic Type์ ์ฌ์ฉ์๊ฐ ์ํ๋ ํ
์คํธ ์ฌ์ด์ฆ๋ก ์ปจํ
์ธ ๋ฅผ ๋ณผ ์ ์๋ ์ ์ฐ์ฑ์ ์ ๊ณตํฉ๋๋ค.
- ์ด ๋ถ๋ถ์ ๋ํด์ ๊ธฐ์กด์ Dynamic Type์ผ๋ก ํ
์คํธ ํฌ๊ธฐ๋ฅผ ํค์ฐ๊ฒ ๋๋ฉด ...์ผ๋ก ํ์๋๋ ๋ฌธ์ ๋ก ์ธํด ์ฌ์ฉ์๊ฐ ๋ด์ฉ์ ์ ์ ์๋ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค. `numberOfLines`์ ๊ฐ์ 0์ผ๋ก ์ค์ ํด์ ๋ชจ๋ ํ
์คํธ ๋ ์ด๋ธ์ ๋ํด ํ
์คํธ์ ํฌ๊ธฐ๊ฐ ์ปค์ ธ๋ ๋ด์ฉ์ ์ ์ ์๋๋ก `...`์ด ์๋ ์ฌ๋ฌ ์ค๋ก ํ์ํ๋๋ก ์ค์ ํ์ต๋๋ค.
## ํด๊ฒฐ์ด ๋์ง ์์ ์
### Layout ๋ฌธ์
- ๋ ๋ฒ์งธ ํ๋ฉด์ธ `ExpositionItemListViewController`์์ debug view hierarchy๋ฅผ ํ์ธํ๋ฉด ๋ค์๊ณผ ๊ฐ์ ์๋ฌ๊ฐ ๋ฐ์ํฉ๋๋ค.
```
Height and vertical position are ambiguous for UIImageView.
```
์๋ฌ ๋ฉ์์ง๋ฅผ ํ์ธํ๋ฉด `UIImageView`์ ๋์ด์ y์ถ์ ์ขํ๊ฐ ๋ถ๋ถ๋ช
ํ๋ค๋ ๋ด์ฉ์ผ๋ก ์ดํดํ์ต๋๋ค. ํ์ง๋ง ์ ์ฝ ์กฐ๊ฑด์ ์ถ๊ฐํ๋ ๋ถ๋ถ์ ๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
```swift
private func configureItemImageViewConstraint() {
NSLayoutConstraint.activate([
...
itemImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
...
])
let itemImageViewHeightConstraint: NSLayoutConstraint = itemImageView.heightAnchor.constraint(equalTo: itemImageView.widthAnchor)
itemImageViewHeightConstraint.priority = .defaultHigh
itemImageViewHeightConstraint.isActive = true
}
```
์ด์ฒ๋ผ ์ง์ `heightAnchor`๋ฅผ ํตํด `itemImageView`์ ๋์ด๋ฅผ `centerYAnchor`๋ฅผ ํตํด y์ถ์ ๋ํ ์ ์ฝ์กฐ๊ฑด์ ์ถ๊ฐํ๊ณ ์์ต๋๋ค. ์์ ์๋ฌ๋ ์ด๋ป๊ฒ ํด๊ฒฐํด์ผ ํ ๊น์?
### ImageView์ ์ ์ฝ์กฐ๊ฑด ์ถ๊ฐ์ dynamicType ์ ์ฉ
- ๊ฐ๋ณ์ ์ธ ์ด๋ฏธ์ง ํ์ผ์ ํฌ๊ธฐ๋ฅผ `ImageView`์์ ํต์ผ๋ ํฌ๊ธฐ๋ก ๋ณด์ฌ์ฃผ๊ธฐ ์ํ์ฌ `ImageView`์ ์ ์ฝ์กฐ๊ฑด์ ์ถ๊ฐํ์์ต๋๋ค. `ImageView`์ ํ
์คํธ์ ๊ฐ์ด ๋ค์ด๋๋ฏนํ์
์ ์ ์ฉํด์ ๋์ด๋๊ฑฐ๋ ์ค์ด๋ค๊ฒ ํ๊ณ ์ถ์์ผ๋, `ImageView`์ ์ ์ฝ์กฐ๊ฑด ๋๋ฌธ์ ๋ค์ด๋๋ฏนํ์
์ ์ฉ์ด ๋ถ๊ฐ๋ฅํ ๋ฌธ์ ๊ฐ ์์ต๋๋ค. ์ด๋ฐ ๊ฒฝ์ฐ์๋ ์ด๋ป๊ฒ ํด๊ฒฐํ ์ ์์๊น์?