Skip to content

Latest commit

ย 

History

History
618 lines (508 loc) ยท 31.3 KB

README.md

File metadata and controls

618 lines (508 loc) ยท 31.3 KB

๐ŸŽฅ ๋ฐ•์Šค์˜คํ”ผ์Šค๐Ÿฟ

๐Ÿ“– ๋ชฉ์ฐจ

  1. ์†Œ๊ฐœ
  2. ํŒ€์›
  3. ํƒ€์ž„๋ผ์ธ ๋ฐ ํ•ต์‹ฌ๊ฒฝํ—˜
  4. ํŒŒ์ผํŠธ๋ฆฌ
  5. ์‹คํ–‰ ํ™”๋ฉด
  6. ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…
  7. ์ฃผ์š” ํ•™์Šต ๋‚ด์šฉ
  8. ํŒ€ ํšŒ๊ณ 
  9. ์ฐธ๊ณ  ๋งํฌ

1. ๐Ÿ“ข ์†Œ๊ฐœ

์ผ์ผ ๋ฐ•์Šค์˜คํ”ผ์Šค๊ฐ€ ๊ถ๊ธˆํ•˜์‹ ๊ฐ€์š”? ํ˜น์€ ์˜ํ™” ๊ฐœ๋ณ„ ์ƒ์„ธ ์กฐํšŒ๋ฅผ ์›ํ•˜์‹œ๋‚˜์š”? ์ €ํฌ์—๊ฒŒ ๋ฌผ์–ด๋ณด์„ธ์š”!

โœ”๏ธ ์บ˜๋ฆฐ๋”์—์„œ ์›ํ•˜์‹œ๋Š” ๋‚ ์งœ๋ฅผ ์„ ํƒํ•ด์ฃผ์„ธ์š” ๐Ÿ“…
โœ”๏ธ ํ•ด๋‹น ๋‚ ์งœ์˜ 1๏ธโƒฃ~๐Ÿ”Ÿ์œ„ ๋ฐ•์Šค์˜คํ”ผ์Šค๋ฅผ ์ œ๊ณตํ•ด๋“œ๋ฆฝ๋‹ˆ๋‹ค!
โœ”๏ธ ์ƒˆ๋กœ๊ณ ์นจ์„ ์›ํ•˜์‹œ๋ฉด ๋ฆฌ์ŠคํŠธ๋ฅผ ์•„๋ž˜๋กœ ์žก์•„ ๋Œ์–ด์ฃผ์„ธ์š”!
โœ”๏ธ ์˜ํ™” ๋ณ„ ์ƒ์„ธ์ •๋ณด๋„ ํ™•์ธ ๊ฐ€๋Šฅํ•˜๋‹ˆ ๋†“์น˜์ง€ ๋งˆ์‹œ๊ณ  ํ™•์ธํ•ด ๋ณด์„ธ์š”๐Ÿ˜†

ํ•ต์‹ฌ ๊ฐœ๋… ์˜คํ”ˆ API / URLSession / JSON Decoding / CodingKeys / UNIT Test /
CollectionView / ModernCollectionView / UIActivityIndicatorView /
UIRefreshControl / NSMutableAttributedString /
API KEY ๋ฐœ๊ธ‰ ๋ฐ ๋…ธ์ถœ ๋ฐฉ์ง€ / Image Fetch / NSCache /
Modal / UICalendarView / DateManager / Image Loading View


2. ๐Ÿ‘ค ํŒ€์›

Serena ๐Ÿท BMO ๐Ÿค–

3. โฑ๏ธ ํƒ€์ž„๋ผ์ธ ๋ฐ ํ•ต์‹ฌ๊ฒฝํ—˜

ํ”„๋กœ์ ํŠธ ๊ธฐ๊ฐ„ : 2023-07-24 ~ 2023-08-18

ํƒ€์ž„๋ผ์ธ

๋‚ ์งœ ๋‚ด์šฉ
2023.07.24 โ—ฝ๏ธ ์ผ๋ณ„ ๋ฐ•์Šค ์˜คํ”ผ์Šค ์ƒ˜ํ”Œ json dataset ์ถ”๊ฐ€
โ—ฝ๏ธ ์ผ๋ณ„ ๋ฐ•์Šค์˜คํ”ผ์Šค Model ์ถ”๊ฐ€
2023.07.25 โ—ฝ๏ธ ์ผ๋ณ„ ๋ฐ•์Šค์˜คํ”ผ์Šค ๊ด€๋ จ ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€ ๋ฐ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ
โ—ฝ๏ธ ๋ฐ•์Šค์˜คํ”ผ์Šค Model ์ถ”๊ฐ€
โ—ฝ๏ธ ์ „์ฒด Model CodingKey ์ ์šฉ
2023.07.27 โ—ฝ๏ธ ๋„คํŠธ์›Œํฌ ๊ด€๋ จ ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•˜๋Š” NetworkManager ํƒ€์ž… ์ถ”๊ฐ€
โ—ฝ๏ธ ์˜ํ™”์ง„ํฅ์œ„์›ํšŒ๋กœ๋ถ€ํ„ฐ ์ผ๋ณ„ ๋ฐ•์Šค์˜คํ”ผ์Šค ์กฐํšŒํ•˜๋Š” ๋กœ์ง ์ž‘์„ฑ
2023.07.28 โ—ฝ๏ธ ์˜ํ™” ์ƒ์„ธ์ •๋ณด ์กฐํšŒ๋ฅผ ์œ„ํ•œ DTO ์ƒ์„ฑ ๋ฐ CodingKey ์ ์šฉ
โ—ฝ๏ธ ์˜ํ™” ์ƒ์„ธ์ •๋ณด ์กฐํšŒ ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€
โ—ฝ๏ธ ๋งค์ง๋ฆฌํ„ฐ๋Ÿด ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ NameSpace ์ถ”๊ฐ€
2023.07.31 โ—ฝ๏ธ Result ํƒ€์ž…์„ ํ™œ์šฉํ•˜์—ฌ Model ๋ฐ”์ธ๋”ฉ ๋ฐ ์—๋Ÿฌ์ฒ˜๋ฆฌ
2023.08.02 โ—ฝ๏ธ ์Šคํ† ๋ฆฌ๋ณด๋“œ ์ œ๊ฑฐ ๋ฐ ์ฝ”๋“œ๋ฒ ์ด์Šค UI ๊ตฌํ˜„
โ—ฝ๏ธ ์ผ๋ฒฝ ๋ฐ•์Šค์˜คํ”ผ์Šค Cell ์ƒ์„ฑ
โ—ฝ๏ธ ์ผ๋ณ„ ๋ฐ•์Šค์˜คํ”ผ์Šค CollectionView๋กœ ๊ตฌํ˜„
2023.08.03 โ—ฝ๏ธ CollectionView ์˜ DataSoruce๋ฅผ DiffableDataSource๋กœ ๋ณ€๊ฒฝ
โ—ฝ๏ธ CollectionView์— refresh control ์ถ”๊ฐ€
2023.08.04 โ—ฝ๏ธ ์ ‘๊ทผ์„ฑ ํ–ฅ์ƒ์„ ์œ„ํ•ด adjustsFontSize ์ ์šฉ
โ—ฝ๏ธ ์—๋Ÿฌ ๋ฐœ์ƒ์‹œ Alert ์ถœ๋ ฅ
2023.08.06 โ—ฝ๏ธ BoxOfficeService๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ณณ์€ ๋ชจ๋‘ ์˜์กด์„ฑ ์ฃผ์ž…์„ ๋ฐ›์•„ ์‚ฌ์šฉํ•˜๋„๋ก ๋ณ€๊ฒฝ
โ—ฝ๏ธ ๊ณตํ†ต Alert ์ค‘๋ณต ๋ฉ”์„œ๋“œ ๋ถ„๋ฆฌ
โ—ฝ๏ธ ์˜ํ™” ์ƒ์„ธ์ •๋ณด ViewController ์ƒ์„ฑ ๋ฐ ๊ตฌํ˜„
โ—ฝ๏ธ ๋‹ค์Œ ์ด๋ฏธ์ง€ ๊ฒ€์ƒ‰ ๊ด€๋ จ DTO ์ถ”๊ฐ€
2023.08.07 โ—ฝ๏ธ NetworkManager ๋กœ์ง ๋ณ€๊ฒฝ ๋ฐ ์‹ฑ๊ธ€ํ†ค ํด๋ž˜์Šค๋กœ ๋ณ€๊ฒฝ
โ—ฝ๏ธ Dynamic Type ์ ์šฉ
2023.08.08 โ—ฝ๏ธ Kakao Developer์— ํŒ€ ์•ฑ ์ƒ์„ฑ
โ—ฝ๏ธ KakaoAPIKey๋ฅผ plist์— ๋“ฑ๋ก
2023.08.09 โ—ฝ๏ธ ์ด๋ฏธ์ง€ ๋กœ๋“œ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ƒ์„ฑ
โ—ฝ๏ธ ์ด๋ฏธ์ง€ ์บ์‹œ ์ €์žฅ ๋กœ์ง ์ถ”๊ฐ€
2023.08.10 โ—ฝ๏ธ ๋ฐ•์Šค์˜คํ”ผ์Šค ๋‚ ์งœ ์„ ํƒ ViewController ์ถ”๊ฐ€
โ—ฝ๏ธ BoxOfficeService์˜ ๋‚ ์งœ ๋กœ์ง์„ DateManager ์‹ฑ๊ธ€ํ†ค ํด๋ž˜์Šค๋กœ ๋ถ„๋ฆฌ

