/ios-movie-app

๐ŸŽฌ ์˜ํ™” ์ •๋ณด, ์ˆœ์œ„ ์•ฑ

Primary LanguageSwift

๐Ÿฟ ์˜ํ™” ์•ฑ

์ตœ์‹  ์˜ํ™”์˜ ์ˆœ์œ„ ๋ฐ ์žฅ๋ฅด ๋ณ„ ์˜ํ™”๋ฅผ ์ฐพ์•„๋ณผ ์ˆ˜ ์žˆ๋Š” ๋ฐ•์Šค์˜คํ”ผ์Šค ์•ฑ์ž…๋‹ˆ๋‹คโ˜บ๏ธ

๊ฐœ๋ฐœ ์ธ์› ์ œ์ž‘ ๊ธฐ๊ฐ„
1์ธ ๊ฐœ๋ฐœ 5.16 ~ 5.30(2์ฃผ)

๐ŸŒณ ๋ชฉ์ฐจ

  1. ๐Ÿ“บ ์‹คํ–‰ ํ™”๋ฉด
  2. ๐Ÿ  ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ
  3. ๐Ÿ“š Library
  4. ๐Ÿคจ ๊ณ ๋ฏผ๊ณผ ํ•ด๊ฒฐ

๐Ÿ“บ ์‹คํ–‰ ํ™”๋ฉด

ํ™ˆํ™”๋ฉด ๋ทฐ ๋””ํ…Œ์ผ ๋ทฐ ์žฅ๋ฅด ๋ทฐ
์„œ์น˜ ๋ทฐ ์ฆ๊ฒจ์ฐพ๊ธฐ ๋ทฐ

๐Ÿ  ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ

image

๐Ÿ“š Library

Autolayout
Snapkit
  • Autolayout์„ ์žก์•„์ค„ ๋•Œ ์‚ฌ์šฉ๋˜๋Š” boilerplate ์ฝ”๋“œ๋ฅผ ์ค„์ด๊ธฐ์œ„ํ•˜์—ฌ Snapkit ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ™œ์šฉํ–ˆ์–ด์š”.

๐Ÿคจ ๊ณ ๋ฏผ๊ณผ ํ•ด๊ฒฐ

๐Ÿ’ฅ ํ•˜๋‚˜์˜ datasource๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์„œ๋กœ ๋‹ค๋ฅธ API ๋ฐ์ดํ„ฐ ๋ชจ๋ธ ์ฒ˜๋ฆฌํ•˜๊ธฐ

๐Ÿ“ Please Open

โ‰๏ธ DiffableDatasource๋ฅผ ์‚ฌ์šฉํ•œ ์ด์œ 

  • Home scene ์† ์ƒ๋‹จ section์€ ์ธ๊ธฐ ์žˆ๋Š” ์˜ํ™” ๋ณด์—ฌ์ฃผ๊ณ , ํ•˜๋‹จ section์€ ์ „์ฒด ์˜ํ™”๋ฅผ ์žฅ๋ฅด ๋ณ„๋กœ ๋‚˜๋ˆด์–ด์š”.
  • ์ƒ๋‹จ section(์ดํ•˜ rank section)์˜ ๊ฒฝ์šฐ, header์— ์ธ๊ธฐ ์ƒ์Šน ์ˆœ, ์˜ํ™” ๊ฐœ๋ด‰ ์ˆœ ๋ฒ„ํŠผ์„ ๋‘์–ด ํ•ด๋‹น ๊ธฐ์ค€์„ ํ†ตํ•˜์—ฌ ์˜ํ™”๋“ค์ด sorting์ด ๋˜๋„๋ก ๊ตฌํ˜„์„ ํ•ด๋ณด์•˜์–ด์š”. ๋˜ํ•œ, ํ•˜๋‹จ์˜ ์žฅ๋ฅด๋ณ„ ์˜ํ™” section(์ดํ•˜ genre section)์˜ ๊ฒฝ์šฐ์—๋„, ๋”๋ณด๊ธฐ ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ ์ˆจ๊ฒจ์ง„ ๋‚˜๋จธ์ง€ ์…€๋“ค์„ ๋ณด์—ฌ์ฃผ๋„๋ก ๊ตฌํ˜„์„ ํ•ด๋ณด์•˜์–ด์š”.
  • ๊ธฐ์กด์—๋Š” reloadData๋ฅผ ํ™œ์šฉํ•˜์—ฌ controller๊ฐ€ ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ UI์—๊ฒŒ ์•Œ๋ ค์ฃผ๋Š” ๋™๊ธฐํ™” ์ž‘์—…์„ ํ•ด์ฃผ์—ˆ์–ด์š”. ๊ทธ๋Ÿฌ๋‚˜, ํ•ด๋‹น ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ๋ ˆ์ด์•„์›ƒ UI๊ฐ€ ๋นˆ๋ฒˆํ•˜๊ฒŒ ๋ณ€๊ฒฝ๋˜๋ฉฐ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์ž๋™์œผ๋กœ ์ ์šฉ๋˜์ง€ ์•Š๋Š” reloadData๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๋™๊ธฐํ™”๋ฅผ ์ง„ํ–‰ํ•  ๊ฒฝ์šฐ, ์‚ฌ์šฉ์ž UX ๊ฒฝํ—˜์ด ๋–จ์–ด์งˆ ์ˆ˜ ์žˆ๋‹ค๊ณ  ํŒ๋‹จ์ด ๋“ค์—ˆ๊ธฐ์— DiffableDatasource๋ฅผ ํ™œ์šฉํ–ˆ์–ด์š”.

