Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Declarative UICollectionView

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.

Declarative UICollectionView

iOSDC Japan 2018

Avatar for Yosuke Ishikawa

Yosuke Ishikawa

August 31, 2018
Tweet

More Decks by Yosuke Ishikawa

Other Decks in Technology

Transcript

  1. w גࣜձࣾ9$50 w 4XJGU ,PUMJO (P +BWB4DSJQU w "1*,JU %*,JU

    w 4XJGU࣮ફೖ໳ J041SPHSBNNJOH JTILBXB
  2. 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)") } }
  3. 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() } }
  4. 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() } }
  5. 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() } }
  6. 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 ηϧͷ਺͸ʁ  Ͱ͢ʂ
  7. 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
  8. 7FOVF%FUBJM7JFX$POUSPMMFS 6*$PMMFDUJPO7JFX w WFOVF w SFWJFXT w SFMBUFE7FOVFT w OVNCFS0G*UFNT

    w DFMM'PS*UFN"U σʔλม׵ ঢ়ଶΛද͢σʔλ 6*$PMMFDUJPO7JFXͷσʔλ
  9. 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() } }
  10. 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() } } ৚݅෼ذ͕ॏෳ͍ͯ͠Δ
  11. 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Λฦͨ͢Ίͷॲཧ
  12. 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Λฦͨ͢Ίͷॲཧ
  13. 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ͷσʔλ ίϨΫγϣϯΛද͢σʔλ ঢ়ଶΛද͢σʔλ
  14. w ηϧͷछྨͱඥ͚ͮΔ஋ͷϖΞ w 6*$PMMFDUJPO7JFX$FMMͱରͷؔ܎ enum CellDeclaration: Equatable { case outline(Venue)

    case sectionHeader(String) case review(Review) case relatedVenue(Venue) } ੒Ռ෺
  15. 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  
 > ίϨΫγϣϯΛද͢σʔλ ঢ়ଶΛද͢σʔλ
  16. 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)) } } } } ͜ͷؔ਺ΛݺΜͰηϧΛએݴ͍ͯ͘͠
  17. 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)) } } } }
  18. 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 } }
  19. 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ͷσʔλ ίϨΫγϣϯΛද͢σʔλ ঢ়ଶΛද͢σʔλ
  20. σʔλม׵ 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ʹ஋Λ౉͢ॲཧ ڞ௨ͷ࢓૊ΈͰߦ͏
  21. 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 ηϧ΁ͷ஋ͷඥ͚ͮ
  22. protocol BindableNibCell { static var nib: UINib { get }

    static var reuseIdentifier: String { get } 
 associatedtype Value func bind(_ value: Value) } ηϧ΁ͷ஋ͷඥ͚ͮ ηϧͷEFRVFVF ඞཁͳ΋ͷΛηϧʹఏڙͤ͞Δ
  23. struct CellBinder { let nib: UINib let reuseIdentifier: String let

    configureCell: (UICollectionViewCell) -> Void fileprivate init<Cell: BindableNibCell>(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಺Ͱ࢖͑ΔΑ͏ʹܕΛফڈ
  24. class CollectionViewDataSource<CellDeclaration>: 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Λฦ͢
  25. final class VenueDetailViewController: UIViewController { private let dataSource = CollectionViewDataSource<CellDeclaration>

    { 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ΛରԠ͚ͮ
  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ͷσʔλ ίϨΫγϣϯΛද͢σʔλ ঢ়ଶΛද͢σʔλ
  27. 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)) } } } } ... }
  28. 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)) } } } } ... }
  29. final class VenueDetailViewController: UIViewController { ... @IBOutlet private weak var

    collectionView: UICollectionView! private let dataSource = CollectionViewDataSource<CellDeclaration> { 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() } } }
  30. 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 } }
  31. 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), ]) }