$30 off During Our Annual Pro Sale. View Details »

Declarative UICollectionView

Declarative UICollectionView

iOSDC Japan 2018

Yosuke Ishikawa

August 31, 2018
Tweet

More Decks by Yosuke Ishikawa

Other Decks in Technology

Transcript

  1. એݴత
    6*$PMMFDUJPO7JFX
    JTILBXB

    View Slide

  2. w גࣜձࣾ9$50
    w 4XJGU ,PUMJO (P +BWB4DSJQU
    w "1*,JU %*,JU
    w 4XJGU࣮ફೖ໳ J041SPHSBNNJOH
    JTILBXB

    View Slide

  3. View Slide

  4. ࣮૷͠·͢

    View Slide

  5. class VenueDetailViewController: UIViewController {
    var venue: Venue
    var reviews: [Review]
    var relatedVenues: [Venue]
    }

    View Slide

  6. func collectionView(
    _ collectionView: UICollectionView,
    numberOfItemsInSection section: Int) -> Int {
    switch section {
    case 0:
    return 1
    case 1:
    return reviews.count
    case 2:
    return relatedVenues.count
    default:
    fatalError("unknown section \(section)")
    }
    }

    View Slide

  7. func collectionView(
    _ collectionView: UICollectionView,
    cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    switch indexPath.section {
    case 0:
    let cell = collectionView.dequeueReusableCell(
    withReuseIdentifier: "VenueOutline", for: indexPath) as! VenueOutlineCell
    cell.bind(venue)
    return cell
    case 1:
    let cell = collectionView.dequeueReusableCell(
    withReuseIdentifier: "Review", for: indexPath) as! ReviewCell
    cell.bind(reviews[indexPath.item])
    return cell
    case 2:
    let cell = collectionView.dequeueReusableCell(
    withReuseIdentifier: "RelatedVenue", for: indexPath) as! RelatedVenueCell
    cell.bind(relatedVenues[indexPath.item])
    return cell
    default:
    fatalError()
    }
    }

    View Slide

  8. func collectionView(
    _ collectionView: UICollectionView,
    viewForSupplementaryElementOfKind kind: String,
    at indexPath: IndexPath) -> UICollectionReusableView {
    guard kind == UICollectionView.elementKindSectionHeader else {
    fatalError()
    }
    switch indexPath.section {
    case 1:
    let view = collectionView.dequeueReusableCell(
    withReuseIdentifier: "SectionHeader", for: indexPath) as! SectionHeaderCell
    view.bind("Reviews")
    return view
    case 2:
    let view = collectionView.dequeueReusableCell(
    withReuseIdentifier: "SectionHeader", for: indexPath) as! SectionHeaderCell
    view.bind("Related Venues")
    return view
    default:
    fatalError()
    }
    }

    View Slide

  9. func collectionView(
    _ collectionView: UICollectionView,
    layout collectionViewLayout: UICollectionViewLayout,
    referenceSizeForHeaderInSection section: Int) -> CGSize {
    let flowLayout = collectionViewLayout as! UICollectionViewFlowLayout
    switch section {
    case 0:
    return .zero
    case 1:
    return reviews.isEmpty ? .zero : flowLayout.headerReferenceSize
    case 2:
    return relatedVenues.isEmpty ? .zero : flowLayout.headerReferenceSize
    default:
    fatalError()
    }
    }

    View Slide

  10. w ίʔυ͔Β݁Ռ͕૝૾ͮ͠Β͍
    w JOEFYʹґଘͨ͠৚݅෼ذ͕͋Ϳͳ͍༧ײ
    w ৚݅෼ذ͕ॏෳ͍ͯ͠ΔͷͰߋʹ͋Ϳͳ͍

    View Slide

  11. ͳΜͱ͔͠Α͏

    View Slide

  12. ॲཧͷϑϩʔΛ௥ͬͯΈΔ

    View Slide

  13. 7FOVF%FUBJM7JFX$POUSPMMFS 6*$PMMFDUJPO7JFX
    switch section {
    case 0:
    return 1
    case 1:
    return reviews.count
    case 2:
    return relatedVenues.count
    default:
    fatalError()
    }
    w WFOVF
    w SFWJFXT
    w SFMBUFE7FOVFT
    ηϧͷ਺͸ʁ
    Ͱ͢ʂ

    View Slide

  14. 7FOVF%FUBJM7JFX$POUSPMMFS 6*$PMMFDUJPO7JFX
    ൪໨ͷηϧ͸ʁ
    7FOVF0VUMJOF$FMMͰ͢ʂ
    switch indexPath.section {
    case 0:
    let cell = collectionView.dequeueReusableCell(
    withReuseIdentifier: "VenueOutline", for: indexPath) as! VenueOutlineCell
    cell.bind(venue)
    return cell
    case 1:
    let cell = collectionView.dequeueReusableCell(
    withReuseIdentifier: "Review", for: indexPath) as! ReviewCell
    cell.bind(reviews[indexPath.item])
    return cell
    case 2:
    let cell = collectionView.dequeueReusableCell(
    withReuseIdentifier: "RelatedVenue", for: indexPath) as! RelatedVenueCell
    cell.bind(relatedVenues[indexPath.item])
    return cell
    default:
    fatalError()
    }
    w WFOVF
    w SFWJFXT
    w SFMBUFE7FOVFT

    View Slide

  15. าҾ͍ͯଊ͑ͯΈΔ

    View Slide

  16. 7FOVF%FUBJM7JFX$POUSPMMFS 6*$PMMFDUJPO7JFX
    w WFOVF
    w SFWJFXT
    w SFMBUFE7FOVFT
    w OVNCFS0G*UFNT
    w DFMM'PS*UFN"U
    σʔλม׵
    ঢ়ଶΛද͢σʔλ 6*$PMMFDUJPO7JFXͷσʔλ

    View Slide

  17. 7FOVF%FUBJM7JFX$POUSPMMFS 6*$PMMFDUJPO7JFX
    ঢ়ଶΛද͢σʔλ 6*$PMMFDUJPO7JFXͷσʔλ
    w OVNCFS0G*UFNT
    w DFMM'PS*UFN"U
    ϝιου͝ͱʹ
    σʔλม׵
    w WFOVF
    w SFWJFXT
    w SFMBUFE7FOVFT

    View Slide

  18. ʮঢ়ଶΛද͢σʔλʯ͔Β
    ʮ6*$PMMFDUJPO7JFXͷσʔλʯ͸ԕ͍ͷʹ

    ϝιου͝ͱʹΠν͔Βม׵͍ͯ͠Δ

    View Slide

  19. σʔλม׵Λ؍࡯͢Δ

    View Slide

  20. func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    switch section {
    case 0:
    return 1
    case 1:
    return reviews.count
    case 2:
    return relatedVenues.count
    default:
    fatalError("unknown section \(section)")
    }
    }
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    switch indexPath.section {
    case 0:
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "VenueOutline", for: indexPath) as! VenueOutlineCell
    cell.bind(venue)
    return cell
    case 1:
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Review", for: indexPath) as! ReviewCell
    cell.bind(reviews[indexPath.item])
    return cell
    case 2:
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "RelatedVenue", for: indexPath) as! RelatedVenueCell
    cell.bind(relatedVenues[indexPath.item])
    return cell
    default:
    fatalError()
    }
    }

    View Slide

  21. func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    switch section {
    case 0:
    return 1
    case 1:
    return reviews.count
    case 2:
    return relatedVenues.count
    default:
    fatalError("unknown section \(section)")
    }
    }
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    switch indexPath.section {
    case 0:
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "VenueOutline", for: indexPath) as! VenueOutlineCell
    cell.bind(venue)
    return cell
    case 1:
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Review", for: indexPath) as! ReviewCell
    cell.bind(reviews[indexPath.item])
    return cell
    case 2:
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "RelatedVenue", for: indexPath) as! RelatedVenueCell
    cell.bind(relatedVenues[indexPath.item])
    return cell
    default:
    fatalError()
    }
    }
    ৚݅෼ذ͕ॏෳ͍ͯ͠Δ

    View Slide

  22. func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    switch section {
    case 0:
    return 1
    case 1:
    return reviews.count
    case 2:
    return relatedVenues.count
    default:
    fatalError("unknown section \(section)")
    }
    }
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    switch indexPath.section {
    case 0:
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "VenueOutline", for: indexPath) as! VenueOutlineCell
    cell.bind(venue)
    return cell
    case 1:
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Review", for: indexPath) as! ReviewCell
    cell.bind(reviews[indexPath.item])
    return cell
    case 2:
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "RelatedVenue", for: indexPath) as! RelatedVenueCell
    cell.bind(relatedVenues[indexPath.item])
    return cell
    default:
    fatalError()
    }
    }
    ݸͷ7FOVF0VUMJOF$FMMΛฦͨ͢Ίͷॲཧ

    View Slide

  23. func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    switch section {
    case 0:
    return 1
    case 1:
    return reviews.count
    case 2:
    return relatedVenues.count
    default:
    fatalError("unknown section \(section)")
    }
    }
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    switch indexPath.section {
    case 0:
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "VenueOutline", for: indexPath) as! VenueOutlineCell
    cell.bind(venue)
    return cell
    case 1:
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Review", for: indexPath) as! ReviewCell
    cell.bind(reviews[indexPath.item])
    return cell
    case 2:
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "RelatedVenue", for: indexPath) as! RelatedVenueCell
    cell.bind(relatedVenues[indexPath.item])
    return cell
    default:
    fatalError()
    }
    }
    SFWJFXTDPVOUݸͷ3FWJFX$FMMΛฦͨ͢Ίͷॲཧ

    View Slide

  24. w ৚݅෼ذηϧͷछྨΛܾΊΔॲཧ
    w ࣮ߦจ6*$PMMFDUJPO7JFXʹ஋Λ౉͢ॲཧ

    View Slide

  25. ॲཧΛஈ֊ʹ෼͚Δ

    View Slide

  26. 7FOVF%FUBJM7JFX$POUSPMMFS 6*$PMMFDUJPO7JFX
    σʔλม׵
    σʔλม׵
    6*$PMMFDUJPO7JFXʹ஋Λ౉͢ॲཧ
    ηϧͷछྨΛܾΊΔॲཧ
    w WFOVF
    w SFWJFXT
    w SFMBUFE7FOVFT
    w DFMM%FDMBSBUJPOT
    w OVNCFS0G*UFNT
    w DFMM'PS*UFN"U
    6*$PMMFDUJPO7JFXͷσʔλ
    ίϨΫγϣϯΛද͢σʔλ
    ঢ়ଶΛද͢σʔλ

    View Slide

  27. ஈ֊໨ͷॲཧ

    View Slide

  28. w ηϧͷछྨͱඥ͚ͮΔ஋ͷϖΞ
    w 6*$PMMFDUJPO7JFX$FMMͱରͷؔ܎
    enum CellDeclaration: Equatable {
    case outline(Venue)
    case sectionHeader(String)
    case review(Review)
    case relatedVenue(Venue)
    }
    ੒Ռ෺

    View Slide

  29. w WFOVF7FOVF

    w SFWJFXT<>
    w SFMBUFE7FOVFT<>
    w DFMM%FDMBSBUJPOT<

    $FMM%FDMBSBUJPOPVUMJOF 7FOVF


    >
    ίϨΫγϣϯΛද͢σʔλ
    ঢ়ଶΛද͢σʔλ

    View Slide

  30. w WFOVF7FOVF

    w SFWJFXT<

    3FWJFX JE

    3FWJFX JE

    >
    w SFMBUFE7FOVFT<>
    w DFMM%FDMBSBUJPOT<

    $FMM%FDMBSBUJPOPVUMJOF 7FOVF


    $FMM%FDMBSBUJPOTFDUJPO)FBEFS 

    $FMM%FDMBSBUJPOSFWJFX 3FWJFX JE


    $FMM%FDMBSBUJPOSFWJFX 3FWJFX JE


    >
    ίϨΫγϣϯΛද͢σʔλ
    ঢ়ଶΛද͢σʔλ

    View Slide

  31. struct Data: CellsDeclarator {
    var venue: Venue
    var reviews: [Review]
    var relatedVenues: [Venue]
    func declareCells(_ cell: (CellDeclaration) -> Void) {
    cell(.outline(venue))
    if !reviews.isEmpty {
    cell(.sectionHeader("Reviews"))
    for review in reviews {
    cell(.review(review))
    }
    }
    if !relatedVenues.isEmpty {
    cell(.sectionHeader("Related Venues"))
    for relatedVenue in relatedVenues {
    cell(.relatedVenue(relatedVenue))
    }
    }
    }
    }
    ͜ͷؔ਺ΛݺΜͰηϧΛએݴ͍ͯ͘͠

    View Slide

  32. struct Data: CellsDeclarator {
    var venue: Venue
    var reviews: [Review]
    var relatedVenues: [Venue]
    func declareCells(_ cell: (CellDeclaration) -> Void) {
    cell(.outline(venue))
    if !reviews.isEmpty {
    cell(.sectionHeader("Reviews"))
    for review in reviews {
    cell(.review(review))
    }
    }
    if !relatedVenues.isEmpty {
    cell(.sectionHeader("Related Venues"))
    for relatedVenue in relatedVenues {
    cell(.relatedVenue(relatedVenue))
    }
    }
    }
    }

    View Slide

  33. protocol CellsDeclarator {
    associatedtype CellDeclaration
    func declareCells(_ cell: (CellDeclaration) -> Void)
    }
    extension CellsDeclarator {
    var cellDeclarations: [CellDeclaration] {
    var declarations = [] as [CellDeclaration]
    declareCells { declaration in
    declarations.append(declaration)
    }
    return declarations
    }
    }

    View Slide

  34. ஈ֊໨ͷॲཧ

    View Slide

  35. 7FOVF%FUBJM7JFX$POUSPMMFS 6*$PMMFDUJPO7JFX
    σʔλม׵
    σʔλม׵
    6*$PMMFDUJPO7JFXʹ஋Λ౉͢ॲཧ
    ڞ௨ͷ࢓૊ΈͰߦ͏

    ηϧͷछྨΛܾΊΔॲཧ
    ը໘͝ͱʹߦ͏

    w WFOVF
    w SFWJFXT
    w SFMBUFE7FOVFT
    w DFMM%FDMBSBUJPOT
    w OVNCFS0G*UFNT
    w DFMM'PS*UFN"U
    6*$PMMFDUJPO7JFXͷσʔλ
    ίϨΫγϣϯΛද͢σʔλ
    ঢ়ଶΛද͢σʔλ

    View Slide

  36. σʔλม׵
    w OVNCFS0G*UFNT
    w DFMM'PS*UFN"U<>
    w DFMM%FDMBSBUJPOT<

    $FMM%FDMBSBUJPOPVUMJOF 7FOVF


    $FMM%FDMBSBUJPOTFDUJPO)FBEFS 

    $FMM%FDMBSBUJPOSFWJFX *UFN JE


    $FMM%FDMBSBUJPOSFWJFX *UFN JE


    >
    ίϨΫγϣϯΛද͢σʔλ 6*$PMMFDUJPO7JFXͷσʔλ
    6*$PMMFDUJPO7JFXʹ஋Λ౉͢ॲཧ
    ڞ௨ͷ࢓૊ΈͰߦ͏

    View Slide

  37. 6*$PMMFDUJPO7JFX$FMMΛ
    ฦͨ͢Ίʹඞཁͳ͜ͱ͸ʁ

    View Slide

  38. func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    switch indexPath.section {
    case 0:
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "VenueOutline", for: indexPath) as! VenueOutlineCell
    cell.bind(venue)
    return cell
    case 1:
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Review", for: indexPath) as! ReviewCell
    cell.bind(reviews[indexPath.item])
    return cell
    case 2:
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "RelatedVenue", for: indexPath) as! RelatedVenueCell
    cell.bind(relatedVenues[indexPath.item])
    return cell
    default:
    fatalError()
    }
    }
    ηϧͷEFRVFVF
    ηϧ΁ͷ஋ͷඥ͚ͮ

    View Slide

  39. protocol BindableNibCell {
    static var nib: UINib { get }
    static var reuseIdentifier: String { get }

    associatedtype Value
    func bind(_ value: Value)
    }
    ηϧ΁ͷ஋ͷඥ͚ͮ
    ηϧͷEFRVFVF
    ඞཁͳ΋ͷΛηϧʹఏڙͤ͞Δ

    View Slide

  40. struct CellBinder {
    let nib: UINib
    let reuseIdentifier: String
    let configureCell: (UICollectionViewCell) -> Void
    fileprivate init(cellType: Cell.Type, value: Cell.Value) {
    self.nib = cellType.nib
    self.reuseIdentifier = cellType.reuseIdentifier
    self.configureCell = { cell in
    guard let cell = cell as? Cell else {
    fatalError("Could not cast UICollectionView cell to \(Cell.self)")
    }
    cell.bind(value)
    }
    }
    }
    extension BindableNibCell {
    static func makeBinder(with value: Value) -> CellBinder {
    return CellBinder(cellType: Self.self, value: value)
    }
    }
    ηϧ΁ͷ஋ͷඥ͚ͮ
    ηϧͷEFRVFVF
    DFMM'PS*UFN"U಺Ͱ࢖͑ΔΑ͏ʹܕΛফڈ

    View Slide

  41. ͜ΕΛ࢖ͬͯ6*$PMMFDUJPO7JFX%BUB4PVSDFΛ࣮૷

    View Slide

  42. class CollectionViewDataSource: NSObject, UICollectionViewDataSource {
    var cellDeclarations = [] as [CellDeclaration]
    private var registeredReuseIdentifiers = [] as [String]
    private let binderFromDeclaration: (CellDeclaration) -> CellBinder
    init(binderFromDeclaration: @escaping (CellDeclaration) -> CellBinder) {
    self.binderFromDeclaration = binderFromDeclaration
    super.init()
    }
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return cellDeclarations.count
    }
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cellBinder = binderFromDeclaration(cellDeclarations[indexPath.item])
    if !registeredReuseIdentifiers.contains(cellBinder.reuseIdentifier) {
    collectionView.register(cellBinder.nib, forCellWithReuseIdentifier: cellBinder.reuseIdentifier)
    registeredReuseIdentifiers.append(cellBinder.reuseIdentifier)
    }
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellBinder.reuseIdentifier, for: indexPath)
    cellBinder.configureCell(cell)
    return cell
    }
    }
    $FMM%FDMBSBUJPOͱ$FMM#JOEFSͷϚοϐϯάΛҾ਺ʹऔΔ
    $FMM%FDMBSBUJPOΛ$FMM#JOEFSΛม׵ͯ͠6*$PMMFDUJPO7JFX$FMMΛฦ͢

    View Slide

  43. ͜ΕΛ7FOVF%FUBJM7JFX$POUSPMMFSͰ
    ΠϯελϯεԽͯ͠࢖͏

    View Slide

  44. final class VenueDetailViewController: UIViewController {
    private let dataSource = CollectionViewDataSource { cellDeclaration in
    switch cellDeclaration {
    case .outline(let venue):
    return VenueOutlineCell.makeBinder(with: venue)
    case .sectionHeader(let title):
    return SectionHeaderCell.makeBinder(with: title)
    case .review(let review):
    return ReviewCell.makeBinder(with: review)
    case .relatedVenue(let venue):
    return RelatedVenueCell.makeBinder(with: venue)
    }
    }
    ...
    }
    $FMM%FDMBSBUJPOͱ$FMM#JOEFSΛରԠ͚ͮ

    View Slide

  45. ׬੒

    View Slide

  46. 7FOVF%FUBJM7JFX$POUSPMMFS 6*$PMMFDUJPO7JFX
    σʔλม׵
    σʔλม׵
    6*$PMMFDUJPO7JFXʹ஋Λ౉͢ॲཧ
    ڞ௨ͷ࢓૊ΈͰߦ͏

    ηϧͷछྨΛܾΊΔॲཧ
    ը໘͝ͱʹߦ͏

    w WFOVF
    w SFWJFXT
    w SFMBUFE7FOVFT
    w DFMM%FDMBSBUJPOT
    w OVNCFS0G*UFNT
    w DFMM'PS*UFN"U
    6*$PMMFDUJPO7JFXͷσʔλ
    ίϨΫγϣϯΛද͢σʔλ
    ঢ়ଶΛද͢σʔλ

    View Slide

  47. View Slide

  48. ;Γ͔͑Γ

    View Slide

  49. final class VenueDetailViewController: UIViewController {
    enum CellDeclaration: Equatable {
    case outline(Venue)
    case sectionHeader(String)
    case review(Review)
    case relatedVenue(Venue)
    }
    struct Data: CellsDeclarator {
    var venue: Venue
    var reviews: [Review]
    var relatedVenues: [Venue]
    func declareCells(_ cell: (CellDeclaration) -> Void) {
    cell(.outline(venue))
    if !reviews.isEmpty {
    cell(.sectionHeader("Reviews"))
    for review in reviews {
    cell(.review(review))
    }
    }
    if !relatedVenues.isEmpty {
    cell(.sectionHeader("Related Venues"))
    for relatedVenue in relatedVenues {
    cell(.relatedVenue(relatedVenue))
    }
    }
    }
    }
    ...
    }

    View Slide

  50. final class VenueDetailViewController: UIViewController {
    enum CellDeclaration: Equatable {
    case outline(Venue)
    case sectionHeader(String)
    case review(Review)
    case relatedVenue(Venue)
    }
    struct Data: CellsDeclarator {
    var venue: Venue
    var reviews: [Review]
    var relatedVenues: [Venue]
    func declareCells(_ cell: (CellDeclaration) -> Void) {
    cell(.outline(venue))
    if !reviews.isEmpty {
    cell(.sectionHeader("Reviews"))
    for review in reviews {
    cell(.review(review))
    }
    }
    if !relatedVenues.isEmpty {
    cell(.sectionHeader("Related Venues"))
    for relatedVenue in relatedVenues {
    cell(.relatedVenue(relatedVenue))
    }
    }
    }
    }
    ...
    }

    View Slide

  51. final class VenueDetailViewController: UIViewController {
    ...
    @IBOutlet private weak var collectionView: UICollectionView!
    private let dataSource = CollectionViewDataSource { cellDeclaration in
    switch cellDeclaration {
    case .outline(let venue):
    return VenueOutlineCell.makeBinder(with: venue)
    case .sectionHeader(let title):
    return SectionHeaderCell.makeBinder(with: title)
    case .review(let review):
    return ReviewCell.makeBinder(with: review)
    case .relatedVenue(let venue):
    return RelatedVenueCell.makeBinder(with: venue)
    }
    }
    private var data: Data! {
    didSet {
    dataSource.cellDeclarations = data.cellDeclarations
    collectionView.reloadData()
    }
    }
    }

    View Slide

  52. w ηϧͷએݴ͸දࣔ݁Ռͱಉ͡ߏ଄ͳͷͰಡΈ΍͍͢
    w ॳݟͰ͸Ͳ͏΍ͬͯಈ͘ͷ͔ཧղ͢Δͷʹֻ͕͔࣌ؒΔ
    w ௕ظతʹ͸ಡΈ΍͢͞ͷϝϦοτ͕উͪͦ͏
    Մಡੑ

    View Slide

  53. ࣮૷ίετ
    w ηϧͷએݴ͸දࣔ݁Ռͱಉ͡ߏ଄ͳͷͰॻ͖΍͍͢
    w ίʔυͷ૯ྔ΋ݮͬͨ
    w ॏෳͨ͠৚݅෼ذΛഉআͰ͖ͨͷͰϛεͮ͠Β͘ͳͬͨ
    w ૝ఆ͍ͯ͠ͳ͍࢖͍ํͰ͸ίετ্͕͕ΔՄೳੑ͕͋Δ

    View Slide

  54. w ηϧͷએݴͷςετ͕Մೳʹͳͬͨ
    w ैདྷ͸Ұؾʹஈ֊ͷॲཧΛ͍ͯͨ͠ͷͰ೉͔ͬͨ͠
    ςελϏϦςΟ

    View Slide

  55. final class VenueDetailViewController: UIViewController {
    enum CellDeclaration: Equatable {
    case outline(Venue)
    case sectionHeader(String)
    case review(Review)
    case relatedVenue(Venue)
    }
    struct Data: CellsDeclarator {
    var venue: Venue
    var reviews: [Review]
    var relatedVenues: [Venue]
    func declareCells(_ cell: (CellDeclaration) -> Void) {
    cell(.outline(venue))
    if !reviews.isEmpty {
    cell(.sectionHeader("Reviews"))
    for review in reviews {
    cell(.review(review))
    }
    }
    if !relatedVenues.isEmpty {
    cell(.sectionHeader("Related Venues"))
    for relatedVenue in relatedVenues {
    cell(.relatedVenue(relatedVenue))
    }
    }
    }
    }
    ...
    }
    ͜͜ͷ݁Ռͷ<$FMM%FDMBSBUJPO>͸ςετՄೳ
    extension CellsDeclarator {
    var cellDeclarations: [CellDeclaration] {
    var declarations = [] as [CellDeclaration]
    declareCells { declaration in
    declarations.append(declaration)
    }
    return declarations
    }
    }

    View Slide

  56. func testEmptyRelatedVenues() {
    let venue = Venue(photo: nil, name: "Kaminarimon")
    let review1 = Review(authorImage: nil, authorName: "Yosuke Ishikawa", body: "Foo")
    let review2 = Review(authorImage: nil, authorName: "Masatake Yamoto", body: "Bar")
    let data = VenueDetailViewController.Data(
    venue: venue,
    reviews: [
    review1,
    review2,
    ],
    relatedVenues: [])
    XCTAssertEqual(data.cellDeclarations, [
    .outline(venue),
    .sectionHeader("Reviews"),
    .review(review1),
    .review(review2),
    ])
    }

    View Slide

  57. ·ͱΊ

    View Slide