Diffabledatasource์˜ ๊ฒฝ์šฐ, ํ•˜๋‚˜์˜ collection view์—์„œ ํ•˜๋‚˜์˜ item identifier ํƒ€์ž…์„ ๊ฐ€์ง€๊ณ  ์žˆ์–ด์š”. ๊ฐ๊ฐ์˜ ์„น์…˜์€ ์„œ๋กœ ๋‹ค๋ฅธ API ํ˜ธ์ถœ์„ ํ•˜๋ฏ€๋กœ, ๋‹ค๋ฅธ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ์„ ๊ฐ€์ง€๊ณ  ํ•˜๋‚˜์˜ datasource item identifier ํƒ€์ž…์„ ๋งŒ๋“ค์–ด์ค˜์•ผํ–ˆ์–ด์š”. ๋‹ค์Œ์€ ์ œ๊ฐ€ ํ•ด๋‹น ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ ๊ณ ๋ฏผํ–ˆ๋˜ ๊ณผ์ •์ด์—์š”.

1๏ธโƒฃ ๋‘ ๊ฐ€์ง€์˜ ๋ชจ๋ธ์„ ๋ฌถ์–ด์ค„ ์ˆ˜ ์žˆ๋Š” ํ•˜๋‚˜์˜ Item Identifier ํƒ€์ž… ์ƒ์„ฑํ•˜๊ธฐ

  • ์ €๋Š” ๋‘ API๋ฅผ ํ†ตํ•ด์„œ ๋ฐ›์•„์˜จ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ์„ ํ•ฉ์นœ Movie ํƒ€์ž…์„ ๋งŒ๋“ค์–ด์„œ item identifier ํƒ€์ž…์œผ๋กœ ํ™œ์šฉํ•˜๋ ค๊ณ  ํ–ˆ์–ด์š”.
  • ๊ทธ๋ฆฌ๊ณ , ์•„๋ž˜์™€ ๊ฐ™์ด ์„œ๋กœ ๋‹ค๋ฅธ section์— items๋“ค์„ appendํ•˜๋„๋ก ๋กœ์ง์„ ๊ตฌํ˜„ํ–ˆ์–ด์š”.
// ๐Ÿฟ in HomeViewController
private var datasource: DataSource?
// โœ… Rank Section
private var movies = [Movie]() {
    didSet {
        applySnapShot()
    }
}
// โœ… genre Section
private var genres = [Movie]() {
    didSet {
        applySnapShot()
    }
}
private func applySnapShot() {
        var snapShot = SnapShot()
        snapShot.appendSections([.rank])
        snapShot.appendItems(movies)

        snapShot.appendSections([.genre])
        snapShot.appendItems(genres)

        self.datasource?.apply(snapShot)
    }
}
  • ์ด๋ ‡๊ฒŒ ๊ตฌํ˜„ํ•  ๊ฒฝ์šฐ, ์›ํ•˜๋Š” ๋Œ€๋กœ ์„œ๋กœ ๋‹ค๋ฅธ ํƒ€์ž…์˜ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ์„ ํ•˜๋‚˜์˜ diffable datasource๋ฅผ ํ™œ์šฉํ•˜์—ฌ collection view์— ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์—ˆ์–ด์š”.
  • ๊ทธ๋Ÿฌ๋‚˜, ๋‘˜์„ ํ•ฉ์นœ model(item identifier)์€ optional์ด ๋‚œ๋ฌดํ–ˆ๊ณ , ๋ถˆํ•„์š”ํ•œ ์ฝ”๋“œ๋“ค์ด ๋งŽ์ด ์ƒ๊ธฐ๊ฒŒ ๋˜์—ˆ์–ด์š”.

2๏ธโƒฃ  ํ”„๋กœํ† ์ฝœ์„ ํ™œ์šฉํ•˜์—ฌ item identifier ํƒ€์ž… ๊ตฌํ˜„ํ•˜๊ธฐ

  • ๋‹ด๊ณ  ์‹ถ์€ ๋ฐ์ดํ„ฐ ํƒ€์ž…์„ ItemIdenfiable ํ”„๋กœํ† ์ฝœ์„ ์ฑ„ํƒํ•˜๋„๋ก ํ•ด์š”.
// ๐Ÿ’ฅ ItemIdenfiable ํ”„๋กœํ† ์ฝœ ์ •์˜
protocol ItemIdenfiable {
    var identity: UUID { get set }
}

