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

ネイティブ製ガントチャートUIを作って学ぶUICollectionViewLayoutの威力

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.
Avatar for Yusaku Nishi Yusaku Nishi
September 22, 2025

 ネイティブ製ガントチャートUIを作って学ぶUICollectionViewLayoutの威力

iOSDC Japan 2025 登壇資料
https://fortee.jp/iosdc-japan-2025/proposal/324484f6-bc04-4c3d-ada6-f83bab0dd114

「こんな複雑なUIどうやって実装してるんだろう」と思ったことはありませんか?
UICollectionViewLayoutはその疑問に対する1つの答えとなるかもしれません。

私たち株式会社アンドパッドが提供している施工アプリには「工程表」と呼ばれるガントチャート機能が存在します。ガントチャートとは縦軸に作業項目、横軸に時間をとる棒グラフです。
この機能は従来WebViewで提供してきましたが、いくつかの理由からネイティブUIで作り直すことに決めました。
iOSのデザインにフィットしてスムーズに動作する、複雑なガントチャートUIを作るには…… たどり着いた答えがUICollectionViewLayoutのサブクラス実装による独自レイアウトでした。

本セッションでは以下の内容をお届けします。

- UICollectionView × UICollectionViewLayoutを採用した理由
- 簡単なガントチャートUIを実装しながらUICollectionViewLayoutの設計思想と実装方法を学ぶ
 - 自由自在なレイアウト
 - UI要素のピン留め
 - セクションの開閉
- その他UICollectionViewLayoutの応用例

UIKitはまだまだ現役のフレームワークです。
UICollectionViewLayoutの圧倒的な応用力を知り、「どんなUIでも作れそう」という万能感をぜひ味わってみてください。

Avatar for Yusaku Nishi

Yusaku Nishi

September 22, 2025

More Decks by Yusaku Nishi

Other Decks in Programming

