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

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

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
Tweet

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() }