// ๐Ÿ’ฅ ํ”„๋กœํ† ์ฝœ์„ ์ฑ„ํƒํ•œ MovieGenre ํƒ€์ž… ์ •์˜
struct MovieGenre: ItemIdenfiable {
    var identity = UUID()
		...
}

// ๐Ÿ’ฅ ํ”„๋กœํ† ์ฝœ์„ ์ฑ„ํƒํ•œ Movie ํƒ€์ž… ์ •์˜
struct Movie: ItemIdenfiable {
    var identity = UUID()
		...
}
  • ItemIdenfiable ํ•œ ํ”„๋กœํผํ‹ฐ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ItemType ๊ตฌ์กฐ์ฒด๋ฅผ ์ •์˜ํ•˜์—ฌ diffable datasource์˜ item identifier ํƒ€์ž…์œผ๋กœ ํ™œ์šฉํ•˜๋Š” ๋ฐฉ์‹์ด์—์š”.
// ๐Ÿ’ฅ Item identifier ํƒ€์ž… ๊ตฌํ˜„
private struct ItemType: Hashable {

    var item: any ItemIdenfiable

    static func == (lhs: MovieHomeViewController.ItemType, rhs: MovieHomeViewController.ItemType) -> Bool {
        return lhs.item.identity == rhs.item.identity
    }

    func hash(into hasher: inout Hasher) {
        hasher.combine(item.identity)
    }

}

private typealias DataSource = UICollectionViewDiffableDataSource<Section, ItemType>
  • ์•„๋ž˜์™€ ๊ฐ™์ด ItemType์œผ๋กœ ItemIdenfiable ์„ ์ฑ„ํƒํ•œ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ์„ ๊ฐ์‹ธ์„œ items๋กœ appendํ•ด์š”.
private func applySnapShot() {
        var snapShot = SnapShot()
        snapShot.appendSections(Section.allCases)

        var rankMovies = movieHomeController.movies
        let movieItems = rankMovies.map { ItemType(item:$0) }
        snapShot.appendItems(movieItems, toSection: .rank)

        var allGenres = movieHomeController.genres
        let genreItems = allGenres.map { ItemType(item: $0) }
        snapShot.appendItems(genreItems, toSection: .genre)

        datasource?.apply(snapShot)
}

2๏ธโƒฃ enum์˜ ์—ฐ๊ด€ ๊ฐ’์„ ํ™œ์šฉํ•˜์—ฌ Item ํƒ€์ž… ๊ตฌํ˜„ํ•˜๊ธฐ

  • ๊ทธ๋Ÿฌ๋˜ ์ค‘, ์• ํ”Œ ๊ณต์‹๋ฌธ์„œ ์† ์˜ˆ์ œ ์ฝ”๋“œ์—์„œ ์„œ๋กœ ๋‹ค๋ฅธ item๋“ค์„ ํ•˜๋‚˜์˜ ํƒ€์ž…์œผ๋กœ ๊ฐ์‹ธ์„œ ํ™œ์šฉํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ๋ณด์•˜์–ด์š”
  • ์ด๋ฅผ ์ฐธ๊ณ ํ•˜์—ฌ ์ €๋„ ํ•˜๋‚˜์˜ enum ํƒ€์ž…์œผ๋กœ ์„œ๋กœ ๋‹ค๋ฅธ section์— ํ™œ์šฉ๋˜๋Š” ๋ฐ์ดํ„ฐ ๋ชจ๋ธ์„ ๋ฌถ๊ณ , ๊ฐ๊ฐ์˜ ๋ชจ๋ธ์„ enum์˜ ์—ฐ๊ด€ ๊ฐ’์œผ๋กœ ๋„ฃ์–ด์ฃผ๋Š” ๋ฐฉ๋ฒ•์œผ๋กœ ๊ตฌํ˜„ํ•ด๋ณด๊ธฐ๋กœ ํ–ˆ์–ด์š”.
private struct SidebarItem: Hashable {
        let title: String
        let type: SidebarItemType

        enum SidebarItemType {
            case standard, collection, expandableHeader
        }
}

private func createSnapshotOfRecipeCollections() -> NSDiffableDataSourceSectionSnapshot<SidebarItem> {
        let items = recipeSplitViewController.recipeCollections.map { SidebarItem(title: $0, type: .collection) }
        return createSidebarItemSnapshot(.recipeCollectionItems, items: items)
}
  • ์—ฐ๊ด€ ๊ฐ’์œผ๋กœ ๋‚ด๊ฐ€ ๋‹ด๊ณ  ์‹ถ์€ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ ํƒ€์ž…์„ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๋Š” item ์—ด๊ฑฐํ˜• ํƒ€์ž… ์ •์˜ํ•ด์š”.
private enum Item: Hashable {
    case rank(Movie)
    case gerne(MovieGenre)
}

private typealias DataSource = UICollectionViewDiffableDataSource<Section, Item>
  • ์•„๋ž˜์™€ ๊ฐ™์ด enum์˜ ์—ฐ๊ด€ ๊ฐ’์œผ๋กœ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ์„ ๋„ฃ์–ด์„œ item identifier ํƒ€์ž…์„ ๊ตฌํ˜„ํ•ด์š”.