Transcript

  1. © 2025 ANDPAD All Rights Reserved. Confidential Confidential 現場の効率化から経営改善まで一元管理できる クラウド型建設プロジェクト管理サービス

    社 内 社 外 営業 / 監督 / 設計 事務 / 管理職 職人 / 業者 メーカー / 流通 案件管理 資料 工程表 写真 報告 チャット 黒板 図面 受発注 • • • ANDPADとは
  2. © 2025 ANDPAD All Rights Reserved. Confidential Confidential 現場の効率化から経営改善まで一元管理できる クラウド型建設プロジェクト管理サービス

    社 内 社 外 営業 / 監督 / 設計 事務 / 管理職 職人 / 業者 メーカー / 流通 案件管理 資料 工程表 写真 報告 チャット 黒板 図面 受発注 • • • ANDPADとは
  3. - ޻ࣄͷεέδϡʔϧ؅ཧද - ͦͷҰछ͕Ψϯτνϟʔτܗࣜ - ॎ͕࣠࡞ۀ߲໨ɺԣ͕࣠࣌ؒ ༨ஊɿ೔ຊͷݐઃۀքͰ͸ʰόʔνϟʔτ޻ఔදʱ ݐઃۀքͷʰ޻ఔදʱ 現場名 現場住所

    月 日 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 1 2 3 曜 火 水 木 金 土 日 月 火 水 木 金 土 日 月 火 水 木 金 土 日 月 火 水 木 金 土 日 月 火 水 木 金 土 日 営業管理業務 ご近隣着工前挨拶 着工前連絡 基礎工事 基礎工事 木工事 土台・建 防蟻工事 断熱工事 足場 足場掛け 屋根工事 外壁工事 水道工事 仮設水道 電気工事 仮設電気 ガス工事 設備工事 塗装・防水 左官工事 株式会社オクト(開発テスト用) サンプル様邸 工事期間 東京都千代 2024/10 8
  4. ४උ 19 let layout = CompositionalLayout.list( using: .init(appearance: .insetGrouped) )

    let collectionView = UICollectionView( frame: .zero, collectionViewLayout: ) layout lazy var dataSource = UICollectionViewDiffableDataSource< >( collectionView: collectionView ) { [weak self] collectionView, indexPath, itemID in } UICollectionView
  5. let layout = CompositionalLayout.list( using: .init(appearance: .insetGrouped) ) ४උ 20

    layout let collectionView = UICollectionView( frame: .zero, collectionViewLayout: ) layout
  6. UICollectionViewLayoutͷαϒΫϥεΛ࣮૷ 22 final class GanttChartViewLayout: UICollectionViewLayout { // Not implemented

    } let layout = GanttChartViewLayout() let collectionView = UICollectionView( frame: .zero, collectionViewLayout: layout )
  7. UICollectionViewLayoutͷαϒΫϥεΛ࣮૷ 23 final class GanttChartViewLayout: { // Not implemented }

    let layout = GanttChartViewLayout() let collectionView = UICollectionView( frame: .zero, collectionViewLayout: layout )
  8. ࠷௿ݶ࣮૷͕ඞཁͳ΋ͷ - layoutAttributesForElements(in:) 25 class GanttChartViewLayout: UICollectionViewLayout { override func

    layoutAttributesForElements( in rect: CGRect ) -> [UICollectionViewLayoutAttributes]? { } }
  9. ࠷௿ݶ࣮૷͕ඞཁͳ΋ͷ - layoutAttributesForElements(in:) 26 class GanttChartViewLayout: UICollectionViewLayout { override func

    layoutAttributesForElements( in rect: CGRect ) -> [UICollectionViewLayoutAttributes]? { } } let dummy = UICollectionViewLayoutAttributes( forCellWith: IndexPath(item: 0, section: 0) ) dummy.frame = .init(x: 20, y: 40, ) return [dummy]
  10. ࠷௿ݶ࣮૷͕ඞཁͳ΋ͷ - layoutAttributesForElements(in:) 27 class GanttChartViewLayout: UICollectionViewLayout { override func

    layoutAttributesForElements( in rect: CGRect ) -> [UICollectionViewLayoutAttributes]? { } } let dummy = UICollectionViewLayoutAttributes( forCellWith: IndexPath(item: 0, section: 0) ) dummy.frame = .init(x: 20, y: 40, ) return [dummy]
  11. ࠷௿ݶ࣮૷͕ඞཁͳ΋ͷ - layoutAttributesForElements(in:) 28 class GanttChartViewLayout: UICollectionViewLayout { override func

    layoutAttributesForElements( in rect: CGRect ) -> [UICollectionViewLayoutAttributes]? { } } let dummy = UICollectionViewLayoutAttributes( forCellWith: IndexPath(item: 0, section: 0) ) dummy.frame = .init(x: 20, y: 40, ) return [dummy]
  12. ࠷௿ݶ࣮૷͕ඞཁͳ΋ͷ - layoutAttributesForElements(in:) 29 class GanttChartViewLayout: UICollectionViewLayout { override func

    layoutAttributesForElements( in rect: CGRect ) -> [UICollectionViewLayoutAttributes]? { } } let dummy = UICollectionViewLayoutAttributes( forCellWith: IndexPath(item: 0, section: 0) ) dummy.frame = .init(x: 20, y: 40, ) return [dummy]
  13. ࠷௿ݶ࣮૷͕ඞཁͳ΋ͷ - layoutAttributesForElements(in:) 30 class GanttChartViewLayout: UICollectionViewLayout { override func

    layoutAttributesForElements( in rect: CGRect ) -> [UICollectionViewLayoutAttributes]? { } } let dummy = UICollectionViewLayoutAttributes( forCellWith: IndexPath(item: 0, section: 0) ) dummy.frame = .init(x: 20, y: 40, ) return [dummy]
  14. ࠷௿ݶ࣮૷͕ඞཁͳ΋ͷ - layoutAttributesForElements(in:) 31 override func layoutAttributesForElements( ) -> {

    var attributes: [UICollectionViewLayoutAttributes] = [] for section in { for item in { let indexPath = IndexPath(item: item, section: section) let dummy = UICollectionViewLayoutAttributes( forCellWith: indexPath ) dummy.frame = .init( ) attributes.append(dummy) } } return attributes }
  15. ࠷௿ݶ࣮૷͕ඞཁͳ΋ͷ - layoutAttributesForElements(in:) override func layoutAttributesForElements( ) -> { var

    attributes: [UICollectionViewLayoutAttributes] = [] for section in { for item in { let indexPath = IndexPath( ) let dummy = UICollectionViewLayoutAttributes( forCellWith: indexPath ) dummy.frame = .init( ) attributes.append(dummy) } } return attributes } 32
  16. ࠷௿ݶ࣮૷͕ඞཁͳ΋ͷ - collectionViewContentSize 33 final class GanttChartViewLayout: { override var

    collectionViewContentSize: CGSize { CGSize(width: 1500, height: 1000) } }
  17. prepare() - ϨΠΞ΢τଐੑͷΩϟογϡ 36 struct LayoutAttributes { typealias Dictionary =

    [ IndexPath: UICollectionViewLayoutAttributes ] var items = Dictionary() // Ωϟογϡ func forVisibleElements( in rect: CGRect ) -> [UICollectionViewLayoutAttributes] { // rectͱॏͳΔ෼͚ͩऔΓग़ͯ͠ฦ͢ let attributesForVisibleItems = items.values .filter { $0.frame.intersects(rect) } return attributesForVisibleItems } }
  18. final class GanttChartViewLayout: UICollectionViewLayout { private var layoutAttributes = LayoutAttributes()

    override func layoutAttributesForElements( in rect: CGRect ) -> [UICollectionViewLayoutAttributes]? { layoutAttributes.forVisibleElements(in: rect) } } prepare() 37 override func prepare() { }
  19. override func prepare() { } prepare() 38 var attributes: [UICollectionViewLayoutAttributes]

    = [] for section in { for item in { let indexPath = IndexPath(item: item, section: section) let dummy = UICollectionViewLayoutAttributes( forCellWith: indexPath ) dummy.frame = .init( ) layoutAttributes.items[indexPath] = dummy } } return attributes
  20. LayoutReferences - ϨΠΞ΢τͷج४஋ 41 struct LayoutReferences { struct DateReference {

    var cellFrame: CGRect } var dates: [Date: DateReference] = [:] }
  21. LayoutReferences - ϨΠΞ΢τͷج४஋ 42 struct LayoutReferences { struct DateReference {

    var cellFrame: CGRect } var dates: [Date: DateReference] = [:] }
  22. LayoutReferences - ϨΠΞ΢τͷج४஋ 43 struct LayoutReferences { struct DateReference {

    var cellFrame: CGRect } var dates: [Date: DateReference] = [:] } struct WorkItemReference { var cellMinY: CGFloat } var workItems: [WorkItem.ID: WorkItemReference] = [:] var contentSize: CGSize = .zero
  23. ೔෇ηϧͷϑϨʔϜΛܭࢉ 44 extension GanttChartViewLayout.LayoutReferences { var dateCellSize: CGSize { }

    private mutating func prepareDateArea( with itemIDs: [GanttChartView.ItemID] ) { var previousDate: Date? for case .date(let date) in itemIDs { let minX: CGFloat = if let previousDate { dates[previousDate]!.cellFrame.maxX } else { 0.0 } let frame = CGRect( origin: .init(x: minX, y: 0), size: dateCellSize ) dates[date] = .init(cellFrame: frame) previousDate = date } } }
  24. extension GanttChartViewLayout.LayoutReferences { mutating func prepare( with itemIDs: [GanttChartView.ItemID] )

    { } } ࡞ۀ߲໨ηϧͷY࠲ඪΛܭࢉ 45 for case .workItem(let workItemID) in itemIDs { workItems[workItemID] = .init(cellMinY: bottomY) bottomY += workItemCellHeight + verticalSpacing } // ͜ͷ஋Λ౎౓ߋ৽͠ͳ͕Β֤ཁૉͷY࠲ඪΛܭࢉ var bottomY = dateCellSize.height let verticalSpacing = 4.0
  25. extension GanttChartViewLayout.LayoutReferences { mutating func prepare( with itemIDs: [GanttChartView.ItemID] )

    { } } contentSizeΛܭࢉ 46 contentSize = CGSize( width: dates[lastDate]!.cellFrame.maxX, height: bottomY ) for case .workItem(let workItemID) in itemIDs { workItems[workItemID] = .init(cellMinY: bottomY) bottomY += workItemCellHeight + verticalSpacing }
  26. ࡞ۀ߲໨ηϧͷϑϨʔϜΛܭࢉ 47 let schedule = workItem.schedule let minX = dates[schedule.lowerBound]!.cellFrame.minX

    let maxX = dates[schedule.upperBound]!.cellFrame.maxX let workItemCellFrame = CGRect( x: minX, y: workItems[workItem.id]!.cellMinY, width: maxX - minX, height: workItemCellHeight )
  27. ࡞ۀ߲໨ηϧͷϑϨʔϜΛܭࢉ 48 let schedule = workItem.schedule let minX = dates[schedule.lowerBound]!.cellFrame.minX

    let maxX = dates[schedule.upperBound]!.cellFrame.maxX let workItemCellFrame = CGRect( x: minX, y: workItems[workItem.id]!.cellMinY, width: maxX - minX, height: workItemCellHeight )
  28. Supplementary Views - ElementKind - Supplementary Views΍Decoration Views͸ಉ͡IndexPathʹରͯ͠ ෳ਺छྨඥ͚ͮΒΕΔ -

    ͦΕΒΛ۠ผ͢ΔͨΊʹ࢖ΘΕΔͷ͕elementKindͱ͍͏จࣈྻ - จࣈྻͰ͸ѻ͍ͮΒ͍ͨΊElementKindܕΛఆٛ 52 struct ElementKind: { let rawValue: String }
  29. Supplementary Views - ϨΠΞ΢τଐੑͷΩϟογϡ 53 struct LayoutAttributes { var items

    = Dictionary() var supplementaryViews = [ElementKind: Dictionary]() func forVisibleElements( in rect: CGRect ) -> [UICollectionViewLayoutAttributes] { let attributesForVisibleItems = items.values .filter { $0.frame.intersects(rect) } let attributesForVisibleSupplementaryViews = supplementaryViews.values .flatMap(\.values) .filter { $0.frame.intersects(rect) } return attributesForVisibleItems + attributesForVisibleSupplementaryViews } }
  30. Supplementary Views - ϨΠΞ΢τଐੑͷΩϟογϡ 54 let elementKind = ElementKind.workItemGroupHeader let

    header = UICollectionViewLayoutAttributes( forSupplementaryViewOfKind: elementKind.rawValue, with: indexPath ) header.frame = layoutAttributes.supplementaryViews[elementKind, default: [:]] [indexPath] = header
  31. Decoration Views 58 class GanttChartViewLayout: { override init() { super.init()

    register( GanttChartSeparator.self, forDecorationViewOfKind: ) } }
  32. ϔομʔͷϐϯཹΊ 61 final class GanttChartViewLayout: UICollectionViewLayout { override func shouldInvalidateLayout(

    forBoundsChange newBounds: CGRect ) -> Bool { // εΫϩʔϧҐஔ͕มΘΔͨͼʹϨΠΞ΢τΛ࠶ܭࢉͤ͞Δ true } }
  33. ηΫγϣϯͷ։ด 63 let layout = collectionView.collectionViewLayout as! GanttChartViewLayout UIView.animate(withDuration: 0.3)

    { // ηΫγϣϯͷ։ดঢ়ଶΛ੾Γସ͑Δ layout.toggleWorkItemGroupSectionExpansion( for: groupID ) layout.invalidateLayout() }