ํ•ต์‹ฌ๊ฒฝํ—˜

  • ์˜ํ™”์ง„ํฅ์œ„์œ„์›ํšŒ ์˜คํ”ˆ API๋ฅผ ์ฐธ๊ณ ํ•˜์—ฌ '์˜ค๋Š˜์˜ ์ผ์ผ ๋ฐ•์Šค์˜คํ”ผ์Šค ๋ฐ์ดํ„ฐ'์™€ '์˜ํ™” ๊ฐœ๋ณ„ ์ƒ์„ธ ๋ฐ์ดํ„ฐ' Model์„ ๊ตฌํ˜„
  • Model์„ ํ™œ์šฉํ•˜์—ฌ URLSession์œผ๋กœ JSON ํŒŒ์ผ์„ Fetch
  • JSON ํŒŒ์ผ Decode์— ๋Œ€ํ•œ Unit Test ์ž‘์„ฑ
  • iOS 14.0 ๋ฏธ๋งŒ ๋ฒ„์ „์„ ์œ„ํ•œ CollecionView / iOS 14.0 ์ด์ƒ ๋ฒ„์ „์„ ์œ„ํ•œ ModernCollecionView ๊ตฌ์„ฑ
  • Kakao API Key๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์˜ํ™” ํฌ์Šคํ„ฐ fetchํ•˜๊ธฐ
  • fetchํ•œ ์ด๋ฏธ์ง€ ๋ฐ ๋ฐ์ดํ„ฐ๋ฅผ StackView์™€ ScrollView์— ๋„ฃ๊ธฐ

4. ๐ŸŒฒ ํŒŒ์ผํŠธ๋ฆฌ

ํŒŒ์ผํŠธ๋ฆฌ

BoxOffice 
โ”œโ”€โ”€ App
โ”‚ย ย  โ”œโ”€โ”€ AppDelegate.swift
โ”‚ย ย  โ””โ”€โ”€ SceneDelegate.swift
โ”œโ”€โ”€ Base.lproj
โ”‚ย ย  โ””โ”€โ”€ LaunchScreen.storyboard
โ”œโ”€โ”€ Error
โ”‚ย ย  โ”œโ”€โ”€ AlertManager.swift
โ”‚ย ย  โ”œโ”€โ”€ JSONDecoderError.swift
โ”‚ย ย  โ””โ”€โ”€ NetworkManagerError.swift
โ”œโ”€โ”€ Extension
โ”‚ย ย  โ”œโ”€โ”€ Bundle+.swift
โ”‚ย ย  โ”œโ”€โ”€ JSONDecoder+.swift
โ”‚ย ย  โ”œโ”€โ”€ String+.swift
โ”‚ย ย  โ””โ”€โ”€ UIFont+.swift
โ”œโ”€โ”€ Info.plist
โ”œโ”€โ”€ KakaoAPIKey.plist
โ”œโ”€โ”€ Model
โ”‚ย ย  โ”œโ”€โ”€ DTO
โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ BoxOffice
โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ BoxOffice.swift
โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ BoxOfficeResult.swift
โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ DailyBoxOffice.swift
โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ DaumSearch
โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ DaumSearchMainText.swift
โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ DaumSearchMeta.swift
โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ ImageDocument.swift
โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ Movie
โ”‚ย ย  โ”‚ย ย      โ”œโ”€โ”€ Audit.swift
โ”‚ย ย  โ”‚ย ย      โ”œโ”€โ”€ Company.swift
โ”‚ย ย  โ”‚ย ย      โ”œโ”€โ”€ Genre.swift
โ”‚ย ย  โ”‚ย ย      โ”œโ”€โ”€ Movie.swift
โ”‚ย ย  โ”‚ย ย      โ”œโ”€โ”€ MovieInfo.swift
โ”‚ย ย  โ”‚ย ย      โ”œโ”€โ”€ MovieInfoResult.swift
โ”‚ย ย  โ”‚ย ย      โ”œโ”€โ”€ Nation.swift
โ”‚ย ย  โ”‚ย ย      โ”œโ”€โ”€ People.swift
โ”‚ย ย  โ”‚ย ย      โ””โ”€โ”€ ShowType.swift
โ”‚ย ย  โ””โ”€โ”€ Section.swift
โ”œโ”€โ”€ NameSpace
โ”‚ย ย  โ”œโ”€โ”€ CustomDateFormatStyle.swift
โ”‚ย ย  โ”œโ”€โ”€ KakaoNameSpace.swift
โ”‚ย ย  โ”œโ”€โ”€ KobisNameSpace.swift
โ”‚ย ย  โ”œโ”€โ”€ MimeType.swift
โ”‚ย ย  โ””โ”€โ”€ MovieDetailNameSpace.swift
โ”œโ”€โ”€ Protocol
โ”‚ย ย  โ”œโ”€โ”€ CalendarViewControllerDelegate.swift
โ”‚ย ย  โ””โ”€โ”€ DaumSearchDocumentable.swift
โ”œโ”€โ”€ Service
โ”‚ย ย  โ””โ”€โ”€ BoxOfficeService.swift
โ”œโ”€โ”€ Util
โ”‚ย ย  โ”œโ”€โ”€ DateManager.swift
โ”‚ย ย  โ”œโ”€โ”€ ImageCacheManager.swift
โ”‚ย ย  โ””โ”€โ”€ NetworkManager.swift
โ”œโ”€โ”€ View
โ”‚ย ย  โ”œโ”€โ”€ BoxOfficeCell.swift
โ”‚ย ย  โ”œโ”€โ”€ Custom
โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ DetailLabel.swift
โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ LabelsStack.swift
โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ TitleLabel.swift
โ”‚ย ย  โ””โ”€โ”€ MovieDetailView.swift
โ””โ”€โ”€ ViewController
    โ”œโ”€โ”€ BoxOfficeViewController.swift
    โ”œโ”€โ”€ CalendarViewController.swift
    โ””โ”€โ”€ MovieDetailViewController.swift