private func applySnapShot() {
    var snapShot = SnapShot()
    snapShot.appendSections(Section.allCases)

    var rankMovies = movieHomeController.movies
		// ๐Ÿ’ฅ ์—ฐ๊ด€ ๊ฐ’ ๋„ฃ์–ด์ฃผ๊ธฐ
    let movieItems = rankMovies.map { Item.rank($0) }

    var allGenres = movieHomeController.genres
    snapShot.appendItems(movieItems, toSection: .rank)

		// ๐Ÿ’ฅ ์—ฐ๊ด€ ๊ฐ’ ๋„ฃ์–ด์ฃผ๊ธฐ
    let genreItems = allGenres.map { Item.gerne($0) }
    snapShot.appendItems(genreItems, toSection: .genre)

    datasource?.apply(snapShot)
}

๐Ÿ’ฌ 2,3๋ฒˆ ๋ฐฉ๋ฒ• ๋ชจ๋‘ ๋‹ค, Hashable์„ ๋งŒ์กฑํ•˜๋Š” ํƒ€์ž…์œผ๋กœ ๋„ฃ๊ณ  ์‹ถ์€ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ์„ ๋ž˜ํ•‘ํ–ˆ๋‹ค๋Š” ๊ณตํ†ต์ ์ด ์žˆ์–ด์š”.

  • 2๋ฒˆ์งธ protocol ๋ฐฉ๋ฒ•์˜ ๊ฒฝ์šฐ, DIP ๋ฒ•์น™์„ ๋งŒ์กฑํ•˜์—ฌ ItemIdentifiable ํ”„๋กœํ† ์ฝœ์„ ์ฑ„ํƒํ•œ ์–ด๋–ค ๋ชจ๋ธ์ด๋“  item identifier๊ฐ€ ๋  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ์†์‰ฝ๊ฒŒ section์ด ์ถ”๊ฐ€๋  ๋•Œ๋งˆ๋‹ค item ํƒ€์ž…์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์–ด์š”.
  • 3๋ฒˆ์งธ enum์˜ ์—ฐ๊ด€ ๊ฐ’์„ ํ™œ์šฉํ•œ ๊ฒฝ์šฐ, ์„น์…˜์ด ์ถ”๊ฐ€๋  ๊ฒฝ์šฐ, case๋งŒ ์ถ”๊ฐ€ํ•ด์ฃผ๋ฉด ์†์‰ฝ๊ฒŒ item ํƒ€์ž…์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์–ด์š”.

โžก๏ธ ๋‘ ๊ฐ€์ง€ ๋ฐฉ๋ฒ• ๋‹ค ์žฅ์ ์ด ์กด์žฌํ•˜์ง€๋งŒ, enum์„ ํ™œ์šฉํ•œ ๋ฐฉ๋ฒ•์ด case๋ฅผ ํ†ตํ•˜์—ฌ ํ•œ๋ˆˆ์— ํ™œ์šฉ๋œ item type์ด ๋ณด์ธ๋‹ค๋Š” ์ ๊ณผ, cell ์žฌ์‚ฌ์šฉ ์‹œ ๋ถ„๊ธฐ์ฒ˜๋ฆฌ์—์„œ ๋ถˆํ•„์š”ํ•œ default ์ผ€์ด์Šค๋ฅผ ์จ์ฃผ์ง€ ์•Š์•„๋„ ๋œ๋‹ค๋Š” ์ ์—์„œ ํ•ด๋‹น ํ”„๋กœ์ ํŠธ์—์„œ๋Š” enum ์ผ€์ด์Šค๋ฅผ ํ™œ์šฉํ•˜์—ฌ item identifier ํƒ€์ž…์„ ๊ตฌํ˜„ํ–ˆ์–ด์š”.

๐Ÿ’ฅ Collection View ๋ ˆ์ด์•„์›ƒ ์งœ๊ธฐ

๐Ÿ“ Please Open
  • ์ €๋Š” ์ƒ๋‹จ์— ์˜ํ™” ์ •๋ณด๋ฅผ ๋„์–ด์ฃผ๋Š” ๋ถ€๋ถ„(์ดํ•˜ detail view)๊ณผ ํ•˜๋‹จ์˜ ์˜ํ™”์ธ ์ •๋ณด๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ๋ถ€๋ถ„(์ดํ•˜ credits view)์„ compositonal layout์œผ๋กœ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•˜์—ฌ ๋„ค ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์„ ๊ณ ๋ฏผํ•ด๋ณด์•˜์–ด์š”.
  1. Detail view ์•„๋ž˜ collection view ๋„ฃ๊ธฐ
  2. Header View๋ฅผ ํ™œ์šฉํ•ด์„œ Detail View๋ฅผ ๊ตฌํ˜„ํ•˜๊ธฐ
  3. Scroll view์•ˆ์— detail view์™€ collection view ๋„ฃ๊ธฐ
  4. Detail view๋ฅผ ์ฒซ ๋ฒˆ์งธ section์—, credit view๋ฅผ ๋‘ ๋ฒˆ์งธ section์œผ๋กœ ๋„ฃ๊ธฐ

