# ๐ฌ๐ฟ๋ฐ์ค์คํผ์ค๐ฟ๐ฌ
## ๐ ๋ชฉ์ฐจ
1. [์๊ฐ](#์๊ฐ)
2. [ํ์](#ํ์)
3. [ํ์๋ผ์ธ](#ํ์๋ผ์ธ)
4. [ํ๋ก์ ํธ ๊ตฌ์กฐ](#ํ๋ก์ ํธ-๊ตฌ์กฐ)
5. [์คํ ํ๋ฉด](#์คํ-ํ๋ฉด)
6. [ํธ๋ฌ๋ธ ์ํ
](#ํธ๋ฌ๋ธ-์ํ
)
7. [์ฐธ๊ณ ๋งํฌ](#์ฐธ๊ณ -๋งํฌ)
<br>
## ์๊ฐ
์ํ์งํฅ์์ํ์ ๋ฐ์ค์คํผ์ค `open API`๋ฅผ ์ฌ์ฉํด ๋ฐ์ค์คํผ์ค ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ `CollectionView`๋ฅผ ์ฌ์ฉํ์ฌ ์ฌ์ฉ์์๊ฒ ์ํ์ ๋ณด๋ฅผ ๋ณด์ฌ์ค๋๋ค. ์บ๋ฆฐ๋๋ฅผ ์ด์ฉํด ํน์ ๋ ์ง์ ๋ฐ์ค์คํผ์ค๋ฅผ ํ์ธํ ์ ์์ต๋๋ค. ๋ํ ๋ฐ์ค์คํผ์ค ์ ๋ณด๋ฅผ ๋ฆฌ์คํธ ํํ๋ก ๋ณด์ฌ์ค ์ง ์์ด์ฝ ํํ๋ก ๋ณด์ฌ์ค ์ง ๋ ์ด์์์ ์ ํํ ์ ์์ต๋๋ค. ๋ฉ์ธ ํ๋ฉด์์ ์ํ๋ฅผ ์ ํํ๋ฉด `Daum ์ด๋ฏธ์ง API`๋ฅผ ์ฌ์ฉํ์ฌ ์ํ ํฌ์คํฐ์ ํจ๊ป ์์ธ ์ ๋ณด๋ฅผ ๋ณด์ฌ์ค๋๋ค.
์ฃผ์๊ฐ๋
: `CollectionView`, `Indicator`, `URLSession`, `async/await`, `Compositional Layout`, `Dynamic Type`, `Device Orientation`
<br>
## ํ์
| minsup | Etial Moon |
| :--------: | :--------: |
| <Img src = "https://avatars.githubusercontent.com/u/79740398?v=4" width="200"> |<Img src="https://avatars.githubusercontent.com/u/86751964?v=4" width="200" height="200"> |
|[Github](https://github.com/agilestarskim) |[Github](https://github.com/hojun-jo) |
<br>
## ํ์๋ผ์ธ
|๋ ์ง|๋ด์ฉ|
|:--:|--|
|2023.07.24| BoxOffice DTO ์์ฑ |
|2023.07.25| NetworkManager ์์ฑ |
|2023.07.27| URLSession์ async/await ๋ฐฉ์์ผ๋ก ๋ณ๊ฒฝ |
|2023.07.27| Movie DTO ์์ฑ |
|2023.07.31| NetworkManager์ Decoder์ ์ญํ ๋ถ๋ฆฌ |
|2023.07.31| CollectionView, Cell ์์ฑ ๋ฐ ๋ ์ด์์ |
|2023.08.02| navi title, ์
์ธ์ฌ๋ฆฌ, separator ์์ฑ |
|2023.08.02| AttributedString์ ํตํด ๋ฐ์ดํฐ๋ณ String ๋ค๋ฅด๊ฒ ์์ฑ |
|2023.08.03| ๋ฐ์ดํฐ ๋ก๋ฉ ๊ฐ Indicator, RefreshControl ์ถ๊ฐ, ์๋ฌ ์ alert ์์ฑ |
|2023.08.08| FetchImage๊ตฌํ, Detail ํ๋ฉด ๊ตฌํ |
|2023.08.09| APIKey ์จ๊ธฐ๊ธฐ, Detail ๋ ์ด์์, Indicator ์์ฑ |
|2023.08.10| ์ํ ํฌ์คํฐ ์ด๋ฏธ์ง ๋์ด ์กฐ์ |
|2023.08.15| CalendarViewController ๊ตฌํ |
|2023.08.16| ์์ด์ฝ ๋ ์ด์์ ์ถ๊ฐ ๋ฐ Orientation, DynamicType ๋์ |
|2023.08.18| ๋ฆฌํฉํ ๋ง |
<br>
## ํ๋ก์ ํธ ๊ตฌ์กฐ
### ํด๋ ๊ตฌ์กฐ
โโโ Appication
โย ย โโโ AppDelegate.swift
โย ย โโโ SceneDelegate.swift
โโโ Controller
โย ย โโโ BoxOfficeCalendarViewController.swift
โย ย โโโ BoxOfficeMainViewController.swift
โย ย โโโ MovieDetailViewController.swift
โย ย โโโ Protocol
โย ย โโโ BoxOfficeCalendarViewControllerDelegate.swift
โโโ Model
โย ย โโโ DTO
โย ย โย ย โโโ BoxOffice
โย ย โย ย โย ย โโโ BoxOffice.swift
โย ย โย ย โย ย โโโ BoxOfficeItem.swift
โย ย โย ย โย ย โโโ BoxOfficeResult.swift
โย ย โย ย โโโ Image
โย ย โย ย โย ย โโโ Image.swift
โย ย โย ย โย ย โโโ ImageDocument.swift
โย ย โย ย โโโ Movie
โย ย โย ย โโโ Movie.swift
โย ย โย ย โโโ MovieInformation.swift
โย ย โย ย โโโ MovieResult.swift
โย ย โโโ Decoding
โย ย โย ย โโโ JSONDecodingManager.swift
โย ย โโโ Error
โย ย โย ย โโโ DecodingError.swift
โย ย โย ย โโโ NetworkError.swift
โย ย โโโ Extension
โย ย โย ย โโโ Array+.swift
โย ย โย ย โโโ Bundle+.swift
โย ย โย ย โโโ Date+.swift
โย ย โย ย โโโ String+.swift
โย ย โโโ Network
โย ย โย ย โโโ NetworkManager.swift
โย ย โโโ RankChangeState.swift
โโโ Resource
โย ย โโโ APIKey.plist
โย ย โโโ Assets.xcassets
โย ย โย ย โโโ Contents.json
โย ย โย ย โโโ boxOfficeTestSample.dataset
โย ย โย ย โโโ Contents.json
โย ย โย ย โโโ boxOfficeTestSample.json
โย ย โโโ Base.lproj
โย ย โย ย โโโ LaunchScreen.storyboard
โย ย โโโ Info.plist
โโโ View
โโโ BoxOfficeCollectionViewIconCell.swift
โโโ BoxOfficeCollectionViewListCell.swift
โโโ BoxOfficeMainView.swift
โโโ Extension
โย ย โโโ UICollectionView+.swift
โย ย โโโ UICollectionViewCell+.swift
โย ย โโโ UIFont+.swift
โย ย โโโ UILabel+.swift
โโโ MovieDetailView.swift
โโโ Protocol
โโโ Reusable.swift
<br>
### ๋ค์ด์ด๊ทธ๋จ
#### Controller

#### Model

<br>
## ์คํ ํ๋ฉด
|๋น๊ฒจ์ ์๋ก๊ณ ์นจ|๋คํธ์ํฌ ํต์ ์ค ๋ก๋ฉUI ํ์|
|:---:|:---:|
|||
|์ํ ์์ธ ์ ๋ณด ํ๋ฉด|๋ ์ง ์ ํ|
|:---:|:---:|
|||
|ํ๋ฉด ๋ชจ๋ ๋ณ๊ฒฝ|์์ด์ฝ ํ๋ฉด ํ์ |
|:---:|:---:|
|||
|์์ด์ฝ ํ๋ฉด(์ธ๋ก) ๋ค์ด๋๋ฏน ํ์
|์์ด์ฝ ํ๋ฉด(๊ฐ๋ก) ๋ค์ด๋๋ฏน ํ์
|
|:---:|:---:|
|||
<br>
## ํธ๋ฌ๋ธ์ํ
### 1๏ธโฃ Completion Handler์์ async/await์ผ๋ก
#### ๐ ๋ฌธ์ ์
URLSession.shared.dataTask() ๋ฉ์๋๋ฅผ ์ฌ์ฉํ๊ฒ ๋๋ฉด ๊ฒฐ๊ณผ๋ฅผ completion Handler๋ฅผ ํตํด ๊ฒฐ๊ณผ๋ฅผ ๋ฐ์ต๋๋ค. ํ์ง๋ง ์ ํฌ๋ ๊ฒฐ๊ณผ๋ก ๋ฐ์ ๋ฐ์ดํฐ๋ฅผ ๋ฆฌํดํ๊ณ ์ถ์์ต๋๋ค.
```swift
func getBoxOffice() -> BoxOffice {
let task = URLSession.shared.dataTask(from: url) { data, response, error in
//์๋ต...
return data //compile error
}
task.resume()
}
```
`return data`๋ dataTask๊ฐ ๋ฐ๋ ํด๋ก์ ์ return์ผ๋ก ๋ค์ด๊ฐ๊ธฐ ๋๋ฌธ์ ์ปดํ์ผ์๋ฌ๊ฐ ์๊น๋๋ค.
๋ฐ๋ผ์ data๋ฅผ ๋ฆฌํดํ๊ณ ์ถ์ผ๋ฉด ๋ completion Handler๋ฅผ ๋ฐ์ ์ ๋ฌํด์ผํฉ๋๋ค.
```swift
func getBoxOffice(completion: (BoxOffice) -> Void) {
let task = URLSession.shared.dataTask(from: url) { data, response, error in
completion(data)
}
task.resume()
}
```
๊ทธ๋ฌ๋ฉด ์ฌ์ฉํ๋ ๊ณณ์์ ๋ completion ์ ์ ๋ฌํด์ผํฉ๋๋ค.
```swift
getBoxOffice { boxOffice in
print(boxoffice)
}
```
ํ์ง๋ง ์ ํฌ๊ฐ ์ํ๋ ๋ฐฉ์์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
```swift
let boxOffice = getBoxOffice()
```
#### ๐ ํด๊ฒฐ ๋ฐฉ๋ฒ
์ํ๋ ๋ฐฉ์์ ๊ณ ๋ฏผํ๋ ์ค async await์ ์๊ฒ ๋์์ต๋๋ค.
async await์ ์ด์ ๊ฐ์ ๋ฌธ์ ๋ฟ๋ง ์๋๋ผ URLSession์ ์ฐ๋ ๋ ๋ฌธ์ , ๋ฒ๊ทธ๋ฐ์ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์์ต๋๋ค.
```swift
static func fetchData<T: Decodable>(fetchType: FetchType) async throws -> T {
guard let url = fetchType.url else {
throw NetworkError.invalidURL
}
guard let (data, response) = try? await URLSession.shared.data(from: url) else {
throw NetworkError.requestFailed
}
guard let httpResponse = response as? HTTPURLResponse else {
throw NetworkError.invalidHTTPResponse
}
guard (200..<300) ~= httpResponse.statusCode else {
throw NetworkError.badStatusCode(statusCode: httpResponse.statusCode)
}
return try decode(from: data)
}
```
async await ๋๋ถ์ ๋คํธ์ํฌ ํต์ ํจ์๋ฅผ ๊น๋ํ๊ฒ ์ ๋ฆฌํ ์ ์์๊ณ ์ํ๋ ๋ฐฉ์์ผ๋ก ๋ฆฌํด๊ฐ์ ๋ฐ์ ์ ์๊ฒ ๋์์ต๋๋ค.
### 2๏ธโฃ separator
#### ๐ ๋ฌธ์ ์
์๊ตฌ์ฌํญ ํ๋ฉด์ separator๊ฐ ์๋ ๊ฒ์ ํ์ธํ์ต๋๋ค.
collectionView๋ฅผ compositionalLayout์ผ๋ก ๊ตฌํํ๋๋ ์๋์ผ๋ก separator๊ฐ ๋ง๋ค์ด์ง์ง ์์์ต๋๋ค.
์ฒ์์๋ ์
์ border๋ฅผ ์ฃผ์์ต๋๋ค. ํ์ง๋ง border๋ฅผ ์ฃผ๋ ๋ฐฉ๋ฒ ๋ง๊ณ ๋ค๋ฅธ ๋ฐฉ๋ฒ์ด ์์ด์ ์ฐพ์๋ณด์์ต๋๋ค.
#### ๐ ํด๊ฒฐ ๋ฐฉ๋ฒ
compositionalLayout์ static method์ธ .list()๋ฅผ ์ฌ์ฉํ๋ฉด list configuration์ ์ฌ์ฉํ ์ ์๊ฒ ๋ฉ๋๋ค. ์ด๋ฅผ ์ด์ฉํด list ๋ชจ์ ํ์์ ๊ทธ๋๋ก collectionView์์๋ ์ฌ์ฉํ ์ ์์์ต๋๋ค.
```swift
let config = UICollectionLayoutListConfiguration(appearance: .plain)
let layout = UICollectionViewCompositionalLayout.list(using: config)
collectionView.collectionViewLayout = layout
```
ํ์ง๋ง ๋ฌธ์ ๋ compositionalLayout์ผ๋ก item๊ณผ section group์ ์ค์ ํ์ ๊ฒฝ์ฐ listConfiguration์ ์ฌ์ฉํ์ง ๋ชปํ๋ค๋ ํ๊ณ๊ฐ ์์ต๋๋ค. ์ด๋๋ cell์ border๋ฅผ ์ฃผ๋ ๊ฒ์ด ์ข์ ๋ฐฉ๋ฒ์ด๋ผ๊ณ ์๊ฐํฉ๋๋ค.
### 3๏ธโฃ separator ๊ธฐ๋ณธ ๊ณต๋ฐฑ
#### ๐ ๋ฌธ์ ์

๊ฐ cell์ separator๋ฅผ ์ค์ ํ์์ง๋ง ์์ ์ฌ์ง๊ณผ ๊ฐ์ด ์ฝ๊ฐ์ ๊ณต๋ฐฑ์ด ์๊ธฐ๋ ๋ฌธ์ ๊ฐ ์๊ฒผ์ต๋๋ค.
#### ๐ ํด๊ฒฐ ๋ฐฉ๋ฒ
๊ทธ ์ด์ ๋ ์ปจํ
์ธ ์ ๋์ ์๋์ผ๋ก ์ค ๋ง์ถค๋๊ธฐ ๋๋ฌธ์
๋๋ค.
์
์ ๋ง๋ค ๋ UICollectionViewCell์ด ์๋ UICollectionViewListCell์ ์์๋ฐ์ผ๋ฉด separator์ ๋ ์ด์์์ ์ก์ ์ ์๋ layoutGuide๋ฅผ ์ฌ์ฉํ ์ ์๊ฒ ๋ฉ๋๋ค. (disclosure accesary๋ ์ฌ์ฉ๊ฐ๋ฅ)
์ด ์์ฑ์ ๋ ์ด์์์ .list()๋ก ๋ง๋ ๋ ์ด์์์์๋ง ์ ์ฉ์ด ๊ฐ๋ฅํฉ๋๋ค.
```swift
separatorLayoutGuide.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
```
### 4๏ธโฃ API key ๊ตฌ๋ถ ๋ฐ ์๋
#### ๐ ๋ฌธ์ ์
๋คํธ์ํฌ ๋งค๋์ ์์ ๋ฐ์ค์คํผ์ค ์ ๋ณด๋ง ๋ถ๋ฌ์ฌ ๋๋ API key๋ฅผ URL์ ํฌํจํ์ฌ URL๋ง ์ฌ์ฉํ์ต๋๋ค. ํ์ง๋ง ์ํ ํฌ์คํฐ ์ด๋ฏธ์ง๋ฅผ ๋ถ๋ฌ์ค๊ธฐ ์ํด ํ์ํ URL ํ์์ด ๋ค๋ฅด๊ธฐ ๋๋ฌธ์ API๋ณ๋ก ๋ถ๋ฆฌ๊ฐ ํ์ํ์ต๋๋ค.
| ์ํ์งํฅ์์ํ | ๋ค์ ์ด๋ฏธ์ง ๊ฒ์ |
|:-:|:-:|
|API key๊ฐ URL ์ฟผ๋ฆฌ์ ํฌํจ|API key๊ฐ ํค๋์ ํฌํจ|
#### ๐ ํด๊ฒฐ ๋ฐฉ๋ฒ
URLRequest๋ฅผ ๋ง๋๋ ๋ฉ์๋๋ฅผ ๋ถ๋ฆฌํ๋ฉฐ URLQueryItem๊ณผ ํค๋๋ฅผ ๋ง๋ค๊ฒ ๋์์ต๋๋ค.
```swift
static private func createRequest(fetchType: FetchType) -> URLRequest? {
guard var urlComponents = URLComponents(string: fetchType.url) else {
return nil
}
switch fetchType {
case .boxOffice(let date):
urlComponents.queryItems = [
URLQueryItem(name: "key", value: Bundle.main.kobisAPIKey),
URLQueryItem(name: "targetDt", value: date)
]
guard let url = urlComponents.url else {
return nil
}
return URLRequest(url: url)
...
}
}
```
์์ธ๋ฌ API key๋ฅผ ์จ๊ธฐ๊ธฐ ์ํด plist์ ํค๋ฅผ ์ ์ฅํ๊ณ gitignore๋ฅผ ํ์ฉํ๋ ๋ฐฉ๋ฒ์ ์ฐพ์์ต๋๋ค.
```swift
extension Bundle {
var kakaoAPIKey: String {
return fetchPropertyList(domain: "KAKAO")
}
var kobisAPIKey: String {
return fetchPropertyList(domain: "KOBIS")
}
private func fetchPropertyList(domain: String) -> String {
guard let file = self.path(forResource: "APIKey", ofType: "plist") else { return "" }
guard let resource = NSDictionary(contentsOfFile: file) else { return "" }
guard let key = resource[domain] as? String else { fatalError("APIKey.plist์ \(domain) APIํค๋ฅผ ๋ฑ๋กํ์ธ์")}
return key
}
}
```
### 5๏ธโฃ ์ด๋ฏธ์ง์ ๋ถํ์ํ ๊ณต๋ฐฑ
#### ๐ ๋ฌธ์ ์
์ด๋ฏธ์ง๋ฅผ ๋ถ๋ฌ์์ ๋ ์ด๋ฏธ์ง ํฌ๊ธฐ์ ๋ค๋ฅด๊ฒ ๊ณต๋ฐฑ์ด ์๊ธฐ๋ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค.
<img src="https://hackmd.io/_uploads/ByRPbKgnn.png" width=300>
#### ๐ ํด๊ฒฐ ๋ฐฉ๋ฒ
์ด๋ฏธ์ง ๋ทฐ์ ๊ฐ๋ก์ ์๋ณธ ์ด๋ฏธ์ง์ ๊ฐ๋ก๋ฅผ ํตํด ๋น์จ์ ๊ตฌํ ํ, ์ด ๋น์จ์ ํตํด ๋์ด๋ฅผ ๊ตฌํ์ฌ ์ด๋ฏธ์ง ๋ทฐ์ ๋์ด๋ฅผ ๊ณ ์ ํ์ต๋๋ค.
```swift
func injectMovieInformation(_ movieInformation: MovieInformation?, image: UIImage?) {
posterImageView.image = image
updatePosterImageViewConstraints()
...
}
private func updatePosterImageViewConstraints() {
guard let imageWidth = posterImageView.image?.size.width,
let imageHeight = posterImageView.image?.size.height else { return }
let ratio = posterImageView.frame.width / imageWidth
let height = ratio * imageHeight
posterImageView.heightAnchor.constraint(equalToConstant: height).isActive = true
}
```
### 6๏ธโฃ async let์ ์ด์ฉํ ๋น๋๊ธฐ ์์
#### ๐ ๋ฌธ์ ์
ํ์ฌ ์ฝ๋์์ ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค๋ ๋ฐฉ์์ผ๋ก async/await์ ์ฌ์ฉ์ค์
๋๋ค.
```swift
let movie = await fetchMovie()
let poster = await fetchPoster()
detailView.injectMovieInformation(movie, poster)
```
ํด๋น ์ฝ๋๋ ์์ฐจ์ ์ผ๋ก ์คํ๋๊ธฐ ๋๋ฌธ์ movie์ ๊ฐ์ ๋ฐ์ ๋ ๊น์ง ๊ธฐ๋ค๋ฆฐ ํ movie ๋ฐ์ดํฐ๋ฅผ ๋ค ๋ฐ์ผ๋ฉด poster๋ฅผ ๋ฐ๊ธฐ ์์ํฉ๋๋ค.
๋ ๊ฐ๋ ์๋ก ์ ํ ๊ด๊ณ๊ฐ ์๊ณ ์ค๋ ๊ฑธ๋ฆฌ๋ ์์
์ด๊ธฐ ๋๋ฌธ์ ์์ฐจ์ ์ผ๋ก ์คํํ๋ฉฐ ๊ธฐ๋ค๋ฆฌ๋ ๊ฒ์ ์ํด๋ผ๊ณ ํ๋จํ์์ต๋๋ค.
#### ๐ ํด๊ฒฐ ๋ฐฉ๋ฒ
async let์ ์ด์ฉํ์ฌ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ์์ต๋๋ค.
```swift
async let movie = fetchMovie()
async let poster = fetchPoster()
detailView.injectMovieInformation(await movie, await poster)
```
async let์ ์ฌ์ฉํด์ ๋น๋๊ธฐ์ ์ผ๋ก movie์ poster๋ฅผ fetchํ ์ ์๋ค๋ ์ฌ์ค์ ๋ฐฐ์ ์ต๋๋ค.
<br>
## ์ฐธ๊ณ ๋งํฌ
[๐Apple Docs: UICollectionView](https://developer.apple.com/documentation/uikit/uicollectionview)
[๐Apple Docs: URLSession](https://developer.apple.com/documentation/foundation/urlsession)
[๐Apple Docs: Fetching Website Data into Memory](https://developer.apple.com/documentation/foundation/url_loading_system/fetching_website_data_into_memory)
[๐Apple Docs: UIRefreshControl](https://developer.apple.com/documentation/uikit/uirefreshcontrol)
[๐Apple Docs: UIActivityIndicatorView](https://developer.apple.com/documentation/uikit/uiactivityindicatorview)
[๐Apple Docs: UICalendarView](https://developer.apple.com/documentation/uikit/uicalendarview)
[๐ผApple WWDC: Meet async/await in Swift](https://developer.apple.com/videos/play/wwdc2021/10132/)
[๐ผApple WWDC: Use async/await with URLSession](https://developer.apple.com/videos/play/wwdc2021/10095/)
[๐blog: [Swift] Actor ๋ฟ์๊ธฐ](https://sujinnaljin.medium.com/swift-actor-%EB%BF%8C%EC%8B%9C%EA%B8%B0-249aee2b732d)
[๐์ผ๊ณฐ๋ท๋ท: Swift Concurrency Programming](https://yagom.net/courses/swift-concurrency-programming/)
<!-- [๐Apple Archive: ]() -->
<!-- [๐Swift forums: ]()
[๐stackOverflow: ]() -->
<br>
[step5]