5. ๐Ÿ“ฒ ์‹คํ–‰ ํ™”๋ฉด

๋ฐ•์Šค์˜คํ”ผ์Šค ๋กœ๋”ฉ ํ™”๋ฉด ๋ฐ•์Šค์˜คํ”ผ์Šค ๋ฆฌ์ŠคํŠธ ์ƒˆ๋กœ๊ณ ์นจ
์ด๋ฏธ์ง€ ๋กœ๋”ฉ ํ™”๋ฉด ์บ˜๋ฆฐ๋”๋กœ ๋‚ ์งœ ์„ ํƒํ•˜๊ธฐ

6. ๐Ÿ›Ž๏ธ ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…

CodingKeys

๐Ÿ”ฅ ๋ฌธ์ œ์ 

  • ์ดˆ๋ฐ˜ ์ฝ”๋“œ ์ž‘์„ฑ ์‹œ decode ๊ณผ์ •์—์„œ ์•Œ ์ˆ˜ ์—†๋Š” ์—๋Ÿฌ๊ฐ€ ๊ณ„์† ๋ฐœ์ƒํ•˜์˜€์Šต๋‹ˆ๋‹ค. decode ๊ณผ์ •์—์„œ ์–ด๋Š ๋ถ€๋ถ„์—์„œ ์ž˜๋ชป๋˜์—ˆ๋Š”์ง€๋ฅผ ์ฐพ๊ธฐ ์œ„ํ•ด ์ฝ”๋“œ๋ฅผ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋‹ค์‹œ ์ž‘์„ฑํ•˜๋‹ค๋ณด๋‹ˆ, CodingKey๋ฅผ ์ž˜๋ชป ์ž‘์„ฑํ•˜์—ฌ JSON decode๊ฐ€ ๋˜์ง€ ์•Š์•˜๋‹ค๋Š” ๊ฒƒ์„ ๋ฐœ๊ฒฌํ–ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿงฏ ํ•ด๊ฒฐ๋ฐฉ๋ฒ•

  • CodingKey๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ ํ”„๋กœํผํ‹ฐ๋ช…์„ swift์—์„œ ์‚ฌ์šฉํ•  ์ด๋ฆ„์œผ๋กœ ์ง€์ •ํ•˜๊ณ , enum case์˜ ๊ฐ’์œผ๋กœ ๊ธฐ์กด์˜ JSON ํ‚ค ๊ฐ’์˜ ์ด๋ฆ„์„ ์ง€์ •ํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด๋ฅผ ๋ฐ˜๋Œ€๋กœ ์ž‘์„ฑํ•˜์˜€๋”๋‹ˆ, ์•Œ ์ˆ˜ ์—†๋Š” ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜์—ฌ ์ด๋ฅผ ๋””๋ฒ„๊น…ํ•˜๋Š”๋ฐ ๋งŽ์€ ์‹œ๊ฐ„์„ ์†Œ์š”ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

    CodingKey ์ž˜๋ชป ์ž‘์„ฑ ์˜ˆ์‹œ

    struct BoxOfficeResult: Decodable {
        let boxofficeType: String
        ...
    
        enum CodingKeys: String, CodingKey {
            case boxofficeType = "boxOfficeType"
            ...
        }
    }

    CodingKey ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ์‹œ

    struct BoxOfficeResult: Decodable {
        let boxOfficeType: String
        ...
    
        private enum CodingKeys: String, CodingKey {
            case boxOfficeType = "boxofficeType"
            ...
        }
    }

UnitTest์—์„œ do-catch๋ฌธ ์‚ฌ์šฉ ์‹œ XCTFail ํ™œ์šฉ