1๏ธโƒฃ Detail view ์•„๋ž˜ collection view ๋„ฃ๊ธฐ โŒ

image
  • ๊ฐ€์žฅ ๋จผ์ € ์ƒ๊ฐํ–ˆ๋˜ ๋ฐฉ๋ฒ•์ด์—ˆ๋Š”๋ฐ, ์ด๋ ‡๊ฒŒ ๊ตฌํ˜„ํ•  ๊ฒฝ์šฐ, ์ „์ฒด ํ™”๋ฉด์ด scroll ๋˜์ง€ ์•Š๊ณ  credit view๋งŒ ๊ฐ€๋กœ๋กœ ์Šคํฌ๋กค๋˜๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์–ด์š”. ๋”ฐ๋ผ์„œ ๋‘ ๋ฒˆ์งธ ๋ฐฉ๋ฒ•์„ ์ƒ๊ฐํ•˜๊ฒŒ๋˜์—ˆ์–ด์š”.

2๏ธโƒฃ Header View๋ฅผ ํ™œ์šฉํ•ด์„œ Detail View๋ฅผ ๊ตฌํ˜„ํ•˜๊ธฐ โŒ

image
  • ์ „์ฒด๋ฅผ ํ•˜๋‚˜์˜ section์œผ๋กœ ๊ตฌํ˜„ํ•˜์—ฌ Detail view๋ฅผ headerview๋กœ ๊ตฌํ˜„ํ•˜๋ ค๊ณ  ํ–ˆ์–ด์š”. Detail view ๋ฐ‘์— ๊ฐ๋… ๋ฐ ๋“ฑ์žฅ์ธ๋ฌผ์ด๋ผ๋Š” section header๊ฐ€ ์กด์žฌํ•˜๋ฏ€๋กœ ๋‘ ๊ฐœ์˜ header๋ฅผ ๊ฐ€์ง€๊ฒŒ ๋˜๋Š” ๋ ˆ์ด์•„์›ƒ์ด์—ˆ์–ด์š”. Table view์˜ ๊ฒฝ์šฐ, ๋‘ ๊ฐœ์˜ header view๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ์ด ์ž์—ฐ์Šค๋Ÿฝ์ง€๋งŒ, collection view์˜ ๊ฒฝ์šฐ 2๊ฐœ์˜ header view๋ฅผ ๊ฐ€์ง€๋Š” ๊ฒƒ์ด ์–ด์ƒ‰ํ–ˆ์–ด์š”. ๊ทธ๋ž˜์„œ ๋” ์ข‹์€ ๋ฐฉ์‹์ด ์žˆ์ง€ ์•Š์„๊นŒ ๊ณ ๋ฏผํ•˜๊ฒŒ๋˜์—ˆ์–ด์š”.

3๏ธโƒฃ Scroll view์•ˆ์— detail view์™€ collection view ๋„ฃ๊ธฐ โœ…

image
  • collection view๋Š” scroll view๋ฅผ ์ƒ์† ๋ฐ›๋Š” ํƒ€์ž…์ด์—์š”. ๋”ฐ๋ผ์„œ ์œ„์™€ ๊ฐ™์ด ๊ตฌํ˜„ํ•˜๋ฉด scroll view ์•ˆ์— scroll view๋ฅผ ๋„ฃ๊ฒŒ๋˜์š”. ์• ํ”Œ์˜ HIG ๋ฌธ์„œ๋ฅผ ์ฐธ๊ณ ํ•ด๋ณธ ๊ฒฐ๊ณผ, ์ค‘์ฒฉ scroll view๋ฅผ ์ง€์–‘ํ•˜๋ผ๊ณ  ํ–ˆ์ง€๋งŒ, ์„œ๋กœ ๋‹ค๋ฅธ ๋ฐฉํ–ฅ์˜ scroll view์˜ ์ค‘์ฒฉ์€ ๋ฌธ์ œ๊ฐ€ ๋˜์ง€ ์•Š์Œ์„ ์•Œ๊ฒŒ๋˜์—ˆ์–ด์š”. ๊ทธ๋Ÿฌ๋‚˜, scroll view๋ฅผ ํ™œ์šฉํ•ด์„œ ์ƒ๊ธฐ๋Š” ๊นŒ๋‹ค๋กœ์šด ๋ ˆ์ด์•„์›ƒ ์žก๊ธฐ ๊ณผ์ •์„ ํ”ผํ•˜๊ณ  ์‹ถ์–ด 4๋ฒˆ์งธ ๋ฐฉ๋ฒ•์„ ์ƒ๊ฐํ•˜๊ฒŒ๋์–ด์š”.

Avoid putting a scroll view inside another scroll view with the same orientation. Doing so creates an unpredictable interface thatโ€™s difficult to control. Itโ€™s alright to place a horizontal scroll view inside a vertical scroll view (or vice versa), however.

