# ๐ŸŽฌ๐Ÿฟ๋ฐ•์Šค์˜คํ”ผ์Šค๐Ÿฟ๐ŸŽฌ ## ๐Ÿ“– ๋ชฉ์ฐจ 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 ![](https://hackmd.io/_uploads/B1mJi8hhn.png) #### Model ![](https://hackmd.io/_uploads/BkIk2Un33.png) <br> ## ์‹คํ–‰ ํ™”๋ฉด |๋‹น๊ฒจ์„œ ์ƒˆ๋กœ๊ณ ์นจ|๋„คํŠธ์›Œํฌ ํ†ต์‹  ์ค‘ ๋กœ๋”ฉUI ํ‘œ์‹œ| |:---:|:---:| |![](https://s3.ap-northeast-2.amazonaws.com/media.yagom-academy.kr/resources/usr/6434a442324ce85b6f359dee/20230818/64dede8145e6bf50b2f4344e.gif)|![](https://s3.ap-northeast-2.amazonaws.com/media.yagom-academy.kr/resources/usr/6434a442324ce85b6f359dee/20230818/64dede7d45e6bf50b2f4344c.gif)| |์˜ํ™” ์ƒ์„ธ ์ •๋ณด ํ™”๋ฉด|๋‚ ์งœ ์„ ํƒ| |:---:|:---:| |![](https://s3.ap-northeast-2.amazonaws.com/media.yagom-academy.kr/resources/usr/6434a442324ce85b6f359dee/20230818/64dede8445e6bf50b2f43450.gif)|![](https://s3.ap-northeast-2.amazonaws.com/media.yagom-academy.kr/resources/usr/6434a442324ce85b6f359dee/20230818/64dede6245e6bf50b2f4344a.gif)| |ํ™”๋ฉด ๋ชจ๋“œ ๋ณ€๊ฒฝ|์•„์ด์ฝ˜ ํ™”๋ฉด ํšŒ์ „| |:---:|:---:| |![](https://s3.ap-northeast-2.amazonaws.com/media.yagom-academy.kr/resources/usr/6434a442324ce85b6f359dee/20230818/64dede9245e6bf50b2f43458.gif)|![](https://s3.ap-northeast-2.amazonaws.com/media.yagom-academy.kr/resources/usr/6434a442324ce85b6f359dee/20230818/64dede8f45e6bf50b2f43456.gif)| |์•„์ด์ฝ˜ ํ™”๋ฉด(์„ธ๋กœ) ๋‹ค์ด๋‚˜๋ฏน ํƒ€์ž…|์•„์ด์ฝ˜ ํ™”๋ฉด(๊ฐ€๋กœ) ๋‹ค์ด๋‚˜๋ฏน ํƒ€์ž…| |:---:|:---:| |![](https://s3.ap-northeast-2.amazonaws.com/media.yagom-academy.kr/resources/usr/6434a442324ce85b6f359dee/20230818/64dede8b45e6bf50b2f43454.gif)|![](https://s3.ap-northeast-2.amazonaws.com/media.yagom-academy.kr/resources/usr/6434a442324ce85b6f359dee/20230818/64dede8845e6bf50b2f43452.gif)| <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 ๊ธฐ๋ณธ ๊ณต๋ฐฑ #### ๐Ÿ”’ ๋ฌธ์ œ์  ![](https://hackmd.io/_uploads/H1g1qfYo3.png) ๊ฐ 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] ![](https://hackmd.io/_uploads/B1gWJDun3.png) ![](https://hackmd.io/_uploads/B1UbHDu2h.png)