๐Ÿ”ฅ ๋ฌธ์ œ์ 

  • Unit Test์—์„œ JSON ํŒŒ์ผ Decode์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ ์‹œ do-catch๋ฌธ์„ ํ™œ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค. ์ด๋•Œ ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํŒจํ–ˆ์„ ๋•Œ XCTTest ๋ฉ”์„œ๋“œ๋ฅผ retrun๋งŒ ํ•˜๊ฒŒ ๋˜๋ฉด ํ…Œ์ŠคํŠธ๊ฐ€ Success์ฒ˜๋ฆฌ ๋˜๋Š”๊ฒƒ์„ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค.
  • ํ…Œ์ŠคํŠธ๊ฐ€ ๊ผญ Then๊ณผ์ •๊นŒ์ง€ ๋„๋‹ฌํ•˜์ง€ ์•Š๋”๋ผ๋„ Given๊ณผ When๊ณผ์ • ๋˜ํ•œ ํ…Œ์ŠคํŠธ์— ์ ์ ˆํ•˜์ง€ ์•Š์€ ๊ฐ’์ด ๋“ค์–ด์˜ค๊ฑฐ๋‚˜, ๊ฐ’์ด ์ฒ˜๋ฆฌ๋˜๋Š” ๊ณผ์ •์— ๋Œ€ํ•œ ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿงฏ ํ•ด๊ฒฐ๋ฐฉ๋ฒ•

  • XCTFail ๋ฉ”์„œ๋“œ๋ฅผ ์ฐพ์•„ ํ…Œ์ŠคํŠธ ์ง„ํ–‰ ์ค‘ ์ ์ ˆํ•˜์ง€ ์•Š์€ ๋ถ€๋ถ„์— ์‚ฝ์ž…ํ•ด ์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค. Apple Developer ๊ณต์‹๋ฌธ์„œ์— ๋”ฐ๋ฅด๋ฉด XCTFail์˜ ์„ค๋ช…์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

    This function generates a failure immediately and unconditionally. (์ด ํ•จ์ˆ˜๋Š” ์ฆ‰์‹œ ๋ฌด์กฐ๊ฑด ์‹คํŒจ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.)

    func test_box_office_sample_json_ํŒŒ์ผ์„_๋””์ฝ”๋”ฉ_ํ• _์ˆ˜_์žˆ๋‹ค() {
        // Given
        guard let result: BoxOffice = JSONDecoder.decode(fileName: "box_office_sample") else {
            XCTFail("ํŒŒ์ผ๋ช… 'box_office_sample'๋กœ JSON ๋””์ฝ”๋”ฉ ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
            return
        }
    
        // When
    
        // Then
        XCTAssertNotNil(result)
    }

Result ํƒ€์ž…์„ ํ™œ์šฉํ•˜์—ฌ URLSession dataTask์—์„œ ๋ฐœ์ƒํ•œ ์—๋Ÿฌ ์ฒ˜๋ฆฌ

๐Ÿ”ฅ ๋ฌธ์ œ์ 

  • ViewController์—์„œ ์š”์ฒญํ•œ dataTask์—์„œ ์ž‘์—… ๋„์ค‘ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ๊ฒฝ์šฐ, ๋ฐœ์ƒํ•œ ์—๋Ÿฌ๋ฅผ ViewController์—์„œ ์ „๋‹ฌ๋ฐ›์•„ ์ฒ˜๋ฆฌํ•˜๊ณ  ์‹ถ์—ˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ dataTask ํด๋กœ์ € ๋‚ด๋ถ€์—์„œ ๋ฐ–์œผ๋กœ ๊ฐ’์„ ๋ฆฌํ„ด์‹œํ‚ฌ์ˆ˜ ์—†๋Š”๊ฒƒ์ฒ˜๋Ÿผ, ์—๋Ÿฌ๋„ ๋ฐ–์œผ๋กœ ๋˜์งˆ ์ˆ˜ ์—†์—ˆ์Šต๋‹ˆ๋‹ค.

    Invalid conversion from throwing function of type '@sendable (Data?, URLResponse?, (any Error)?) throws -> Void' to non-throwing function type '@sendable (Data?, URLResponse?, (any Error)?) -> Void'

๐Ÿงฏ ํ•ด๊ฒฐ๋ฐฉ๋ฒ•

  • Return ๊ฐ’์ด ์—†๋Š” ๊ฒฝ์šฐ๋‚˜ Error throws๋ฅผ ํ•  ์ˆ˜ ์—†๋Š” ์ƒํ™ฉ์—์„œ Result ํƒ€์ž…์„ ํ™œ์šฉํ•˜์—ฌ Error Handling์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํŠนํžˆ URLSession์˜ dataTask์ฒ˜๋Ÿผ Void ํƒ€์ž…์œผ๋กœ ๊ธฐ๋ณธ ๊ตฌํ˜„๋˜์–ด ์žˆ๋Š” ๋ฉ”์†Œ๋“œ์•ˆ์—์„œ ๋ฐœ์ƒํ•œ Error๋ฅผ ์™ธ๋ถ€๋กœ ์ „๋‹ฌํ•˜๊ณ  ์‹ถ์„ ๋•Œ ์œ ์šฉํ•˜๊ฒŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

    ์ฝ”๋“œ ์˜ˆ์‹œ

    1. Error ํƒ€์ž…์„ ์ƒ์„ฑ
    enum NetworkManagerError: Error {
        case notExistedUrl
        ...
    }
    1. Result Type์„ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ฐ›๋Š” ํ•จ์ˆ˜ ์ •์˜ (Success: Generic, Failure: NetworkManagerError)
    struct NetworkManager {
    // completion Handler ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ Result Type์„ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ฐ›๋Š” Void ํด๋กœ์ €
    static func loadData<T: Decodable>(_ components: URLComponents?,_ dataType: T.Type,_ completion: @escaping (Result<T, NetworkManagerError>) -> Void) {
        ...
                // Void ํƒ€์ž…์œผ๋กœ ๊ธฐ๋ณธ ๊ตฌํ˜„๋˜์–ด ์žˆ๋Š” ๋ฉ”์†Œ๋“œ(dataTask)์•ˆ์—์„œ Error ๋ฐœ์ƒ
                do {
                    ...
                    // ์„ฑ๊ณตํ•œ ๊ฒฝ์šฐ
                    completion(.success(result))
                } catch let error as JSONDecoderError {
                    ...
                    // ์‹คํŒจํ•œ ๊ฒฝ์šฐ
                    completion(.failure(NetworkManagerError.failureJsonDecode))
                } catch {
                    ...
                    completion(.failure(NetworkManagerError.unknown))
                }
            ...
    }
    
    1. Generic ํƒ€์ž…์œผ๋กœ ์ „๋‹ฌ๋ฐ›์€ Success ํƒ€์ž…์„ ๊ตฌ์ฒด์  ํƒ€์ž…์œผ๋กœ ๋‹ค์‹œ ์ „๋‹ฌ
    func loadDailyBoxOfficeData(_ completion: @escaping (Result<BoxOffice, NetworkManagerError>) -> Void) {
        ...
        NetworkManager.loadData(components, BoxOffice.self) { result in
            switch result {
            case .success(let data):
                completion(.success(data))
            case .failure(let error):
                completion(.failure(error)) 
            }
        }
    }
    1. ViewController์—์„œ Success/ Failure ์ฒ˜๋ฆฌ
    private func fetchBoxOffice(_ result: Result<BoxOffice, NetworkManagerError>) {
        switch result {
        case .success(let boxOffice):
            ...
        case .failure(let error):
            ...
        }
    }

NSMutableAttributedString๋ฅผ ํ™œ์šฉํ•˜์—ฌ ํŠน์ • ๋ฌธ์ž ๋ณ€๊ฒฝํ•˜๊ธฐ

๐Ÿ”ฅ ๋ฌธ์ œ์ 

  • ๋žญํ‚น ๋ณ€๋™ ์ •๋ณด๋ฅผ ํ‘œ์‹œํ•˜๊ธฐ ์œ„ํ•ด ํ™”์‚ดํ‘œ ํŠน์ˆ˜๋ฌธ์ž๋ฅผ ํ™œ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค. ์ด๋•Œ ๋ณ€๋™ ์ •๋ณด์— ๋งž์ถ”์–ด Label Text์˜ ํ•ด๋‹น ํŠน์ˆ˜๋ฌธ์ž์˜ ์ƒ‰์ƒ๋งŒ ๋ฐ”๊พธ๊ณ ์ž ํ•˜์˜€์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด ๋žญํ‚น์ด ๋†’์•„์ง„ ๊ฒฝ์šฐ ๋นจ๊ฐ„์ƒ‰์˜ ์œ„๋กœ ํ–ฅํ•˜๋Š” ํ™”์‚ดํ‘œ๋ฅผ, ๋žญํ‚น์ด ๋‚ฎ์•„์ง„ ๊ฒฝ์šฐ ํŒŒ๋ž€์ƒ‰์˜ ์•„๋ž˜๋กœ ํ–ฅํ•˜๋Š” ํ™”์‚ดํ‘œ๋ฅผ ํ‘œ์‹œํ•˜๊ณ ์ž ํ–ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿงฏ ํ•ด๊ฒฐ๋ฐฉ๋ฒ•

  • UILable์€ String Text๋ฟ๋งŒ์ด ์•„๋‹ˆ๋ผ attributedText๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. String Text๋ฅผ ์‚ฌ์šฉํ•˜๊ฒŒ๋˜๋ฉด ๊ธ€์ž์— foregroundColor๋ฅผ ์ค„ ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์—, attributeText๋ฅผ ์‚ฌ์šฉํ•˜๊ณ ์ž ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • attributedText์—๋Š” NSMutableAttributedString ํƒ€์ž…์˜ ๊ฐ’์„ ๋Œ€์ž…ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์—ฌ String์„ NSMutableAttributedString ํƒ€์ž…์œผ๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.
  • addAttribute(_:value:range:) ๋ฉ”์„œ๋“œ๋ฅผ ์ด์šฉํ•˜์—ฌ ํ•ด๋‹นํ•˜๋Š” ๋ฌธ์ž๋ฅผ ์ง€์ •ํ•œ ์ƒ‰์ƒ์œผ๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.

API Key๋ฅผ git์— ๋…ธ์ถœ์‹œํ‚ค์ง€ ์•Š๋Š” ๋ฐฉ๋ฒ•

๐Ÿ”ฅ ๋ฌธ์ œ์ 

  • ์ด๋ฏธ์ง€ ๊ฒ€์ƒ‰ API๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด Kakao Developer์—์„œ ์•ฑ์„ ์ƒ์„ฑํ•˜์—ฌ REST API Key๋ฅผ ๋ฐœ๊ธ‰๋ฐ›์•˜์Šต๋‹ˆ๋‹ค. ๋ฐœ๊ธ‰๋ฐ›์€ REST API Key๋ฅผ ์ด์šฉํ•ด ์ด๋ฏธ์ง€ ๊ฒ€์ƒ‰ API๋ฅผ ์ด์šฉํ•˜๋Š”๋ฐ ์„ฑ๊ณตํ–ˆ๊ณ , ํ•ด๋‹น ๋‚ด์šฉ์„ ์ปค๋ฐ‹ํ•˜๋ ค๊ณ  ํ–ˆ์Šต๋‹ˆ๋‹ค.
  • ๋ณ€๊ฒฝ ๋‚ด์—ญ์„ ํ™•์ธํ•˜๋˜ ์ค‘ API Key๊ฐ€ ํฌํ•จ๋œ ์ฝ”๋“œ๊ฐ€ ์ปค๋ฐ‹๋œ๋‹ค๋ฉด ์ดํ›„ ๋ณ„๋„ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•ด ํ•ด๋‹น ์ฝ”๋“œ๋ฅผ ์ œ๊ฑฐํ•˜๋”๋ผ๋„ ๊นƒ ์ปค๋ฐ‹ ์ด๋ ฅ์— API Key๊ฐ€ ๊ทธ๋Œ€๋กœ ๋…ธ์ถœ ๋˜๋Š” ์ƒํ™ฉ์ด ๋ฐœ์ƒํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

๐Ÿงฏ ํ•ด๊ฒฐ๋ฐฉ๋ฒ•

  • ์ €ํฌ๋Š” ์ด๋Ÿฌํ•œ ์ƒํ™ฉ์ด ๋ฐœ์ƒํ•˜์ง€ ์•Š๋„๋ก ํ•˜๊ธฐ ์œ„ํ•ด KakaoAPIKey.plist ํŒŒ์ผ์„ ๋งŒ๋“ค๊ณ  gitignore์— ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค. ํ•ด๋‹น ํŒŒ์ผ์€ ๊นƒ์„ ํ†ตํ•ด ๋ฐ›์„ ์ˆ˜ ์—†๊ฒŒ ๋˜์—ˆ๊ธฐ ๋•Œ๋ฌธ์— ํŒ€์›์—๊ฒŒ ์ง์ ‘ ํŒŒ์ผ ์ „๋‹ฌ์„ ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์ž‘์—…ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.
  • plist๋‚ด๋ถ€์˜ ๋ฐ์ดํ„ฐ๋Š” Bundle์„ ํ™•์žฅํ•˜์—ฌ ์ฝ๊ธฐ์ „์šฉ ํ”„๋กœํผํ‹ฐ๋ฅผ ํ†ตํ•ด ๊ฐ€์ ธ์˜ค๋„๋ก ํ–ˆ์Šต๋‹ˆ๋‹ค.
    extension Bundle {
        var kakaoApiKey: String {
            guard let file = self.path(forResource: "KakaoAPIKey", ofType: "plist") else { return "" }
            guard let resource = NSDictionary (contentsOfFile: file) else { return "" }
            guard let key = resource["Authorization"] as? String else {
                fatalError("KakaoAPIKey.plist์— Authorization๋ฅผ ์„ค์ •ํ•ด์ฃผ์„ธ์š”.")
            }
    
            return key
        }
    }
    
    enum KakaoNameSpace {
        ...
        static let authorization = "Authorization"
        static let apiKey = Bundle.main.kakaoApiKey // Bundle์— ๋“ฑ๋ก๋œ Key๋ฅผ NameSpace๋กœ ๊ด€๋ฆฌ
    }
    
    // ์ดํ›„ URLRequest์— header์— ํ•„์š”ํ•œ ์ •๋ณด๋ฅผ ์ฃผ์ž…
    let headers = [
        KakaoNameSpace.authorization : KakaoNameSpace.apiKey
    ]

    Bundle์€ ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ์ฝ”๋“œ์™€ ํ•ด๋‹น ์ฝ”๋“œ์˜ ์ž์›์„ ํฌํ•จํ•˜๋Š” ๋””๋ ‰ํ† ๋ฆฌ์ž…๋‹ˆ๋‹ค. Bundle์€ ์—ฌ๋Ÿฌ๊ฐ€์ง€๊ฐ€ ์žˆ๋Š”๋ฐ, ๊ทธ ์ค‘ main์€ ์•ฑ์ด ์‹คํ–‰๋˜๋Š” ์ฝ”๋“œ๊ฐ€ ์žˆ๋Š” Bundle ๋””๋ ‰ํ† ๋ฆฌ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋Š” bundle์ž…๋‹ˆ๋‹ค.


UIImageView์˜ Height๋ฅผ ๋™์ ์œผ๋กœ ์ž…๋ ฅ

๐Ÿ”ฅ ๋ฌธ์ œ์ 

  • ์ด๋ฏธ์ง€ ๊ฒ€์ƒ‰ API๋ฅผ ํ†ตํ•ด ์–ด๋–ค ์‚ฌ์ด์ฆˆ์˜ ์ด๋ฏธ์ง€๋ฅผ ๊ฐ€์ง€ ์™€๋„ ์ด๋ฏธ์ง€์˜ width๋Š” contentView์˜ width์™€ ๋งž์ถ”๋ฉด ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ UIImage.contentMode๋ฅผ ์–ด๋–ป๊ฒŒ ์กฐ์ •ํ•ด๋„ ๊ฐ€๋กœ ํ˜น์€ ์„ธ๋กœ ์‚ฌ์ด์ฆˆ์˜ ์š”๊ตฌ์กฐ๊ฑด์„ ๋งž์ถœ ์ˆ˜ ์—†์—ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿงฏ ํ•ด๊ฒฐ๋ฐฉ๋ฒ•

  • UIImageView.contentMode์™€ ์ƒ๊ด€์—†์ด, ๋น„์œจ์„ ๊ณ„์‚ฐํ•˜์—ฌ ์„ธ๋กœ ์‚ฌ์ด์ฆˆ๋ฅผ ์กฐ์ •ํ•ด์ฃผ๊ธฐ๋กœ ํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹คํ–‰ํžˆ๋„ ์ด๋ฏธ์ง€ ๊ฒ€์ƒ‰ ์‹œ ๊ฐ€๋กœ, ์„ธ๋กœ ์‚ฌ์ด์ฆˆ ์ •๋ณด๊ฐ€ ํ•จ๊ป˜ ์ œ๊ณต๋˜์—ˆ๊ธฐ ๋•Œ๋ฌธ์— ์–ด๋ ต์ง€ ์•Š๊ฒŒ ๋†’์ด๋ฅผ ๋™์ ์œผ๋กœ ์ž…๋ ฅํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.
    private func setPosterImage(_ imageDocument: ImageDocument, _ image: UIImage) {
        // ๋น„์œจ = UIImage ํ”„๋ ˆ์ž„ ๊ฐ€๋กœ รท ๋กœ๋“œ๋œ ์ด๋ฏธ์ง€ ์‹ค์ œ ๊ฐ€๋กœ ์‚ฌ์ด์ฆˆ
        let ratio = self.movieDetailView.posterImage.frame.width / CGFloat(integerLiteral: imageDocument.width)
        // ๋†’์ด = ๋น„์œจ ร— ๋กœ๋“œ๋œ ์ด๋ฏธ์ง€ ์‹ค์ œ ์„ธ๋กœ ์‚ฌ์ด์ฆˆ
        let height = ratio * CGFloat(integerLiteral: imageDocument.height)
        self.movieDetailView.posterImage.heightAnchor.constraint(equalToConstant: height).isActive = true
        self.movieDetailView.posterImage.image = image
    }

UIFont Extension์„ ํ™œ์šฉํ•˜์—ฌ Dynamic Bold Font ๊ตฌํ˜„

๐Ÿ”ฅ ๋ฌธ์ œ์ 

  • ํŠน์ • ๋ฌธ์ž์˜ ๋‘๊ป˜๋ฅผ ๋ณ€๊ฒฝํ•˜๊ณ ์ž ํ•  ๋•Œ ์–ด๋–ค ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•  ์ง€ ๊ณ ๋ฏผํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • swift ๊ธฐ๋ณธ ์ œ๊ณต ๋ฉ”์„œ๋“œ๋ฅผ ํ™œ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ์žˆ์ง€๋งŒ, ์ด๋Š” Font์˜ ์‚ฌ์ด์ฆˆ๊ฐ€ ๊ณ ์ • ๋œ๋‹ค๋Š” ๋‹จ์ ์ด ์กด์žฌํ–ˆ์Šต๋‹ˆ๋‹ค.
    .systemFont(ofSize: 17, weight: .bold)
  • Label๊ณผ Button์€ Dynamic Type์— ๋Œ€ํ•œ ๋Œ€์‘์ด ๋˜์–ด์•ผํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๊ธฐ ๋•Œ๋ฌธ์—, Font์˜ ์‚ฌ์ด์ฆˆ๊ฐ€ ๊ณ ์ •๋˜์ง€ ์•Š์œผ๋ฉด์„œ ํŠน์ • Font์˜ ๋‘๊ป˜๋ฅผ ์กฐ์ ˆํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์„ ์ฐพ๊ณ ์ž ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

๐Ÿงฏ ํ•ด๊ฒฐ๋ฐฉ๋ฒ•

  • UIFont๋ฅผ extensionํ•˜์—ฌ ํฐํŠธ๋ฅผ Customํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ์•Œ๊ฒŒ๋˜์–ด ์ด๋ฅผ ํ™œ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค.
    extension UIFont {
        static func preferredFont(for style: TextStyle, weight: Weight) -> UIFont {
            let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: style)
            let font = UIFont.systemFont(ofSize: descriptor.pointSize, weight: weight)
            let metrics = UIFontMetrics(forTextStyle: style)
            return metrics.scaledFont(for: font)
        }
    }

Singleton ๊ตฌ์กฐ์˜ DateManager๋กœ Date ๊ด€๋ฆฌ ๋กœ์ง ๋ถ„๋ฆฌ

๐Ÿ”ฅ ๋ฌธ์ œ์ 

  • ์ด์ „ Step์—์„œ๋Š” ์–ด์ œ์˜ ๋‚ ์งœ ๊ธฐ์ค€์œผ๋กœ ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ๋กœ๋“œํ•˜๋ฉด ๋˜์—ˆ์œผ๋‚˜, ์ด๋ฒˆ Step๋ถ€ํ„ฐ๋Š” ๋‹ค์–‘ํ•œ ๋‚ ์งœ๋ฅผ ๋Œ€์‘ํ•ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค. ์—ฌ๋Ÿฌ ViewController์—์„œ ์ง€์ • ๋‚ ์งœ๋ฅผ ๊ณต์œ ํ•ด์•ผ ํ•˜๋Š” ์ƒํ™ฉ์—์„œ ์–ด๋–ค ๋ฐฉ์‹์œผ๋กœ ๋Œ€์‘ํ• ์ง€ ๊ณ ๋ฏผ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.
  • ViewController๊ฐ„ ๋‚ ์งœ ์ •๋ณด๋ฅผ ์ฃผ๊ณ  ๋ฐ›์„ ์ˆ˜ ์žˆ์ง€๋งŒ, ๋‚ ์งœ ์ •๋ณด๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ViewController๊ฐ€ ๋ชจ๋‘ ๋ฉ”๋ชจ๋ฆฌ์—์„œ ํ•ด์ œ๋˜๋Š” ๊ฒฝ์šฐ ๋‚ ์งœ ์ •๋ณด๊ฐ€ ์ดˆ๊ธฐํ™” ๋˜๋Š” ์œ„ํ—˜์ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.
  • ๊ธฐ์กด์— ์ƒ์„ฑํ•œ BoxOfficeService๋Š” ์•ฑ์˜ ์ƒ๋ช…์ฃผ๊ธฐ์™€ ํ•จ๊ป˜ํ•˜๋Š” ๊ตฌ์กฐ์ฒด์ด๊ธฐ ๋•Œ๋ฌธ์—, ๊ธฐ์กด ๋‚ ์งœ ๊ด€๋ จ ๋กœ์ง์„ ์ด๊ณณ์—์„œ ๊ด€๋ฆฌํ•˜์˜€์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋‚ ์งœ ๊ด€๋ จ ๋กœ์ง์ด ์ฆ๊ฐ€ํ•˜๋ฉด์„œ ์ด์ „๊ณผ ๊ฐ™์ด BoxOfficeService์—์„œ ์ด๋ฅผ ๋ชจ๋‘ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ์€ ์ ์ ˆํ•˜์ง€ ์•Š๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿงฏ ํ•ด๊ฒฐ๋ฐฉ๋ฒ•

  • DateManager๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ๋‚ ์งœ ๊ด€๋ จ ํ”„๋กœํผํ‹ฐ๋ฅผ ํ•ด๋‹น ํด๋ž˜์Šค์—์„œ ์ฒ˜๋ฆฌํ•˜๋„๋ก ํ–ˆ์Šต๋‹ˆ๋‹ค.
    class DateManager {
        static private let dateFormatter = DateFormatter()
        static let yesterday: Date = .now - (24 * 60 * 60)
        static var selectedDate: Date = yesterday
        ...
    
        private init() {}
    }

UICalendarView ์„ ํƒ๋œ ๋‚ ์งœ

๐Ÿ”ฅ ๋ฌธ์ œ์ 

  • ๋‚ ์งœ์„ ํƒ ํ™”๋ฉด์˜ ๋‹ฌ๋ ฅ์—๋Š” ํ˜„์žฌ ์„ ํƒ๋œ ๋‚ ์งœ๊ฐ€ ๋ฏธ๋ฆฌ ์„ ํƒ๋˜์–ด ์žˆ์–ด์•ผ ํ•œ๋‹ค๋Š” ๋‚ด์šฉ์ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ํ•ด๋‹น ์š”๊ตฌ์‚ฌํ•ญ์„ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด UICalendarView.Decoration๋ฅผ ์ด์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

    ์ปค์Šคํ…€ ๋ฐ์ฝ”๋ ˆ์ด์…˜์— ์ ์šฉํ•œ ์ฝ”๋“œ

    private func customDecoration() -> UIView {
        let view = UIView()
        view.backgroundColor = .red
        view.clipsToBounds = false
        view.frame = CGRect(x: 0, y: 0, width: 50, height: 50)
        return view
    }
  • ๊ฒฐ๊ณผ๋Š” ๋‚ ์งœ์˜ ํ•˜๋‹จ ์ผ๋ถ€ ์˜์—ญ์—๋งŒ ์ปค์Šคํ…€์„ ํ•  ์ˆ˜ ์žˆ์„ ๋ฟ, ๋‚ ์งœ ์ž์ฒด๊ฐ€ ์„ ํƒ๋œ ํšจ๊ณผ๋ฅผ ์ค„ ์ˆ˜ ์—†์—ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿงฏ ํ•ด๊ฒฐ๋ฐฉ๋ฒ•

  • UICalendarSelectionSingleDate๋ฅผ ์ธ์Šคํ„ด์Šคํ™” ํ•˜๊ณ (UICalendarSelectionSingleDateDelegate๋„ ์ฑ„ํƒํ•ฉ๋‹ˆ๋‹ค.) ์•„๋ž˜ ์ฝ”๋“œ๋ฅผ ์ ์šฉํ•˜๋ฉด, ์›ํ•˜๋Š” ํšจ๊ณผ๊ฐ€ ์ ์šฉ๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.
    let dateSelection = UICalendarSelectionSingleDate(delegate: self)
    calendarView.selectionBehavior = dateSelection
  • ์ถ”๊ฐ€๋กœ ์•„๋ž˜ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์—ฌ ์บ˜๋ฆฐ๋”๋ทฐ๊ฐ€ ์—ด๋ฆด ๋•Œ๋ถ€ํ„ฐ ๋‚ ์งœ๊ฐ€ ์„ ํƒ๋œ ํšจ๊ณผ๋ฅผ ์ ์šฉํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.
    dateSelection.selectedDate = DateComponents(year: year, month: month, day: day)

7. ๐Ÿ“š ์ฃผ์š” ํ•™์Šต ๋‚ด์šฉ

โœ๏ธ Modern Collection View - DiffableDataSource ์‚ฌ์šฉํ•˜๊ธฐ

  • ์ผ๋ฐ˜ CollectionView
    • ViewController์— UICollectionViewDataSource ํ”„๋กœํ† ์ฝœ ์ฑ„ํƒ ํ›„ ํ•„์š” ๋ฉ”์„œ๋“œ๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.
    • ์ดํ›„ CollectionView์˜ dataSource๋ฅผ self๋กœ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.
  • Modern Collection View
    • dataSource๋กœ ์‚ฌ์šฉํ•  UICollectionViewDiffableDataSource ํด๋ž˜์Šค๋ฅผ ์ธ์Šคํ„ด์Šคํ™” ํ•ฉ๋‹ˆ๋‹ค.
    • DiffableDataSource๋ฅผ ์ธ์Šคํ„ด์Šคํ™” ํ• ๋•Œ๋Š” CollectionView, ์‚ฌ์šฉํ•  ๋ฐ์ดํ„ฐ(e.g. DTO, Model ๋“ฑ), Section, IndexPath๊ฐ€ ํ•„์š”๋กœ ํ•ฉ๋‹ˆ๋‹ค.
    • DiffableDataSource์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๋ฐ์ดํ„ฐ ์ •๋ณด๊ฐ€ ์ถ”๊ฐ€, ์‚ญ์ œ, ๋ณ€๊ฒฝ์ด ๋˜๋Š” ๊ฒฝ์šฐ๋Š” NSDiffableDataSourceSnapshot ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
    • SnapShot์—๋Š” Section์ •๋ณด์™€ Items์ •๋ณด๋ฅผ ๊ฐ๊ฐ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.
    • ์ดํ›„ dataSource์ธ์Šคํ„ด์Šค์— Snapshot์„ applyํ•ฉ๋‹ˆ๋‹ค.

โœ๏ธ iOS14๋ฒ„์ „ ์ดํ•˜์™€๋„ ํ˜ธํ™˜์ด ๋˜๋Š” CollectionView ๊ตฌ์„ฑํ•˜๊ธฐ

  • CollectionView ์š”์†Œ๋ฅผ ๊ตฌ์„ฑํ•  ๋•Œ, ๋ฒ„์ „ ํ˜ธํ™˜ ๋ฌธ์ œ์™€ ์ง๋ฉดํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  1. ๋””ํ…Œ์ผ ์•…์„ธ์‚ฌ๋ฆฌ ๊ตฌํ˜„ ์‹œ ์˜ˆ์‹œ์— ์ œ์‹œ๋˜์–ด ์žˆ๋Š” ๋””ํ…Œ์ผ ์•…์„ธ์‚ฌ๋ฆฌ๋ฅผ UICellAccessory์—์„œ ์ง€์›ํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.

    cell.accessories = [ 
        .disclosureIndicator(options: .init(tintColor: .systemGray)), 
    ]

    ํ•˜์ง€๋งŒ UICellAccessory์€ iOS 14.0 ์ด์ƒ๋ถ€ํ„ฐ ์ง€์›์ด ๊ฐ€๋Šฅํ•˜์˜€์Šต๋‹ˆ๋‹ค.

    ์ฐธ๊ณ  - UICellAccessory ๊ณต์‹ ๋ฌธ์„œ

    ์ด๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด iOS14.0 ์ดํ•˜ ๋ฒ„์ „์„ ์ง€์›ํ•  ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์— ๋‹ค๋ฅธ ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„ํ•ด๋ณด๊ณ ์ž ํ–ˆ์Šต๋‹ˆ๋‹ค. ํ•˜์—ฌ ๋ณ„๋„์˜ label์„ ๋งŒ๋“ค์–ด ๋””ํ…Œ์ผ ์•…์„ธ์‚ฌ๋ฆฌ๋ทฐ์™€ ๊ฐ™์€ ํ˜•ํƒœ๋ฅผ ๋Œ ์ˆ˜ ์žˆ๋„๋ก ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

    private let disclosureIndicatorLabel: UILabel = {
        var label = UILabel()
        label.text = "ใ€‰"
        label.textColor = UIColor(displayP3Red: 0.8, green: 0.8, blue: 0.8, alpha: 1)
        return label
    }()
  2. ์…€ Separator ๊ตฌํ˜„ ์‹œ UICollectionViewListCell์˜ ํ”„๋กœํผํ‹ฐ ์ค‘ separator๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๊ฐ CollecionViewCell ๋ณ„๋กœ ๊ตฌ๋ถ„์„ ์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ UICollectionViewListCell์€ iOS14.0 ์ด์ƒ ๋ฒ„์ „์—์„œ๋งŒ ์ง€์›์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

    ์ฐธ๊ณ  - UICollectionViewListCell ๊ณต์‹ ๋ฌธ์„œ

    ํ•˜์—ฌ iOS14.0์ดํ•˜ ๋ฒ„์ „๊ณผ์˜ ํ˜ธํ™˜์„ ์œ„ํ•ด CustomCell์˜ ๋‚ด์šฉ์„ View๋กœ ๊ฐ์‹ธ์„œ ์œ„,์•„๋ž˜ ๋†’์ด 1pt ์—ฌ๋ฐฑ ๊ณต๊ฐ„์„ ๋งŒ๋“ค์–ด separator๋กœ ๋ณด์ผ ์ˆ˜ ์žˆ๊ฒŒ๊ธˆ ์ฝ”๋“œ ์ž‘์„ฑ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

    private lazy var separatorLineView: UIView = {
        let view = UIView()
        view.backgroundColor = .init(displayP3Red: 0.9, green: 0.9, blue: 0.9, alpha: 1)
        view.frame.size.width = view.frame.width
        view.translatesAutoresizingMaskIntoConstraints = false
        
        return view
    }()

โœ๏ธ animatedImage๋ฅผ ํ™œ์šฉํ•˜์—ฌ Loading ํ™”๋ฉด ๊ตฌ์„ฑ

  • ์ด๋ฏธ์ง€ ๋กœ๋”ฉ ํ™”๋ฉด์„ ๊ตฌ์„ฑ ์‹œ ์–ด๋–ค ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•  ์ง€ ๊ณ ๋ฏผ์ด ๋งŽ์•˜์Šต๋‹ˆ๋‹ค. BoxOfficeViewController์ฒ˜๋Ÿผ activityIndicatorView๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜๋„ ์žˆ์—ˆ์ง€๋งŒ, ๋‹ค๋ฅธ ์ข…๋ฅ˜์˜ ๋กœ๋”ฉํ™”๋ฉด๋„ ๊ตฌํ˜„ํ•ด๋ณด๊ณ ์ž ํ•˜์˜€์Šต๋‹ˆ๋‹ค. ๊ณ ๋ฏผ์„ ํ•˜๋˜ ์ค‘ ํ†ต์ƒ์ ์ธ ์•ฑ์—์„œ ๋กœ๋”ฉํ™”๋ฉด์„œ ์›€์ง์ด๋Š” ์ด๋ฏธ์ง€๋ฅผ ์ฐธ๊ณ ํ•˜์—ฌ ์ด์™€ ๋น„์Šทํ•˜๊ฒŒ ๊ตฌํ˜„์„ ํ•ด๋ณด๊ณ ์ž ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • asset์— gif ์ด๋ฏธ์ง€๋ฅผ frame๋ณ„ pngํŒŒ์ผ๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ์ €์žฅํ•˜์˜€์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ UIImage์—์„œ animatedImage๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์ž„์˜์˜ duration์„ ์ง€์ •ํ•˜์—ฌ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์›€์ง์ด๋Š” ํ˜•์ƒ์„ ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ๋„๋ก ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • ํ˜„์žฌ ์ €ํฌ ํ”„๋กœ์ ํŠธ์—์„œ ๋กœ๋”ฉํ•˜๋Š” ์ด๋ฏธ์ง€๋Š” ๋น ๋ฅธ ์†๋„๋กœ ์ฒ˜๋ฆฌ๊ฐ€ ๋˜๊ธฐ ๋•Œ๋ฌธ์— ์ €ํฌ๊ฐ€ ๊ตฌ์„ฑํ•œ Image Loadingํ™”๋ฉด์ด ์งง์€ ์ฐฐ๋‚˜์— ๊นœ๋นก์ด๊ณ  ์‚ฌ๋ผ์ง€๋Š” ํ˜•์ƒ์„ ๋„๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ €ํฌ๋Š” ์˜คํžˆ๋ ค ์ด๋ ‡๊ฒŒ ์งง์€ ๋กœ๋”ฉํ™”๋ฉด์ด user์—๊ฒŒ ์˜ค๋ฅ˜๊ฐ€ ๋‚˜๋Š” ํ˜•์ƒ์ฒ˜๋Ÿผ ๋ณด์—ฌ์งˆ ์ˆ˜ ์žˆ๋‹ค ์ƒ๊ฐํ•˜์˜€์Šต๋‹ˆ๋‹ค. ํ•˜์—ฌ ์ด๋ฏธ์ง€๊ฐ€ ๋กœ๋”ฉ ์ค‘์ด๋ผ๋Š” ๊ฒƒ์„ user์—๊ฒŒ ๋ช…์‹œํ•˜๊ธฐ ์œ„ํ•ด usleep(500000)์„ ์ฃผ์–ด Image Loading์˜ ๊ณผ์ •์ด ๋ณด๋‹ค ์ €ํฌ์˜ ์˜๋„์™€ ๋งž๊ฒŒ๋” ์กฐ์ •ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

โœ๏ธ URLCache in Memory

  • ์ €ํฌ๋Š” ํ”„๋กœ์ ํŠธ์— NSCache๋ฅผ ์ ์šฉํ–ˆ์ง€๋งŒ, URLCache๋„ ๊ณต๋ถ€ํ•ด ๋ณด์•˜์Šต๋‹ˆ๋‹ค.
  • URLCache๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ์บ์‹œ ์ €์žฅ์ด ondisk์ธ ๊ฒƒ์„ ํ™•์ธํ–ˆ๊ณ , ์ด๊ฒƒ์„ ๋ณ€๊ฒฝํ•˜๊ธฐ ์œ„ํ•ด StoragePolicy๋ฅผ allowedInMemoryOnly๋กœ ์ง€์ •ํ•ด ๋ณด์•˜์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ €ํฌ์˜ ์˜ˆ์ƒ๊ณผ ๋‹ฌ๋ฆฌ StoragePolicy๋ฅผ ๋ณ€๊ฒฝํ•˜์˜€์Œ์—๋„ ์บ์‹œ ๋ฐ์ดํ„ฐ๊ฐ€ Memory์— ์ €์žฅ ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.
  • ์•„๋ž˜์™€ ๊ฐ™์ด ์—ฌ๋Ÿฌ ์‹คํ—˜ ๋์— (30 * 1024 * 1024) ๋ถ€ํ„ฐ๋Š” URLCache๊ฐ€ ๋ฉ”๋ชจ๋ฆฌ์— ์ €์žฅ์ด ๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.
    ------------------------------------------------------------------------------
    URLCache.shared์˜ memoryCapacity: 512,000 bytes
                      diskCapacity: 10,000,000 bytes
    
    CachedURLResponse์˜ storagePolicy๊ฐ€ .allowedInMemoryOnly์ผ ๋•Œ,
    memoryCapacity: 10, 20 (* 1024 * 1024)์ผ ๋•Œ๋Š” ์‹คํŒจํ•จ. 30๋ถ€ํ„ฐ ์„ฑ๊ณต. 31,457,280 bytes
    ์ฒซ๋ฒˆ์งธ data - 1,469,837 bytes
    ๋‘๋ฒˆ์งธ data - 1,078,478 bytes
    ------------------------------------------------------------------------------
    
  • ๋•Œ๋ฌธ์— ์ €ํฌ๋Š” ์ €์žฅ๋˜์–ด์•ผํ•˜๋Š” ๋ฐ์ดํ„ฐ ๋ณด๋‹ค ์ง€์ • memoryCapacity๊ฐ€ ํด ๋•Œ๋งŒ URLCache์˜ inmemory Policy๊ฐ€ ์ ์šฉ๋œ๋‹ค๊ณ  ์ถ”์ธกํ–ˆ์Šต๋‹ˆ๋‹ค.

8. ๐Ÿ’ญ ํŒ€ ํšŒ๊ณ 

ํŒ€ ํšŒ๊ณ 

์šฐ๋ฆฌํŒ€์ด ์ž˜ํ•œ ์ ๐Ÿ˜ƒ

์ถ”ํ›„ ์ž‘์„ฑ ์˜ˆ์ •

์šฐ๋ฆฌํŒ€์ด ์•„์‰ฌ์› ๋˜ ์ ๐Ÿ˜ญ

์ถ”ํ›„ ์ž‘์„ฑ ์˜ˆ์ •


9. ๐Ÿ”— ์ฐธ๊ณ  ๋งํฌ

๐ŸŽ Developer Apple

๐Ÿ“’ Blog

๐Ÿ‘พ Git

๐ŸŒŠ stack overflow