4๏ธโƒฃ Detail view๋ฅผ ์ฒซ ๋ฒˆ์งธ section์—, credit view๋ฅผ ๋‘ ๋ฒˆ์งธ section์œผ๋กœ ๋„ฃ๊ธฐ โœ…

image
  • ์ฒซ ๋ฒˆ์งธ section์—๋Š” detail view cell ํ•˜๋‚˜๋งŒ์„ ๋ณด์—ฌ์ฃผ๊ณ , ๋‘ ๋ฒˆ์งธ section์—์„œ๋Š” credit view๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„์„ ํ–ˆ์–ด์š”. ํ•ด๋‹น ๋ฐฉ๋ฒ•์„ ํ™œ์šฉํ•˜๋‹ˆ, ์ „์ฒด ํ™”๋ฉด์ด scroll์ด ๋˜๋ฉด์„œ ์›ํ•˜๋Š” layout์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์—ˆ์–ด์š”.

๐Ÿ’ฅ ๊ฐ€๋ณ€์ ์ธ cell ๊ตฌํ˜„ํ•˜๊ธฐ

๐Ÿ“ Please Open
  • feat. cell์•ˆ์— ๋ฒ„ํŠผ ๋„ฃ๊ธฐ
๋””ํ…Œ์ผ ๋ทฐ
  • Detail view์•ˆ์— ๋”๋ณด๊ธฐ ๋ฒ„ํŠผ์„ ์ถ”๊ฐ€ํ•˜์—ฌ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด label์˜ numberOfLines๊ฐ€ ๋ณ€๊ฒฝ๋˜์–ด, cell์˜ height๊ฐ€ ๋Š˜์–ด๋‚˜๋Š” ๋™์ ์ธ cell์„ ๋งŒ๋“œ๋ ค๊ณ  ํ–ˆ์–ด์š”.
  • Delegate ํŒจํ„ด์„ ํ†ตํ•˜์—ฌ button ํƒญ์ด ๋˜๋ฉด, view controller๊ฐ€ label์˜ numberOfLines๋ฅผ ๋ณ€๊ฒฝํ•˜๊ณ , button์˜ ํƒ€์ดํ‹€์„ ๋ณ€๊ฒฝํ•˜๋„๋ก ๊ตฌํ˜„ํ–ˆ์–ด์š”.
// ๐ŸŒˆ delegate ํŒจํ„ด ํ™œ์šฉ!
protocol MovieDetailFirstSectionViewDelegate: AnyObject {
    func movieDetailFirstSectionView(
        _ movieDetailFirstSectionView: MovieDetailFirstSectionView,
        didButtonTapped sender: UIButton
    )
}
  • ๋˜ํ•œ NSCollectionLayoutSection์„ ๊ตฌํ˜„ํ•˜๋Š” ์ฝ”๋“œ์—์„œ itemsize ๋ฐ groupsize๋ฅผ .fractional์ด๋‚˜ .absolute๊ฐ€ ์•„๋‹Œ .estimated์„ ํ†ตํ•˜์—ฌ ๊ตฌํ˜„ํ–ˆ์–ด์š”. ๋˜ํ•œ, view์•ˆ์˜ layout์„ ๊ธฐ์กด์˜ equalTo๊ฐ€ ์•„๋‹Œ greaterThanEqual๋“ฑ์œผ๋กœ ๊ตฌํ˜„์„ ํ•ด์ฃผ์—ˆ์–ด์š”.
// ๐Ÿฟ in MovieDetailViewController
private func createDetailLayout() -> NSCollectionLayoutSection {
        let itemSize = NSCollectionLayoutSize(
            widthDimension: .fractionalWidth(1),
						// ๐Ÿ’ฆ .estimated ํ™œ์šฉ
            heightDimension: .estimated(600)
        )
        let item = NSCollectionLayoutItem(layoutSize: itemSize)
        let groupSize = NSCollectionLayoutSize(
            widthDimension: .fractionalWidth(1),
            heightDimension: .estimated(600)
        )

        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])

        let section = NSCollectionLayoutSection(group: group)

        return section
    }

๐Ÿ’ฅ ๋ฌด๊ฑฐ์›Œ์ง„ view controller ๋œ์–ด๋‚ด๊ธฐ

๐Ÿ“ Please Open
  • MVC ๊ตฌ์กฐ๋กœ ๋กœ์ง์„ ์งœ๋‹ค๋ณด๋‹ˆ, view controller๊ฐ€ ๋งŽ์ด ๋ฌด๊ฑฐ์›Œ์กŒ์–ด์š”. View controller๋Š” collection view๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๋‹ค์–‘ํ•œ ๋กœ์ง๊ณผ ๋‹ค๋ฅธ delegate์„ ์ฑ„ํƒํ•œ ๋‹ค์–‘ํ•œ ๋ฉ”์†Œ๋“œ๊ฐ€ ์กด์žฌํ–ˆ์–ด์š”. ๋˜ํ•œ, Networking์„ ํ•˜๋ฉฐ ํ•„์š”ํ•œ data๋ฅผ ๋ฐ›์•„์˜ค๋Š” ๋กœ์ง ๋˜ํ•œ view controller๊ฐ€ ๊ด€์žฅํ•˜๊ณ  ์žˆ์—ˆ์–ด์š”.
  • ์ €๋Š” ๋„ˆ๋ฌด ์ปค์ง„ view controller์˜ ์—ญํ• ์„ ์ค„์ด๊ธฐ ์œ„ํ•˜์—ฌ controller๋ผ๋Š” ๋ชจ๋ธ ํƒ€์ž…์„ ์ƒ์„ฑํ–ˆ์–ด์š”. ํ•ด๋‹น ํƒ€์ž…์€ MVC ๊ตฌ์กฐ์—์„œ Model์˜ ์—ญํ• ์ด๋ฉฐ ๋ฐ์ดํ„ฐ์™€ ๊ด€๋ จ๋œ ๋‚ด์šฉ ๋ฐ ๋กœ์ง์„ ๊ฐ€์ง€๊ณ  ์žˆ์ง€๋งŒ, UI์™€๋Š” ์ง์ ‘์ ์œผ๋กœ ์—ฐ๊ฒฐ๋˜์ง€ ์•Š์•„์š”. ์ฆ‰, Model ํƒ€์ž…์€ view controller๋ฅผ ๋ชจ๋ฅด์ง€๋งŒ, view controller๋Š” model์„ ๋‚ด๋ถ€ ํ”„๋กœํผํ‹ฐ๋กœ ๊ฐ€์ง€๊ณ  ์žˆ์–ด์š”.
  • Model์€ API ํ†ต์‹ ์„ ํ†ตํ•˜์—ฌ view controller๊ฐ€ collection view์— ๋„์šธ ๋•Œ ํ™œ์šฉํ•˜๋Š” ๋ฐ์ดํ„ฐ ๋ชจ๋ธ ํƒ€์ž…์„ ์ƒ์„ฑํ•ด์š”. ๊ทธ๋ฆฌ๊ณ  model ํƒ€์ž…์„ ์™„์„ฑํ•˜๋ฉด observer ํŒจํ„ด์„ ํ†ตํ•˜์—ฌ view controller์—๊ฒŒ model์ด ์™„์„ฑ๋˜์—ˆ์Œ์„ ์•Œ๋ ค์ฃผ๊ณ , view controller๋Š” ํ•ด๋‹น model์„ ํ†ตํ•˜์—ฌ collection view๋ฅผ ๋„์šฐ๊ฒŒ ๋ผ์š”.

๐Ÿ”Š Controller - MovieDetailController

final class MovieDetailModel {

    private let movie: Movie
		// ๐ŸŒŸ ๋„คํŠธ์›Œํฌ ๊ฐ์ฒด
    private let movieNetworkAPIManager = NetworkAPIManager()
    private let movieNetworkDispatcher = NetworkDispatcher()

   ...

    private func fetchMovieDetails() {
				// ๐Ÿ”ฅ ๋ฐ์ดํ„ฐ  ๋ชจ๋ธ ์ƒ์„ฑ
        guard let movieID = movie.ID else { return }
        let movieDetailEndPoint = MovieDetailsAPIEndPoint(movieCode: movieID)
        let movieCertificationEndPoint = MovieCertificationAPIEndPoint(movieCode: movieID)
        Task {
            do {
               ....
                }
            } catch {
							 ....
            }
            NotificationCenter.default.post(
                name: NSNotification.Name("MovieDetailModelDidFetchCreditData"),
                object: nil
            )
        }
    }

๐Ÿ”Š ViewController - MovieDetailViewController

// ๐Ÿฟ in configureNotificationCenter()
NotificationCenter.default.addObserver(
    self,
    selector: #selector(didFetchMovieCreditsData(_:)),
    name: NSNotification.Name("MovieDetailModelDidFetchCreditData"),
    object: nil
)
// ๐Ÿฟ in MovieDetailViewController
@objc private func didFetchMovieCreditsData(_ notification: Notification) {
        DispatchQueue.main.async {
            self.movieDetailCollectionView.reloadSections([Section.credit.rawValue])
        }
    }

๐Ÿ’ฌ ์ด๋ฅผ ํ†ตํ•ด์„œ, view controller๋Š” view์™€ ๊ด€๋ จ ์—†๋Š” ์ฝ”๋“œ๋ฅผ ๋œ์–ด๋‚ผ ์ˆ˜ ์žˆ์—ˆ๊ณ , view controller์˜ ์—ญํ• ์ด ๋งŽ์•„์ ธ์„œ ๋–จ์–ด์ง€๋˜ ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ์—ˆ์–ด์š”.

๐Ÿ’ฅ View controller์˜ ์ˆœํ™˜ ์ฐธ์กฐ ๋ฌธ์ œ

๐Ÿ“ Please Open
  • ํ•ด๋‹น ์•ฑ์€ NotificationCenter๋ฅผ ํ™œ์šฉํ•˜์—ฌ view๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๋กœ์ง์„ ํ™œ์šฉํ•ด์š”.
  • ๊ทธ๋ฆฌ๊ณ  navigation controller๋ฅผ ํ™œ์šฉํ•˜์—ฌ push๊ฐ€ ๋˜๊ณ  pop์ด๋˜๋ฉด pop๋œ view controller๋Š” ์ž๋™์œผ๋กœ deinit์ด ๋˜๋ฉด์„œ NotificationCenter๊ฐ€ ์ž๋™์œผ๋กœ remove๊ฐ€ ๋ผ์š”.

์•„๋ž˜์˜ ์• ํ”Œ ๊ณต์‹๋ฌธ์„œ๋ฅผ ์ฝ์–ด๋ณด๋ฉด ๊ฐœ๋ฐœ์ž๊ฐ€ ์ง์ ‘์ ์œผ๋กœ NotificationCenter๋ฅผ removeํ•˜์ง€ ์•Š์•„๋„ ๋จ์„ ์•Œ ์ˆ˜ ์žˆ์–ด์š”.

์• ํ”Œ ๊ณต์‹๋ฌธ์„œ

Unregister an observer to stop receiving notifications. To unregister an observer, use removeObserver(:) or removeObserver(:name:object:) with the most specific detail possible. For example, if you used a name and object to register the observer, use the name and object to remove it. If your app targets iOS 9.0 and later or macOS 10.11 and later, you do not need to unregister an observer that you created with this function. If you forget or are unable to remove an observer, the system cleans up the next time it would have posted to it.

  • ๊ทธ๋Ÿฌ๋‚˜, ํ”„๋กœ์ ํŠธ ์ง„ํ–‰ ์ค‘ Notification์ด ์ค‘๋ณต์œผ๋กœ ๋ฐ›์•„์ง€๋Š” ๊ฒฝ์šฐ๋ฅผ ํ™•์ธํ–ˆ์–ด์š”.
    • Home viewcontroller์—์„œ detail viewcontroller๋กœ ๋„˜์–ด๊ฐˆ ๋•Œ, NotificationCenter๋ฅผ addObserver๋ฅผ ํ•˜๋Š” ๋ฐ, detail view controller๊ฐ€ pop๋  ๋•Œ์— Notification center๊ฐ€ remove๊ฐ€ ๋˜์ง€ ์•Š์œผ๋ฏ€๋กœ, ๊ณ„์†ํ•ด์„œ Notification์ด ์ค‘๋ณต์œผ๋กœ ๋ฐ›์•„์ง€๋Š” ๊ฒƒ์ด์—ˆ์–ด์š”.

๐Ÿ–๏ธ ์œ„์™€ ๊ฐ™์€ ์ƒํ™ฉ์ด ์ผ์–ด๋‚˜๋Š” ์ด์œ ๋Š” detail view controller๊ฐ€ pop๋  ๋•Œ, view controller ์† ํด๋กœ์ ธ๋‚˜ delegate ๋ณ€์ˆ˜์— ์˜ํ•ด์„œ ์ˆœํ™˜์ฐธ์กฐ๊ฐ€ ์ผ์–ด๋‚˜ ๋ฉ”๋ชจ๋ฆฌ์—์„œ ํ• ๋‹น ํ•ด์ œ(deinit)์ด ๋˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์ด์—ˆ์–ด์š”.

  • ๋”ฐ๋ผ์„œ ์•„๋ž˜์™€ ๊ฐ™์ด ์•ฝํ•œ ์ฐธ์กฐ(weak)๋ฅผ ํ™œ์šฉํ•˜์—ฌ view controller๊ฐ€ pop๋  ๋•Œ, ๋ฉ”๋ชจ๋ฆฌ์—์„œ ํ• ๋‹น ํ•ด์ œ๊ฐ€ ๋  ์ˆ˜ ์žˆ๊ฒŒ ๋ณ€๊ฒฝํ•ด์ฃผ์—ˆ์–ด์š”.
private func createlayout() -> UICollectionViewCompositionalLayout {
    let layout = UICollectionViewCompositionalLayout { [weak self] sectionIndex, layoutEnvironment in
        let sectionType = Section.allCases[sectionIndex]
        switch sectionType {
        case .detail:
            return self?.createDetailLayout()
        case .credit:
            return self?.createCreditLayout()
        }
    }
    return layout
}

๐Ÿ’ฌ ์ด๋ฅผ ํ†ตํ•ด์„œ, pop๋  ๋•Œ, ์ •์ƒ์ ์œผ๋กœ view controller๊ฐ€ ๋ฉ”๋ชจ๋ฆฌ์—์„œ ํ• ๋‹น ํ•ด์ œ๊ฐ€ ๋˜์–ด Notification์ด ์ค‘๋ณต์œผ๋กœ ๋ฐ›์•„์ง€๋Š” ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์—ˆ์–ด์š”.