├── .gitignore ├── CollectionViewLayoutPatternSample.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── CollectionViewLayoutPatternSample ├── AppDelegate.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Extension │ └── UICollectionViewExtension.swift ├── Info.plist ├── SceneDelegate.swift ├── TopViewController.swift └── View │ ├── Cell.swift │ ├── Detail │ ├── DescriptionCell.swift │ ├── DetailHeaderView.swift │ ├── DetailSectionModel.swift │ ├── DetailSectionProvider.swift │ ├── DetailSectionProvider │ │ ├── DescriptionSectionProvider.swift │ │ ├── FeatureSectionProvider.swift │ │ ├── MainSectionProvider.swift │ │ └── PurchaseSectionProvider.swift │ └── DetailViewController.swift │ ├── Grid │ ├── GridSectionProvider.swift │ └── GridViewController.swift │ ├── Item.swift │ ├── List │ └── ListViewController.swift │ ├── Mosaic │ ├── MosaicSectionProvider.swift │ └── MosaicViewController.swift │ └── TopAligned │ ├── MultiLineTextCell.swift │ ├── TopAlignedCollectionViewFlowLayout.swift │ └── TopAlignedMultiHeightItemViewController.swift ├── Images ├── data_flow.png ├── detail_layout.png ├── grid_layout.png ├── list_layout.png ├── mosaic_layout.png └── top_aligned_layout.png ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | ### macOS ### 2 | *.DS_Store 3 | 4 | ### Swift ### 5 | # Xcode 6 | # 7 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 8 | 9 | ## Build generated 10 | DerivedData/ 11 | 12 | ## Various settings 13 | *.pbxuser 14 | !default.pbxuser 15 | *.mode1v3 16 | !default.mode1v3 17 | *.mode2v3 18 | !default.mode2v3 19 | *.perspectivev3 20 | !default.perspectivev3 21 | xcuserdata/ 22 | 23 | # Project 24 | # *.xcodeproj 25 | # *.xcworkspace 26 | Configs/user.xcconfig 27 | -------------------------------------------------------------------------------- /CollectionViewLayoutPatternSample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1E13EACB26D7F87F00727846 /* DetailHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E13EACA26D7F87F00727846 /* DetailHeaderView.swift */; }; 11 | 1E13EACD26D8042800727846 /* DescriptionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E13EACC26D8042800727846 /* DescriptionCell.swift */; }; 12 | 1E17798326D1217600827634 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E17798226D1217600827634 /* AppDelegate.swift */; }; 13 | 1E17798526D1217600827634 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E17798426D1217600827634 /* SceneDelegate.swift */; }; 14 | 1E17798726D1217600827634 /* TopViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E17798626D1217600827634 /* TopViewController.swift */; }; 15 | 1E17798A26D1217600827634 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1E17798826D1217600827634 /* Main.storyboard */; }; 16 | 1E17798C26D1217700827634 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1E17798B26D1217700827634 /* Assets.xcassets */; }; 17 | 1E17798F26D1217700827634 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1E17798D26D1217700827634 /* LaunchScreen.storyboard */; }; 18 | 1E17799826D122DE00827634 /* UICollectionViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E17799726D122DE00827634 /* UICollectionViewExtension.swift */; }; 19 | 1E17799B26D122FA00827634 /* Cell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E17799A26D122FA00827634 /* Cell.swift */; }; 20 | 1E1779A226D123E900827634 /* ListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E1779A126D123E900827634 /* ListViewController.swift */; }; 21 | 1E1779A626D12FD100827634 /* GridViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E1779A526D12FD100827634 /* GridViewController.swift */; }; 22 | 1E22F99E26DA494300E73866 /* DetailSectionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E22F99D26DA494300E73866 /* DetailSectionModel.swift */; }; 23 | 1E48F9A326D2A0FA00C94C0E /* PurchaseSectionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E48F9A226D2A0FA00C94C0E /* PurchaseSectionProvider.swift */; }; 24 | 1E51635726D222B0002F81BA /* GridSectionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E51635626D222B0002F81BA /* GridSectionProvider.swift */; }; 25 | 1E51635926D23757002F81BA /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E51635826D23757002F81BA /* Item.swift */; }; 26 | 1E51635B26D23D83002F81BA /* MosaicViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E51635A26D23D83002F81BA /* MosaicViewController.swift */; }; 27 | 1E51635D26D23E83002F81BA /* DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E51635C26D23E83002F81BA /* DetailViewController.swift */; }; 28 | 1E51635F26D244EC002F81BA /* MosaicSectionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E51635E26D244EC002F81BA /* MosaicSectionProvider.swift */; }; 29 | 1E5BD7AD26D6893100BDB6A0 /* TopAlignedCollectionViewFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E5BD7AC26D6893100BDB6A0 /* TopAlignedCollectionViewFlowLayout.swift */; }; 30 | 1EAAC0AA26D4CF440013911A /* MultiLineTextCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EAAC0A926D4CF440013911A /* MultiLineTextCell.swift */; }; 31 | 1EAAC0AC26D4D1D00013911A /* TopAlignedMultiHeightItemViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EAAC0AB26D4D1D00013911A /* TopAlignedMultiHeightItemViewController.swift */; }; 32 | 1EB008FD26D3D04D00585DB1 /* FeatureSectionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EB008FC26D3D04D00585DB1 /* FeatureSectionProvider.swift */; }; 33 | 1EB008FF26D3E29900585DB1 /* DetailSectionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EB008FE26D3E29900585DB1 /* DetailSectionProvider.swift */; }; 34 | 1EB0090126D3E76200585DB1 /* DescriptionSectionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EB0090026D3E76200585DB1 /* DescriptionSectionProvider.swift */; }; 35 | 1ED6994426D282AF00B85088 /* MainSectionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED6994326D282AF00B85088 /* MainSectionProvider.swift */; }; 36 | /* End PBXBuildFile section */ 37 | 38 | /* Begin PBXFileReference section */ 39 | 1E13EACA26D7F87F00727846 /* DetailHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailHeaderView.swift; sourceTree = ""; }; 40 | 1E13EACC26D8042800727846 /* DescriptionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DescriptionCell.swift; sourceTree = ""; }; 41 | 1E17797F26D1217600827634 /* CollectionViewLayoutPatternSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CollectionViewLayoutPatternSample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 42 | 1E17798226D1217600827634 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 43 | 1E17798426D1217600827634 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 44 | 1E17798626D1217600827634 /* TopViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopViewController.swift; sourceTree = ""; }; 45 | 1E17798926D1217600827634 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 46 | 1E17798B26D1217700827634 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 47 | 1E17798E26D1217700827634 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 48 | 1E17799026D1217700827634 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 49 | 1E17799726D122DE00827634 /* UICollectionViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UICollectionViewExtension.swift; sourceTree = ""; }; 50 | 1E17799A26D122FA00827634 /* Cell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cell.swift; sourceTree = ""; }; 51 | 1E1779A126D123E900827634 /* ListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListViewController.swift; sourceTree = ""; }; 52 | 1E1779A526D12FD100827634 /* GridViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridViewController.swift; sourceTree = ""; }; 53 | 1E22F99D26DA494300E73866 /* DetailSectionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailSectionModel.swift; sourceTree = ""; }; 54 | 1E48F9A226D2A0FA00C94C0E /* PurchaseSectionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseSectionProvider.swift; sourceTree = ""; }; 55 | 1E51635626D222B0002F81BA /* GridSectionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridSectionProvider.swift; sourceTree = ""; }; 56 | 1E51635826D23757002F81BA /* Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = ""; }; 57 | 1E51635A26D23D83002F81BA /* MosaicViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MosaicViewController.swift; sourceTree = ""; }; 58 | 1E51635C26D23E83002F81BA /* DetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailViewController.swift; sourceTree = ""; }; 59 | 1E51635E26D244EC002F81BA /* MosaicSectionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MosaicSectionProvider.swift; sourceTree = ""; }; 60 | 1E5BD7AC26D6893100BDB6A0 /* TopAlignedCollectionViewFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopAlignedCollectionViewFlowLayout.swift; sourceTree = ""; }; 61 | 1EAAC0A926D4CF440013911A /* MultiLineTextCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiLineTextCell.swift; sourceTree = ""; }; 62 | 1EAAC0AB26D4D1D00013911A /* TopAlignedMultiHeightItemViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopAlignedMultiHeightItemViewController.swift; sourceTree = ""; }; 63 | 1EB008FC26D3D04D00585DB1 /* FeatureSectionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureSectionProvider.swift; sourceTree = ""; }; 64 | 1EB008FE26D3E29900585DB1 /* DetailSectionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailSectionProvider.swift; sourceTree = ""; }; 65 | 1EB0090026D3E76200585DB1 /* DescriptionSectionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DescriptionSectionProvider.swift; sourceTree = ""; }; 66 | 1ED6994326D282AF00B85088 /* MainSectionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSectionProvider.swift; sourceTree = ""; }; 67 | /* End PBXFileReference section */ 68 | 69 | /* Begin PBXFrameworksBuildPhase section */ 70 | 1E17797C26D1217600827634 /* Frameworks */ = { 71 | isa = PBXFrameworksBuildPhase; 72 | buildActionMask = 2147483647; 73 | files = ( 74 | ); 75 | runOnlyForDeploymentPostprocessing = 0; 76 | }; 77 | /* End PBXFrameworksBuildPhase section */ 78 | 79 | /* Begin PBXGroup section */ 80 | 1E17797626D1217600827634 = { 81 | isa = PBXGroup; 82 | children = ( 83 | 1E17798126D1217600827634 /* CollectionViewLayoutPatternSample */, 84 | 1E17798026D1217600827634 /* Products */, 85 | ); 86 | sourceTree = ""; 87 | }; 88 | 1E17798026D1217600827634 /* Products */ = { 89 | isa = PBXGroup; 90 | children = ( 91 | 1E17797F26D1217600827634 /* CollectionViewLayoutPatternSample.app */, 92 | ); 93 | name = Products; 94 | sourceTree = ""; 95 | }; 96 | 1E17798126D1217600827634 /* CollectionViewLayoutPatternSample */ = { 97 | isa = PBXGroup; 98 | children = ( 99 | 1E17799F26D1234300827634 /* View */, 100 | 1E17799626D122CD00827634 /* Extension */, 101 | 1E17798226D1217600827634 /* AppDelegate.swift */, 102 | 1E17798426D1217600827634 /* SceneDelegate.swift */, 103 | 1E17798626D1217600827634 /* TopViewController.swift */, 104 | 1E17798826D1217600827634 /* Main.storyboard */, 105 | 1E17798B26D1217700827634 /* Assets.xcassets */, 106 | 1E17798D26D1217700827634 /* LaunchScreen.storyboard */, 107 | 1E17799026D1217700827634 /* Info.plist */, 108 | ); 109 | path = CollectionViewLayoutPatternSample; 110 | sourceTree = ""; 111 | }; 112 | 1E17799626D122CD00827634 /* Extension */ = { 113 | isa = PBXGroup; 114 | children = ( 115 | 1E17799726D122DE00827634 /* UICollectionViewExtension.swift */, 116 | ); 117 | path = Extension; 118 | sourceTree = ""; 119 | }; 120 | 1E17799F26D1234300827634 /* View */ = { 121 | isa = PBXGroup; 122 | children = ( 123 | 1E442AD226D9065B0020D7C5 /* List */, 124 | 1E442AD126D906430020D7C5 /* Grid */, 125 | 1E442AD326D906730020D7C5 /* Mosaic */, 126 | 1E442AD426D906910020D7C5 /* TopAligned */, 127 | 1E442AD526D906B10020D7C5 /* Detail */, 128 | 1E17799A26D122FA00827634 /* Cell.swift */, 129 | 1E51635826D23757002F81BA /* Item.swift */, 130 | ); 131 | path = View; 132 | sourceTree = ""; 133 | }; 134 | 1E442AD126D906430020D7C5 /* Grid */ = { 135 | isa = PBXGroup; 136 | children = ( 137 | 1E51635626D222B0002F81BA /* GridSectionProvider.swift */, 138 | 1E1779A526D12FD100827634 /* GridViewController.swift */, 139 | ); 140 | path = Grid; 141 | sourceTree = ""; 142 | }; 143 | 1E442AD226D9065B0020D7C5 /* List */ = { 144 | isa = PBXGroup; 145 | children = ( 146 | 1E1779A126D123E900827634 /* ListViewController.swift */, 147 | ); 148 | path = List; 149 | sourceTree = ""; 150 | }; 151 | 1E442AD326D906730020D7C5 /* Mosaic */ = { 152 | isa = PBXGroup; 153 | children = ( 154 | 1E51635E26D244EC002F81BA /* MosaicSectionProvider.swift */, 155 | 1E51635A26D23D83002F81BA /* MosaicViewController.swift */, 156 | ); 157 | path = Mosaic; 158 | sourceTree = ""; 159 | }; 160 | 1E442AD426D906910020D7C5 /* TopAligned */ = { 161 | isa = PBXGroup; 162 | children = ( 163 | 1EAAC0AB26D4D1D00013911A /* TopAlignedMultiHeightItemViewController.swift */, 164 | 1EAAC0A926D4CF440013911A /* MultiLineTextCell.swift */, 165 | 1E5BD7AC26D6893100BDB6A0 /* TopAlignedCollectionViewFlowLayout.swift */, 166 | ); 167 | path = TopAligned; 168 | sourceTree = ""; 169 | }; 170 | 1E442AD526D906B10020D7C5 /* Detail */ = { 171 | isa = PBXGroup; 172 | children = ( 173 | 1E51635C26D23E83002F81BA /* DetailViewController.swift */, 174 | 1E13EACC26D8042800727846 /* DescriptionCell.swift */, 175 | 1E13EACA26D7F87F00727846 /* DetailHeaderView.swift */, 176 | 1E22F99D26DA494300E73866 /* DetailSectionModel.swift */, 177 | 1EB008FE26D3E29900585DB1 /* DetailSectionProvider.swift */, 178 | 1EB008FB26D3D02800585DB1 /* DetailSectionProvider */, 179 | ); 180 | path = Detail; 181 | sourceTree = ""; 182 | }; 183 | 1EB008FB26D3D02800585DB1 /* DetailSectionProvider */ = { 184 | isa = PBXGroup; 185 | children = ( 186 | 1ED6994326D282AF00B85088 /* MainSectionProvider.swift */, 187 | 1E48F9A226D2A0FA00C94C0E /* PurchaseSectionProvider.swift */, 188 | 1EB0090026D3E76200585DB1 /* DescriptionSectionProvider.swift */, 189 | 1EB008FC26D3D04D00585DB1 /* FeatureSectionProvider.swift */, 190 | ); 191 | path = DetailSectionProvider; 192 | sourceTree = ""; 193 | }; 194 | /* End PBXGroup section */ 195 | 196 | /* Begin PBXNativeTarget section */ 197 | 1E17797E26D1217600827634 /* CollectionViewLayoutPatternSample */ = { 198 | isa = PBXNativeTarget; 199 | buildConfigurationList = 1E17799326D1217700827634 /* Build configuration list for PBXNativeTarget "CollectionViewLayoutPatternSample" */; 200 | buildPhases = ( 201 | 1E17797B26D1217600827634 /* Sources */, 202 | 1E17797C26D1217600827634 /* Frameworks */, 203 | 1E17797D26D1217600827634 /* Resources */, 204 | ); 205 | buildRules = ( 206 | ); 207 | dependencies = ( 208 | ); 209 | name = CollectionViewLayoutPatternSample; 210 | productName = CollectionViewLayoutPatternSample; 211 | productReference = 1E17797F26D1217600827634 /* CollectionViewLayoutPatternSample.app */; 212 | productType = "com.apple.product-type.application"; 213 | }; 214 | /* End PBXNativeTarget section */ 215 | 216 | /* Begin PBXProject section */ 217 | 1E17797726D1217600827634 /* Project object */ = { 218 | isa = PBXProject; 219 | attributes = { 220 | LastSwiftUpdateCheck = 1250; 221 | LastUpgradeCheck = 1250; 222 | TargetAttributes = { 223 | 1E17797E26D1217600827634 = { 224 | CreatedOnToolsVersion = 12.5; 225 | }; 226 | }; 227 | }; 228 | buildConfigurationList = 1E17797A26D1217600827634 /* Build configuration list for PBXProject "CollectionViewLayoutPatternSample" */; 229 | compatibilityVersion = "Xcode 9.3"; 230 | developmentRegion = en; 231 | hasScannedForEncodings = 0; 232 | knownRegions = ( 233 | en, 234 | Base, 235 | ); 236 | mainGroup = 1E17797626D1217600827634; 237 | productRefGroup = 1E17798026D1217600827634 /* Products */; 238 | projectDirPath = ""; 239 | projectRoot = ""; 240 | targets = ( 241 | 1E17797E26D1217600827634 /* CollectionViewLayoutPatternSample */, 242 | ); 243 | }; 244 | /* End PBXProject section */ 245 | 246 | /* Begin PBXResourcesBuildPhase section */ 247 | 1E17797D26D1217600827634 /* Resources */ = { 248 | isa = PBXResourcesBuildPhase; 249 | buildActionMask = 2147483647; 250 | files = ( 251 | 1E17798F26D1217700827634 /* LaunchScreen.storyboard in Resources */, 252 | 1E17798C26D1217700827634 /* Assets.xcassets in Resources */, 253 | 1E17798A26D1217600827634 /* Main.storyboard in Resources */, 254 | ); 255 | runOnlyForDeploymentPostprocessing = 0; 256 | }; 257 | /* End PBXResourcesBuildPhase section */ 258 | 259 | /* Begin PBXSourcesBuildPhase section */ 260 | 1E17797B26D1217600827634 /* Sources */ = { 261 | isa = PBXSourcesBuildPhase; 262 | buildActionMask = 2147483647; 263 | files = ( 264 | 1E48F9A326D2A0FA00C94C0E /* PurchaseSectionProvider.swift in Sources */, 265 | 1EB008FF26D3E29900585DB1 /* DetailSectionProvider.swift in Sources */, 266 | 1EAAC0AC26D4D1D00013911A /* TopAlignedMultiHeightItemViewController.swift in Sources */, 267 | 1E13EACB26D7F87F00727846 /* DetailHeaderView.swift in Sources */, 268 | 1E51635D26D23E83002F81BA /* DetailViewController.swift in Sources */, 269 | 1E17799826D122DE00827634 /* UICollectionViewExtension.swift in Sources */, 270 | 1E17798726D1217600827634 /* TopViewController.swift in Sources */, 271 | 1E51635726D222B0002F81BA /* GridSectionProvider.swift in Sources */, 272 | 1E17799B26D122FA00827634 /* Cell.swift in Sources */, 273 | 1E17798326D1217600827634 /* AppDelegate.swift in Sources */, 274 | 1E51635F26D244EC002F81BA /* MosaicSectionProvider.swift in Sources */, 275 | 1E51635926D23757002F81BA /* Item.swift in Sources */, 276 | 1E22F99E26DA494300E73866 /* DetailSectionModel.swift in Sources */, 277 | 1E17798526D1217600827634 /* SceneDelegate.swift in Sources */, 278 | 1E1779A226D123E900827634 /* ListViewController.swift in Sources */, 279 | 1EB0090126D3E76200585DB1 /* DescriptionSectionProvider.swift in Sources */, 280 | 1E51635B26D23D83002F81BA /* MosaicViewController.swift in Sources */, 281 | 1E5BD7AD26D6893100BDB6A0 /* TopAlignedCollectionViewFlowLayout.swift in Sources */, 282 | 1ED6994426D282AF00B85088 /* MainSectionProvider.swift in Sources */, 283 | 1EB008FD26D3D04D00585DB1 /* FeatureSectionProvider.swift in Sources */, 284 | 1EAAC0AA26D4CF440013911A /* MultiLineTextCell.swift in Sources */, 285 | 1E1779A626D12FD100827634 /* GridViewController.swift in Sources */, 286 | 1E13EACD26D8042800727846 /* DescriptionCell.swift in Sources */, 287 | ); 288 | runOnlyForDeploymentPostprocessing = 0; 289 | }; 290 | /* End PBXSourcesBuildPhase section */ 291 | 292 | /* Begin PBXVariantGroup section */ 293 | 1E17798826D1217600827634 /* Main.storyboard */ = { 294 | isa = PBXVariantGroup; 295 | children = ( 296 | 1E17798926D1217600827634 /* Base */, 297 | ); 298 | name = Main.storyboard; 299 | sourceTree = ""; 300 | }; 301 | 1E17798D26D1217700827634 /* LaunchScreen.storyboard */ = { 302 | isa = PBXVariantGroup; 303 | children = ( 304 | 1E17798E26D1217700827634 /* Base */, 305 | ); 306 | name = LaunchScreen.storyboard; 307 | sourceTree = ""; 308 | }; 309 | /* End PBXVariantGroup section */ 310 | 311 | /* Begin XCBuildConfiguration section */ 312 | 1E17799126D1217700827634 /* Debug */ = { 313 | isa = XCBuildConfiguration; 314 | buildSettings = { 315 | ALWAYS_SEARCH_USER_PATHS = NO; 316 | CLANG_ANALYZER_NONNULL = YES; 317 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 318 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 319 | CLANG_CXX_LIBRARY = "libc++"; 320 | CLANG_ENABLE_MODULES = YES; 321 | CLANG_ENABLE_OBJC_ARC = YES; 322 | CLANG_ENABLE_OBJC_WEAK = YES; 323 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 324 | CLANG_WARN_BOOL_CONVERSION = YES; 325 | CLANG_WARN_COMMA = YES; 326 | CLANG_WARN_CONSTANT_CONVERSION = YES; 327 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 328 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 329 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 330 | CLANG_WARN_EMPTY_BODY = YES; 331 | CLANG_WARN_ENUM_CONVERSION = YES; 332 | CLANG_WARN_INFINITE_RECURSION = YES; 333 | CLANG_WARN_INT_CONVERSION = YES; 334 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 335 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 336 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 337 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 338 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 339 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 340 | CLANG_WARN_STRICT_PROTOTYPES = YES; 341 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 342 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 343 | CLANG_WARN_UNREACHABLE_CODE = YES; 344 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 345 | COPY_PHASE_STRIP = NO; 346 | DEBUG_INFORMATION_FORMAT = dwarf; 347 | ENABLE_STRICT_OBJC_MSGSEND = YES; 348 | ENABLE_TESTABILITY = YES; 349 | GCC_C_LANGUAGE_STANDARD = gnu11; 350 | GCC_DYNAMIC_NO_PIC = NO; 351 | GCC_NO_COMMON_BLOCKS = YES; 352 | GCC_OPTIMIZATION_LEVEL = 0; 353 | GCC_PREPROCESSOR_DEFINITIONS = ( 354 | "DEBUG=1", 355 | "$(inherited)", 356 | ); 357 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 358 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 359 | GCC_WARN_UNDECLARED_SELECTOR = YES; 360 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 361 | GCC_WARN_UNUSED_FUNCTION = YES; 362 | GCC_WARN_UNUSED_VARIABLE = YES; 363 | IPHONEOS_DEPLOYMENT_TARGET = 14.5; 364 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 365 | MTL_FAST_MATH = YES; 366 | ONLY_ACTIVE_ARCH = YES; 367 | SDKROOT = iphoneos; 368 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 369 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 370 | }; 371 | name = Debug; 372 | }; 373 | 1E17799226D1217700827634 /* Release */ = { 374 | isa = XCBuildConfiguration; 375 | buildSettings = { 376 | ALWAYS_SEARCH_USER_PATHS = NO; 377 | CLANG_ANALYZER_NONNULL = YES; 378 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 379 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 380 | CLANG_CXX_LIBRARY = "libc++"; 381 | CLANG_ENABLE_MODULES = YES; 382 | CLANG_ENABLE_OBJC_ARC = YES; 383 | CLANG_ENABLE_OBJC_WEAK = YES; 384 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 385 | CLANG_WARN_BOOL_CONVERSION = YES; 386 | CLANG_WARN_COMMA = YES; 387 | CLANG_WARN_CONSTANT_CONVERSION = YES; 388 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 389 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 390 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 391 | CLANG_WARN_EMPTY_BODY = YES; 392 | CLANG_WARN_ENUM_CONVERSION = YES; 393 | CLANG_WARN_INFINITE_RECURSION = YES; 394 | CLANG_WARN_INT_CONVERSION = YES; 395 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 396 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 397 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 398 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 399 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 400 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 401 | CLANG_WARN_STRICT_PROTOTYPES = YES; 402 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 403 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 404 | CLANG_WARN_UNREACHABLE_CODE = YES; 405 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 406 | COPY_PHASE_STRIP = NO; 407 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 408 | ENABLE_NS_ASSERTIONS = NO; 409 | ENABLE_STRICT_OBJC_MSGSEND = YES; 410 | GCC_C_LANGUAGE_STANDARD = gnu11; 411 | GCC_NO_COMMON_BLOCKS = YES; 412 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 413 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 414 | GCC_WARN_UNDECLARED_SELECTOR = YES; 415 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 416 | GCC_WARN_UNUSED_FUNCTION = YES; 417 | GCC_WARN_UNUSED_VARIABLE = YES; 418 | IPHONEOS_DEPLOYMENT_TARGET = 14.5; 419 | MTL_ENABLE_DEBUG_INFO = NO; 420 | MTL_FAST_MATH = YES; 421 | SDKROOT = iphoneos; 422 | SWIFT_COMPILATION_MODE = wholemodule; 423 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 424 | VALIDATE_PRODUCT = YES; 425 | }; 426 | name = Release; 427 | }; 428 | 1E17799426D1217700827634 /* Debug */ = { 429 | isa = XCBuildConfiguration; 430 | buildSettings = { 431 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 432 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 433 | CODE_SIGN_STYLE = Automatic; 434 | DEVELOPMENT_TEAM = SJ4J2CMQ6B; 435 | INFOPLIST_FILE = CollectionViewLayoutPatternSample/Info.plist; 436 | IPHONEOS_DEPLOYMENT_TARGET = 14.1; 437 | LD_RUNPATH_SEARCH_PATHS = ( 438 | "$(inherited)", 439 | "@executable_path/Frameworks", 440 | ); 441 | PRODUCT_BUNDLE_IDENTIFIER = com.github.to4iki.CollectionViewLayoutPatternSample; 442 | PRODUCT_NAME = "$(TARGET_NAME)"; 443 | SWIFT_VERSION = 5.0; 444 | TARGETED_DEVICE_FAMILY = 1; 445 | }; 446 | name = Debug; 447 | }; 448 | 1E17799526D1217700827634 /* Release */ = { 449 | isa = XCBuildConfiguration; 450 | buildSettings = { 451 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 452 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 453 | CODE_SIGN_STYLE = Automatic; 454 | DEVELOPMENT_TEAM = SJ4J2CMQ6B; 455 | INFOPLIST_FILE = CollectionViewLayoutPatternSample/Info.plist; 456 | IPHONEOS_DEPLOYMENT_TARGET = 14.1; 457 | LD_RUNPATH_SEARCH_PATHS = ( 458 | "$(inherited)", 459 | "@executable_path/Frameworks", 460 | ); 461 | PRODUCT_BUNDLE_IDENTIFIER = com.github.to4iki.CollectionViewLayoutPatternSample; 462 | PRODUCT_NAME = "$(TARGET_NAME)"; 463 | SWIFT_VERSION = 5.0; 464 | TARGETED_DEVICE_FAMILY = 1; 465 | }; 466 | name = Release; 467 | }; 468 | /* End XCBuildConfiguration section */ 469 | 470 | /* Begin XCConfigurationList section */ 471 | 1E17797A26D1217600827634 /* Build configuration list for PBXProject "CollectionViewLayoutPatternSample" */ = { 472 | isa = XCConfigurationList; 473 | buildConfigurations = ( 474 | 1E17799126D1217700827634 /* Debug */, 475 | 1E17799226D1217700827634 /* Release */, 476 | ); 477 | defaultConfigurationIsVisible = 0; 478 | defaultConfigurationName = Release; 479 | }; 480 | 1E17799326D1217700827634 /* Build configuration list for PBXNativeTarget "CollectionViewLayoutPatternSample" */ = { 481 | isa = XCConfigurationList; 482 | buildConfigurations = ( 483 | 1E17799426D1217700827634 /* Debug */, 484 | 1E17799526D1217700827634 /* Release */, 485 | ); 486 | defaultConfigurationIsVisible = 0; 487 | defaultConfigurationName = Release; 488 | }; 489 | /* End XCConfigurationList section */ 490 | }; 491 | rootObject = 1E17797726D1217600827634 /* Project object */; 492 | } 493 | -------------------------------------------------------------------------------- /CollectionViewLayoutPatternSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CollectionViewLayoutPatternSample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /CollectionViewLayoutPatternSample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @main 4 | class AppDelegate: UIResponder, UIApplicationDelegate { 5 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 6 | return true 7 | } 8 | 9 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 10 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /CollectionViewLayoutPatternSample/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /CollectionViewLayoutPatternSample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /CollectionViewLayoutPatternSample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /CollectionViewLayoutPatternSample/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /CollectionViewLayoutPatternSample/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 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 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /CollectionViewLayoutPatternSample/Extension/UICollectionViewExtension.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UICollectionView { 4 | enum ElementKind { 5 | case header 6 | case footer 7 | 8 | var rawValue: String { 9 | switch self { 10 | case .header: 11 | return UICollectionView.elementKindSectionHeader 12 | case .footer: 13 | return UICollectionView.elementKindSectionFooter 14 | } 15 | } 16 | } 17 | 18 | func registerCell(type cell: T.Type) { 19 | register(cell.self, forCellWithReuseIdentifier: cell.identifier) 20 | } 21 | 22 | func dequeueReusableCell(type: T.Type, for indexPath: IndexPath) -> T { 23 | dequeueReusableCell(withReuseIdentifier: type.identifier, for: indexPath) as! T 24 | } 25 | 26 | func registerSupplementaryView(type view: T.Type, kind: ElementKind) { 27 | register(view.self, forSupplementaryViewOfKind: kind.rawValue, withReuseIdentifier: view.identifier) 28 | } 29 | 30 | func dequeueReusableSupplementaryView(type view: T.Type, kind: ElementKind, for indexPath: IndexPath) -> T { 31 | dequeueReusableSupplementaryView(ofKind: kind.rawValue, withReuseIdentifier: view.identifier, for: indexPath) as! T 32 | } 33 | } 34 | 35 | // MARK: - Reusable 36 | protocol Reusable: AnyObject { 37 | static var identifier: String { get } 38 | } 39 | 40 | extension Reusable { 41 | static var identifier: String { 42 | String(describing: self) 43 | } 44 | } 45 | 46 | extension UICollectionReusableView: Reusable {} 47 | -------------------------------------------------------------------------------- /CollectionViewLayoutPatternSample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | UISceneStoryboardFile 37 | Main 38 | 39 | 40 | 41 | 42 | UIApplicationSupportsIndirectInputEvents 43 | 44 | UILaunchStoryboardName 45 | LaunchScreen 46 | UIMainStoryboardFile 47 | Main 48 | UIRequiredDeviceCapabilities 49 | 50 | armv7 51 | 52 | UISupportedInterfaceOrientations 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationLandscapeLeft 56 | UIInterfaceOrientationLandscapeRight 57 | 58 | UISupportedInterfaceOrientations~ipad 59 | 60 | UIInterfaceOrientationPortrait 61 | UIInterfaceOrientationPortraitUpsideDown 62 | UIInterfaceOrientationLandscapeLeft 63 | UIInterfaceOrientationLandscapeRight 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /CollectionViewLayoutPatternSample/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 4 | 5 | var window: UIWindow? 6 | 7 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 8 | guard let _ = (scene as? UIWindowScene) else { return } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /CollectionViewLayoutPatternSample/TopViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class TopViewController: UIViewController { 4 | private lazy var collectionView: UICollectionView = { 5 | let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) 6 | collectionView.backgroundColor = .systemBackground 7 | collectionView.delegate = self 8 | return collectionView 9 | }() 10 | 11 | private let collectionViewLayout: UICollectionViewCompositionalLayout = { 12 | var configuration = UICollectionLayoutListConfiguration(appearance: .insetGrouped) 13 | return UICollectionViewCompositionalLayout.list(using: configuration) 14 | }() 15 | 16 | private lazy var dataSource: UICollectionViewDiffableDataSource = { 17 | let cellRegistration = UICollectionView.CellRegistration { (cell, indexPath, item) in 18 | var contentConfiguration = cell.defaultContentConfiguration() 19 | contentConfiguration.text = item.rawValue 20 | cell.contentConfiguration = contentConfiguration 21 | cell.accessories = [.disclosureIndicator()] 22 | } 23 | 24 | let dataSource = UICollectionViewDiffableDataSource( 25 | collectionView: collectionView 26 | ) { (collectionView, indexPath, item) -> UICollectionViewCell? in 27 | collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item) 28 | } 29 | 30 | return dataSource 31 | }() 32 | 33 | private let sectionModels: [SectionModel] = [ 34 | SectionModel(section: .pattern, items: SectionItem.allCases) 35 | ] 36 | 37 | override func viewDidLoad() { 38 | super.viewDidLoad() 39 | 40 | navigationController?.navigationBar.prefersLargeTitles = true 41 | navigationItem.largeTitleDisplayMode = .always 42 | navigationItem.title = "Pattern" 43 | 44 | collectionView.translatesAutoresizingMaskIntoConstraints = false 45 | view.addSubview(collectionView) 46 | NSLayoutConstraint.activate([ 47 | collectionView.topAnchor.constraint(equalTo: view.topAnchor), 48 | collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), 49 | collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), 50 | collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor) 51 | ]) 52 | 53 | applyDataSource() 54 | } 55 | 56 | override func viewWillAppear(_ animated: Bool) { 57 | super.viewWillAppear(animated) 58 | 59 | collectionView.indexPathsForSelectedItems?.forEach { indexPath in 60 | collectionView.deselectItem(at: indexPath, animated: true) 61 | } 62 | } 63 | 64 | private func applyDataSource() { 65 | var snapshot = NSDiffableDataSourceSnapshot() 66 | for sectionModel in sectionModels { 67 | snapshot.appendSections([sectionModel.section]) 68 | snapshot.appendItems(sectionModel.items, toSection: sectionModel.section) 69 | } 70 | dataSource.apply(snapshot, animatingDifferences: false) 71 | } 72 | } 73 | 74 | // MARK: - UICollectionViewDelegate 75 | extension TopViewController: UICollectionViewDelegate { 76 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 77 | switch sectionModels[indexPath.section].items[indexPath.item] { 78 | case .list: 79 | navigationController?.pushViewController(ListViewController(), animated: true) 80 | case .grid: 81 | navigationController?.pushViewController(GridViewController(), animated: true) 82 | case .mosaic: 83 | navigationController?.pushViewController(MosaicViewController(), animated: true) 84 | case .topAligned: 85 | navigationController?.pushViewController(TopAlignedMultiHeightItemViewController(), animated: true) 86 | case .detail: 87 | navigationController?.pushViewController(DetailViewController(), animated: true) 88 | } 89 | } 90 | } 91 | 92 | // MARK: - SectionModel 93 | extension TopViewController { 94 | private struct SectionModel { 95 | var section: Section 96 | var items: [SectionItem] 97 | } 98 | 99 | private enum Section { 100 | case pattern 101 | } 102 | 103 | private enum SectionItem: String, CaseIterable { 104 | case list 105 | case grid 106 | case mosaic 107 | case topAligned = "top aligned multi height item" 108 | case detail 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /CollectionViewLayoutPatternSample/View/Cell.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class Cell: UICollectionViewCell { 4 | private let titleLabel: UILabel = { 5 | let titleLabel = UILabel() 6 | titleLabel.font = .systemFont(ofSize: 17) 7 | titleLabel.textColor = .label 8 | titleLabel.textAlignment = .center 9 | titleLabel.numberOfLines = 1 10 | titleLabel.translatesAutoresizingMaskIntoConstraints = false 11 | return titleLabel 12 | }() 13 | 14 | var title: String? { 15 | didSet { 16 | titleLabel.text = title 17 | } 18 | } 19 | 20 | override init(frame: CGRect) { 21 | super.init(frame: frame) 22 | 23 | contentView.layer.borderWidth = 1 24 | contentView.layer.borderColor = UIColor.label.cgColor 25 | contentView.layer.masksToBounds = true 26 | 27 | contentView.addSubview(titleLabel) 28 | NSLayoutConstraint.activate([ 29 | titleLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), 30 | titleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor) 31 | ]) 32 | } 33 | 34 | @available(*, unavailable) 35 | required init?(coder aDecoder: NSCoder) { 36 | fatalError("init(coder:) has not been implemented") 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /CollectionViewLayoutPatternSample/View/Detail/DescriptionCell.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class DescriptionCell: UICollectionViewCell { 4 | private let textLabel: UILabel = { 5 | let textLabel = UILabel() 6 | textLabel.font = .systemFont(ofSize: 14) 7 | textLabel.textColor = .label 8 | textLabel.textAlignment = .left 9 | textLabel.numberOfLines = 0 10 | textLabel.translatesAutoresizingMaskIntoConstraints = false 11 | return textLabel 12 | }() 13 | 14 | var text: String? { 15 | didSet { 16 | textLabel.text = text 17 | } 18 | } 19 | 20 | override init(frame: CGRect) { 21 | super.init(frame: frame) 22 | 23 | contentView.layer.borderWidth = 1 24 | contentView.layer.borderColor = UIColor.label.cgColor 25 | contentView.layer.masksToBounds = true 26 | 27 | contentView.addSubview(textLabel) 28 | NSLayoutConstraint.activate([ 29 | textLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 4), 30 | textLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), 31 | textLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), 32 | textLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -4) 33 | ]) 34 | } 35 | 36 | @available(*, unavailable) 37 | required init?(coder aDecoder: NSCoder) { 38 | fatalError("init(coder:) has not been implemented") 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /CollectionViewLayoutPatternSample/View/Detail/DetailHeaderView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class DetailHeaderView: UICollectionReusableView { 4 | private let titleLabel: UILabel = { 5 | let titleLabel = UILabel() 6 | titleLabel.font = .boldSystemFont(ofSize: 22) 7 | titleLabel.textColor = .label 8 | titleLabel.textAlignment = .left 9 | titleLabel.translatesAutoresizingMaskIntoConstraints = false 10 | return titleLabel 11 | }() 12 | 13 | var title: String? { 14 | didSet { 15 | titleLabel.text = title 16 | } 17 | } 18 | 19 | override init(frame: CGRect) { 20 | super.init(frame: frame) 21 | 22 | addSubview(titleLabel) 23 | NSLayoutConstraint.activate([ 24 | titleLabel.topAnchor.constraint(equalTo: topAnchor), 25 | titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor), 26 | titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor), 27 | titleLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8) 28 | ]) 29 | } 30 | 31 | @available(*, unavailable) 32 | required init?(coder aDecoder: NSCoder) { 33 | fatalError("init(coder:) has not been implemented") 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /CollectionViewLayoutPatternSample/View/Detail/DetailSectionModel.swift: -------------------------------------------------------------------------------- 1 | struct DetailSectionModel { 2 | var provider: DetailSectionProvider 3 | var section: DetailSection 4 | var items: [DetailSectionItem] 5 | } 6 | 7 | enum DetailSection: Hashable { 8 | case main 9 | case purchase 10 | case description 11 | case feature(Int) 12 | } 13 | 14 | enum DetailSectionItem: Hashable { 15 | case main 16 | case purchase 17 | case description 18 | case feature(Item) 19 | } 20 | -------------------------------------------------------------------------------- /CollectionViewLayoutPatternSample/View/Detail/DetailSectionProvider.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | protocol DetailSectionProvider { 4 | func layoutSection(contentWidth: CGFloat, traitCollection: UITraitCollection) -> NSCollectionLayoutSection 5 | func provideCell(_ collectionView: UICollectionView, indexPath: IndexPath, item: DetailSectionItem) -> UICollectionViewCell 6 | func provideHeaderView(_ collectionView: UICollectionView, indexPath: IndexPath, section: DetailSection) -> UICollectionReusableView? 7 | func provideFooterView(_ collectionView: UICollectionView, indexPath: IndexPath, section: DetailSection) -> UICollectionReusableView? 8 | } 9 | 10 | extension DetailSectionProvider { 11 | func provideHeaderView(_ collectionView: UICollectionView, indexPath: IndexPath, section: DetailSection) -> UICollectionReusableView? { 12 | return nil 13 | } 14 | 15 | func provideFooterView(_ collectionView: UICollectionView, indexPath: IndexPath, section: DetailSection) -> UICollectionReusableView? { 16 | return nil 17 | } 18 | } 19 | 20 | extension DetailSectionProvider { 21 | var sectionInsets: NSDirectionalEdgeInsets { 22 | NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16) 23 | } 24 | 25 | var sectionHeader: NSCollectionLayoutBoundarySupplementaryItem { 26 | let size = NSCollectionLayoutSize( 27 | widthDimension: .fractionalWidth(1.0), 28 | heightDimension: .estimated(30) 29 | ) 30 | return NSCollectionLayoutBoundarySupplementaryItem( 31 | layoutSize: size, 32 | elementKind: UICollectionView.elementKindSectionHeader, 33 | alignment: .top 34 | ) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /CollectionViewLayoutPatternSample/View/Detail/DetailSectionProvider/DescriptionSectionProvider.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | struct DescriptionSectionProvider: DetailSectionProvider { 4 | func layoutSection(contentWidth: CGFloat, traitCollection: UITraitCollection) -> NSCollectionLayoutSection { 5 | let itemSize = NSCollectionLayoutSize( 6 | widthDimension: .fractionalWidth(1), 7 | heightDimension: .estimated(22) // DescriptionCell.textLabel minimum `1` line 8 | ) 9 | let item = NSCollectionLayoutItem(layoutSize: itemSize) 10 | 11 | let groupSize = NSCollectionLayoutSize( 12 | widthDimension: .fractionalWidth(1), 13 | heightDimension: .estimated(22) 14 | ) 15 | let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) 16 | 17 | let section = NSCollectionLayoutSection(group: group) 18 | section.contentInsets = sectionInsets 19 | return section 20 | } 21 | 22 | func provideCell(_ collectionView: UICollectionView, indexPath: IndexPath, item: DetailSectionItem) -> UICollectionViewCell { 23 | let cell = collectionView.dequeueReusableCell(type: DescriptionCell.self, for: indexPath) 24 | cell.text = (0..<20).reduce("") { str, _ in str + "description" + " "} 25 | cell.backgroundColor = .systemGray 26 | return cell 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /CollectionViewLayoutPatternSample/View/Detail/DetailSectionProvider/FeatureSectionProvider.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | struct FeatureSectionProvider: DetailSectionProvider { 4 | func layoutSection(contentWidth: CGFloat, traitCollection: UITraitCollection) -> NSCollectionLayoutSection { 5 | let itemSize = NSCollectionLayoutSize( 6 | widthDimension: .fractionalWidth(1), 7 | heightDimension: .fractionalHeight(1) 8 | ) 9 | let item = NSCollectionLayoutItem(layoutSize: itemSize) 10 | 11 | let groupWidth = (contentWidth - (sectionInsets.leading + sectionInsets.trailing)) / 3 12 | let groupSize = NSCollectionLayoutSize( 13 | widthDimension: .absolute(groupWidth), 14 | heightDimension: .absolute(groupWidth * 4/3) 15 | ) 16 | let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitem: item, count: 1) 17 | group.contentInsets = .init(top: 4, leading: 4, bottom: 4, trailing: 4) 18 | 19 | let section = NSCollectionLayoutSection(group: group) 20 | section.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary 21 | section.contentInsets = sectionInsets 22 | 23 | section.boundarySupplementaryItems = [sectionHeader] 24 | 25 | return section 26 | } 27 | 28 | func provideCell(_ collectionView: UICollectionView, indexPath: IndexPath, item: DetailSectionItem) -> UICollectionViewCell { 29 | guard case .feature(let raw) = item else { 30 | fatalError("Should never be reached") 31 | } 32 | let cell = collectionView.dequeueReusableCell(type: Cell.self, for: indexPath) 33 | cell.title = raw.text 34 | cell.backgroundColor = .systemGreen 35 | return cell 36 | } 37 | 38 | func provideHeaderView(_ collectionView: UICollectionView, indexPath: IndexPath, section: DetailSection) -> UICollectionReusableView? { 39 | guard case .feature(let index) = section else { 40 | fatalError("Should never be reached") 41 | } 42 | let view = collectionView.dequeueReusableSupplementaryView(type: DetailHeaderView.self, kind: .header, for: indexPath) 43 | view.title = "Feature_\(index)" 44 | return view 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /CollectionViewLayoutPatternSample/View/Detail/DetailSectionProvider/MainSectionProvider.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | struct MainSectionProvider: DetailSectionProvider { 4 | func layoutSection(contentWidth: CGFloat, traitCollection: UITraitCollection) -> NSCollectionLayoutSection { 5 | let itemSize = NSCollectionLayoutSize( 6 | widthDimension: .fractionalWidth(1), 7 | heightDimension: .fractionalHeight(1) 8 | ) 9 | let item = NSCollectionLayoutItem(layoutSize: itemSize) 10 | 11 | let groupSize = NSCollectionLayoutSize( 12 | widthDimension: .fractionalWidth(1), 13 | heightDimension: .fractionalWidth(1) 14 | ) 15 | let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) 16 | 17 | let section = NSCollectionLayoutSection(group: group) 18 | section.contentInsets = sectionInsets 19 | return section 20 | } 21 | 22 | func provideCell(_ collectionView: UICollectionView, indexPath: IndexPath, item: DetailSectionItem) -> UICollectionViewCell { 23 | let cell = collectionView.dequeueReusableCell(type: Cell.self, for: indexPath) 24 | cell.title = "main" 25 | cell.backgroundColor = .systemBlue 26 | return cell 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /CollectionViewLayoutPatternSample/View/Detail/DetailSectionProvider/PurchaseSectionProvider.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | struct PurchaseSectionProvider: DetailSectionProvider { 4 | func layoutSection(contentWidth: CGFloat, traitCollection: UITraitCollection) -> NSCollectionLayoutSection { 5 | let itemSize = NSCollectionLayoutSize( 6 | widthDimension: .fractionalWidth(1), 7 | heightDimension: .absolute(60) 8 | ) 9 | let item = NSCollectionLayoutItem(layoutSize: itemSize) 10 | 11 | let groupSize = NSCollectionLayoutSize( 12 | widthDimension: .fractionalWidth(1), 13 | heightDimension: .estimated(64) 14 | ) 15 | let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) 16 | 17 | let section = NSCollectionLayoutSection(group: group) 18 | section.contentInsets = sectionInsets 19 | return section 20 | } 21 | 22 | func provideCell(_ collectionView: UICollectionView, indexPath: IndexPath, item: DetailSectionItem) -> UICollectionViewCell { 23 | let cell = collectionView.dequeueReusableCell(type: Cell.self, for: indexPath) 24 | cell.title = "purchase" 25 | cell.backgroundColor = .systemYellow 26 | return cell 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /CollectionViewLayoutPatternSample/View/Detail/DetailViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class DetailViewController: UIViewController { 4 | private typealias SectionModel = DetailSectionModel 5 | private typealias Section = DetailSection 6 | private typealias SectionItem = DetailSectionItem 7 | 8 | private lazy var collectionView: UICollectionView = { 9 | let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) 10 | collectionView.backgroundColor = .systemBackground 11 | collectionView.registerCell(type: Cell.self) 12 | collectionView.registerCell(type: DescriptionCell.self) 13 | collectionView.registerSupplementaryView(type: DetailHeaderView.self, kind: .header) 14 | return collectionView 15 | }() 16 | 17 | private lazy var collectionViewLayout: UICollectionViewCompositionalLayout = { 18 | let sectionModels = self.sectionModels 19 | 20 | let sectionProvider = { (sectionIndex: Int, environment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in 21 | let provider = sectionModels[sectionIndex].provider 22 | let section = provider.layoutSection( 23 | contentWidth: environment.container.effectiveContentSize.width, 24 | traitCollection: environment.traitCollection 25 | ) 26 | return section 27 | } 28 | 29 | let config = UICollectionViewCompositionalLayoutConfiguration() 30 | config.interSectionSpacing = 16 31 | 32 | let layout = UICollectionViewCompositionalLayout(sectionProvider: sectionProvider, configuration: config) 33 | return layout 34 | }() 35 | 36 | private lazy var dataSource: UICollectionViewDiffableDataSource = { 37 | let dataSource = UICollectionViewDiffableDataSource( 38 | collectionView: collectionView 39 | ) { [weak self] (collectionView, indexPath, item) -> UICollectionViewCell? in 40 | return self?.provideCell(collectionView, indexPath: indexPath, item: item) 41 | } 42 | 43 | dataSource.supplementaryViewProvider = { [weak self] (collectionView, elementKind, indexPath) in 44 | self?.provideSupplementaryView(collectionView, viewForSupplementaryElementOfKind: elementKind, at: indexPath) 45 | } 46 | 47 | return dataSource 48 | }() 49 | 50 | private let sectionModels: [SectionModel] = [ 51 | SectionModel(provider: MainSectionProvider(), section: .main, items: [.main]), 52 | SectionModel(provider: PurchaseSectionProvider(), section: .purchase, items: [.purchase]), 53 | SectionModel(provider: DescriptionSectionProvider(), section: .description, items: [.description]), 54 | SectionModel(provider: FeatureSectionProvider(), section: .feature(0), items: (0..<8).map { .feature(Item(text: "\($0)")) }), 55 | SectionModel(provider: FeatureSectionProvider(), section: .feature(1), items: (0..<8).map { .feature(Item(text: "\($0)")) }) 56 | ] 57 | 58 | override func viewDidLoad() { 59 | super.viewDidLoad() 60 | 61 | navigationController?.navigationBar.prefersLargeTitles = true 62 | navigationItem.largeTitleDisplayMode = .never 63 | navigationItem.title = "Detail" 64 | 65 | collectionView.translatesAutoresizingMaskIntoConstraints = false 66 | view.addSubview(collectionView) 67 | NSLayoutConstraint.activate([ 68 | collectionView.topAnchor.constraint(equalTo: view.topAnchor), 69 | collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), 70 | collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), 71 | collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor) 72 | ]) 73 | 74 | applyDataSource() 75 | } 76 | 77 | private func applyDataSource() { 78 | var snapshot = NSDiffableDataSourceSnapshot() 79 | for sectionModel in sectionModels { 80 | snapshot.appendSections([sectionModel.section]) 81 | snapshot.appendItems(sectionModel.items, toSection: sectionModel.section) 82 | } 83 | dataSource.apply(snapshot, animatingDifferences: false) 84 | } 85 | 86 | private func provideCell(_ collectionView: UICollectionView, indexPath: IndexPath, item: SectionItem) -> UICollectionViewCell { 87 | let sectionProvider = sectionModels[indexPath.section].provider 88 | return sectionProvider.provideCell(collectionView, indexPath: indexPath, item: item) 89 | } 90 | 91 | private func provideSupplementaryView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView? { 92 | let sectionModel = sectionModels[indexPath.section] 93 | 94 | if kind == UICollectionView.elementKindSectionHeader { 95 | return sectionModel.provider.provideHeaderView(collectionView, indexPath: indexPath, section: sectionModel.section) 96 | } else if kind == UICollectionView.elementKindSectionFooter { 97 | return sectionModel.provider.provideFooterView(collectionView, indexPath: indexPath, section: sectionModel.section) 98 | } else { 99 | return nil 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /CollectionViewLayoutPatternSample/View/Grid/GridSectionProvider.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | struct GridSectionProvider { 4 | func layoutSection() -> NSCollectionLayoutSection { 5 | let itemSize = NSCollectionLayoutSize( 6 | widthDimension: .fractionalWidth(1/5), 7 | heightDimension: .fractionalHeight(1.0) 8 | ) 9 | let item = NSCollectionLayoutItem(layoutSize: itemSize) 10 | item.contentInsets = NSDirectionalEdgeInsets(top: 4, leading: 4, bottom: 4, trailing: 4) 11 | 12 | let groupSize = NSCollectionLayoutSize( 13 | widthDimension: .fractionalWidth(1.0), 14 | heightDimension: .fractionalWidth(1/5) 15 | ) 16 | let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) 17 | 18 | let section = NSCollectionLayoutSection(group: group) 19 | section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 4, bottom: 0, trailing: 4) 20 | 21 | return section 22 | } 23 | 24 | func provideCell(_ collectionView: UICollectionView, indexPath: IndexPath, item: Item) -> UICollectionViewCell { 25 | let cell = collectionView.dequeueReusableCell(type: Cell.self, for: indexPath) 26 | cell.title = item.text 27 | cell.backgroundColor = .systemBlue 28 | return cell 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /CollectionViewLayoutPatternSample/View/Grid/GridViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class GridViewController: UIViewController { 4 | private lazy var collectionView: UICollectionView = { 5 | let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) 6 | collectionView.backgroundColor = .systemBackground 7 | collectionView.registerCell(type: Cell.self) 8 | return collectionView 9 | }() 10 | 11 | private lazy var collectionViewLayout: UICollectionViewCompositionalLayout = { 12 | let provider = sectionModel.provider 13 | let layout = UICollectionViewCompositionalLayout(section: provider.layoutSection()) 14 | return layout 15 | }() 16 | 17 | private lazy var dataSource: UICollectionViewDiffableDataSource = { 18 | let dataSource = UICollectionViewDiffableDataSource( 19 | collectionView: collectionView 20 | ) { [weak self] (collectionView, indexPath, item) -> UICollectionViewCell? in 21 | return self?.provideCell(collectionView, indexPath: indexPath, item: item) 22 | } 23 | 24 | return dataSource 25 | }() 26 | 27 | private let sectionModel = SectionModel( 28 | provider: GridSectionProvider(), 29 | section: .main, 30 | items: (0...50).map { Item(text: $0.description) } 31 | ) 32 | 33 | override func viewDidLoad() { 34 | super.viewDidLoad() 35 | 36 | navigationController?.navigationBar.prefersLargeTitles = true 37 | navigationItem.largeTitleDisplayMode = .never 38 | navigationItem.title = "Grid" 39 | 40 | collectionView.translatesAutoresizingMaskIntoConstraints = false 41 | view.addSubview(collectionView) 42 | NSLayoutConstraint.activate([ 43 | collectionView.topAnchor.constraint(equalTo: view.topAnchor), 44 | collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), 45 | collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), 46 | collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor) 47 | ]) 48 | 49 | applyDataSource() 50 | } 51 | 52 | private func applyDataSource() { 53 | var snapshot = NSDiffableDataSourceSnapshot() 54 | snapshot.appendSections([sectionModel.section]) 55 | snapshot.appendItems(sectionModel.items) 56 | dataSource.apply(snapshot, animatingDifferences: false) 57 | } 58 | 59 | private func provideCell(_ collectionView: UICollectionView, indexPath: IndexPath, item: Item) -> UICollectionViewCell { 60 | let provider = sectionModel.provider 61 | return provider.provideCell(collectionView, indexPath: indexPath, item: item) 62 | } 63 | } 64 | 65 | // MARK: - SectionModel 66 | extension GridViewController { 67 | private struct SectionModel { 68 | var provider: GridSectionProvider 69 | var section: Section 70 | var items: [Item] 71 | } 72 | 73 | private enum Section { 74 | case main 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /CollectionViewLayoutPatternSample/View/Item.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct Item: Hashable { 4 | var id: String 5 | var text: String 6 | 7 | init(text: String) { 8 | self.id = UUID().uuidString 9 | self.text = text 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /CollectionViewLayoutPatternSample/View/List/ListViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class ListViewController: UIViewController { 4 | private lazy var collectionView: UICollectionView = { 5 | let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) 6 | collectionView.backgroundColor = .systemBackground 7 | collectionView.translatesAutoresizingMaskIntoConstraints = false 8 | return collectionView 9 | }() 10 | 11 | private let collectionViewLayout: UICollectionViewCompositionalLayout = { 12 | var configuration = UICollectionLayoutListConfiguration(appearance: .grouped) 13 | return UICollectionViewCompositionalLayout.list(using: configuration) 14 | }() 15 | 16 | private lazy var dataSource: UICollectionViewDiffableDataSource = { 17 | let headerCellRegistration = UICollectionView.CellRegistration { (cell, indexPath, item) in 18 | var contentConfiguration = cell.defaultContentConfiguration() 19 | contentConfiguration.text = item.title 20 | cell.contentConfiguration = contentConfiguration 21 | 22 | let headerDisclosureOption = UICellAccessory.OutlineDisclosureOptions(style: .header) 23 | cell.accessories = [.outlineDisclosure(options: headerDisclosureOption)] 24 | } 25 | 26 | let contentCellRegistration = UICollectionView.CellRegistration { (cell, indexPath, item) in 27 | var contentConfiguration = cell.defaultContentConfiguration() 28 | contentConfiguration.text = item.emoji 29 | contentConfiguration.secondaryText = item.title 30 | cell.contentConfiguration = contentConfiguration 31 | } 32 | 33 | let dataSource = UICollectionViewDiffableDataSource( 34 | collectionView: collectionView 35 | ) { (collectionView, indexPath, item) -> UICollectionViewCell? in 36 | switch item { 37 | case .header(let headerItem): 38 | return collectionView.dequeueConfiguredReusableCell(using: headerCellRegistration, for: indexPath, item: headerItem) 39 | case .content(let contentItem): 40 | return collectionView.dequeueConfiguredReusableCell(using: contentCellRegistration, for: indexPath, item: contentItem) 41 | } 42 | } 43 | 44 | return dataSource 45 | }() 46 | 47 | /// SeeAlso: https://lets-emoji.com/emojilist/emojilist-4/ 48 | private let headerItems: [ListItem.Header] = [ 49 | ListItem.Header( 50 | title: "Food", 51 | contents: [ 52 | ListItem.Content(emoji: "🍙", title: "rice ball"), 53 | ListItem.Content(emoji: "🍛", title: "curry rice"), 54 | ListItem.Content(emoji: "🍔", title: "hamburger"), 55 | ListItem.Content(emoji: "🍟", title: "french fries"), 56 | ListItem.Content(emoji: "🍰", title: "shortcake") 57 | ] 58 | ), 59 | ListItem.Header( 60 | title: "Alcohol", 61 | contents: [ 62 | ListItem.Content(emoji: "🍺", title: "beer mug"), 63 | ListItem.Content(emoji: "🍶", title: "sake"), 64 | ListItem.Content(emoji: "🍷", title: "wine glass"), 65 | ListItem.Content(emoji: "🍸", title: "cocktail glass"), 66 | ListItem.Content(emoji: "🥃", title: "whisky") 67 | ] 68 | ), 69 | ListItem.Header( 70 | title: "NonAlcohol", 71 | contents: [ 72 | ListItem.Content(emoji: "🥛", title: "milk"), 73 | ListItem.Content(emoji: "🧋", title: "bubble tea"), 74 | ListItem.Content(emoji: "🧉", title: "mate"), 75 | ListItem.Content(emoji: "☕", title: "coffee"), 76 | ListItem.Content(emoji: "🫖", title: "tea") 77 | ] 78 | ) 79 | ] 80 | 81 | override func viewDidLoad() { 82 | super.viewDidLoad() 83 | 84 | navigationController?.navigationBar.prefersLargeTitles = true 85 | navigationItem.largeTitleDisplayMode = .never 86 | navigationItem.title = "List" 87 | 88 | view.addSubview(collectionView) 89 | NSLayoutConstraint.activate([ 90 | collectionView.topAnchor.constraint(equalTo: view.topAnchor), 91 | collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), 92 | collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), 93 | collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor) 94 | ]) 95 | 96 | applyDataSource() 97 | } 98 | 99 | private func applyDataSource() { 100 | // main section 101 | var dataSourceSnapshot = NSDiffableDataSourceSnapshot() 102 | dataSourceSnapshot.appendSections([.main]) 103 | dataSource.apply(dataSourceSnapshot, animatingDifferences: false) 104 | 105 | // inner section 106 | var sectionSnapshot = NSDiffableDataSourceSectionSnapshot() 107 | for headerItem in headerItems { 108 | let listItem = ListItem.header(headerItem) 109 | let contentItems = headerItem.contents.map(ListItem.content) 110 | sectionSnapshot.append([listItem]) 111 | sectionSnapshot.append(contentItems, to: listItem) 112 | } 113 | dataSource.apply(sectionSnapshot, to: .main, animatingDifferences: false) 114 | } 115 | } 116 | 117 | // MARK: - Section, ListItem 118 | extension ListViewController { 119 | private enum Section { 120 | case main 121 | } 122 | 123 | private enum ListItem: Hashable { 124 | case header(Header) 125 | case content(Content) 126 | 127 | struct Header: Hashable { 128 | var title: String 129 | var contents: [Content] 130 | } 131 | 132 | struct Content: Hashable { 133 | var emoji: String 134 | var title: String 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /CollectionViewLayoutPatternSample/View/Mosaic/MosaicSectionProvider.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | struct MosaicSectionProvider { 4 | func layoutSection() -> NSCollectionLayoutSection { 5 | // twoThreeItemGroup 6 | let grid1x1ItemSize = NSCollectionLayoutSize( 7 | widthDimension: .fractionalWidth(1), 8 | heightDimension: .fractionalHeight(1) 9 | ) 10 | let grid1x1Item = NSCollectionLayoutItem(layoutSize: grid1x1ItemSize) 11 | let grid1x1GroupSize = NSCollectionLayoutSize( 12 | widthDimension: .fractionalWidth(1), 13 | heightDimension: .fractionalWidth(1/3) 14 | ) 15 | let grid1x1Group = NSCollectionLayoutGroup.horizontal(layoutSize: grid1x1GroupSize, subitem: grid1x1Item, count: 3) 16 | grid1x1Group.interItemSpacing = .fixed(2) 17 | grid1x1Group.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 2, trailing: 0) 18 | 19 | let twoThreeItemGroupSize = NSCollectionLayoutSize( 20 | widthDimension: .fractionalWidth(1), 21 | heightDimension: .fractionalWidth(2/3) 22 | ) 23 | let twoThreeItemGroup = NSCollectionLayoutGroup.vertical(layoutSize: twoThreeItemGroupSize, subitems: [grid1x1Group]) 24 | 25 | // leadingLargeItemGroup 26 | let grid2x2ItemSize = NSCollectionLayoutSize( 27 | widthDimension: .fractionalWidth(2/3), 28 | heightDimension: .fractionalWidth(2/3) 29 | ) 30 | let grid2x2Item = NSCollectionLayoutItem(layoutSize: grid2x2ItemSize) 31 | 32 | let grid2x1ItemSize = NSCollectionLayoutSize( 33 | widthDimension: .fractionalWidth(1), 34 | heightDimension: .fractionalHeight(1/2) 35 | ) 36 | let grid2x1Item = NSCollectionLayoutItem(layoutSize: grid2x1ItemSize) 37 | let grid2x1GroupSize = NSCollectionLayoutSize( 38 | widthDimension: .fractionalWidth(1/3), 39 | heightDimension: .fractionalWidth(2/3) 40 | ) 41 | let leadingGrid2x1Group = NSCollectionLayoutGroup.vertical(layoutSize: grid2x1GroupSize, subitem: grid2x1Item, count: 2) 42 | leadingGrid2x1Group.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 2, bottom: 0, trailing: 0) 43 | leadingGrid2x1Group.interItemSpacing = .fixed(2) 44 | 45 | let leadingLargeItemGroupSize = NSCollectionLayoutSize( 46 | widthDimension: .fractionalWidth(1), 47 | heightDimension: .fractionalWidth(2/3) 48 | ) 49 | let leadingLargeItemGroup = NSCollectionLayoutGroup.horizontal(layoutSize: leadingLargeItemGroupSize, subitems: [grid2x2Item, leadingGrid2x1Group]) 50 | 51 | // trailingLargeItemGroup 52 | let trailingGrid2x1Group = NSCollectionLayoutGroup.vertical(layoutSize: grid2x1GroupSize, subitem: grid2x1Item, count: 2) 53 | trailingGrid2x1Group.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 2) 54 | trailingGrid2x1Group.interItemSpacing = .fixed(2) 55 | 56 | let trailingLargeItemGroupSize = NSCollectionLayoutSize( 57 | widthDimension: .fractionalWidth(1), 58 | heightDimension: .fractionalWidth(2/3) 59 | ) 60 | let trailingLargeItemGroup = NSCollectionLayoutGroup.horizontal(layoutSize: trailingLargeItemGroupSize, subitems: [trailingGrid2x1Group, grid2x2Item]) 61 | trailingLargeItemGroup.contentInsets = NSDirectionalEdgeInsets(top: 2, leading: 0, bottom: 2, trailing: 0) 62 | 63 | // nestedGroup 64 | let nestedGroupSize = NSCollectionLayoutSize( 65 | widthDimension: .fractionalWidth(1), 66 | heightDimension: .fractionalWidth(2/3 * 3) 67 | ) 68 | let nestedGroup = NSCollectionLayoutGroup.vertical( 69 | layoutSize: nestedGroupSize, 70 | subitems: [twoThreeItemGroup, leadingLargeItemGroup, trailingLargeItemGroup] 71 | ) 72 | 73 | let section = NSCollectionLayoutSection(group: nestedGroup) 74 | section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 8, bottom: 0, trailing: 8) 75 | return section 76 | 77 | } 78 | 79 | func provideCell(_ collectionView: UICollectionView, indexPath: IndexPath, item: Item) -> UICollectionViewCell { 80 | let cell = collectionView.dequeueReusableCell(type: Cell.self, for: indexPath) 81 | cell.title = item.text 82 | 83 | let index = indexPath.item + 1 84 | if index % 12 == 0 { 85 | cell.backgroundColor = .systemBlue.withAlphaComponent(0.8) 86 | } else if index % 6 == 1 && index % 12 != 1 { 87 | cell.backgroundColor = .systemBlue.withAlphaComponent(0.8) 88 | } else { 89 | cell.backgroundColor = .systemBlue 90 | } 91 | 92 | return cell 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /CollectionViewLayoutPatternSample/View/Mosaic/MosaicViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class MosaicViewController: UIViewController { 4 | private lazy var collectionView: UICollectionView = { 5 | let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) 6 | collectionView.backgroundColor = .systemBackground 7 | collectionView.registerCell(type: Cell.self) 8 | return collectionView 9 | }() 10 | 11 | private lazy var collectionViewLayout: UICollectionViewCompositionalLayout = { 12 | let provider = sectionModel.provider 13 | let layout = UICollectionViewCompositionalLayout(section: provider.layoutSection()) 14 | return layout 15 | }() 16 | 17 | private lazy var dataSource: UICollectionViewDiffableDataSource = { 18 | let dataSource = UICollectionViewDiffableDataSource( 19 | collectionView: collectionView 20 | ) { [weak self] (collectionView, indexPath, item) -> UICollectionViewCell? in 21 | return self?.provideCell(collectionView, indexPath: indexPath, item: item) 22 | } 23 | 24 | return dataSource 25 | }() 26 | 27 | private let sectionModel = SectionModel( 28 | provider: MosaicSectionProvider(), 29 | section: .main, 30 | items: (0...50).map { Item(text: $0.description) } 31 | ) 32 | 33 | override func viewDidLoad() { 34 | super.viewDidLoad() 35 | 36 | navigationController?.navigationBar.prefersLargeTitles = true 37 | navigationItem.largeTitleDisplayMode = .never 38 | navigationItem.title = "Mosaic" 39 | 40 | collectionView.translatesAutoresizingMaskIntoConstraints = false 41 | view.addSubview(collectionView) 42 | NSLayoutConstraint.activate([ 43 | collectionView.topAnchor.constraint(equalTo: view.topAnchor), 44 | collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), 45 | collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), 46 | collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor) 47 | ]) 48 | 49 | applyDataSource() 50 | } 51 | 52 | private func applyDataSource() { 53 | var snapshot = NSDiffableDataSourceSnapshot() 54 | snapshot.appendSections([sectionModel.section]) 55 | snapshot.appendItems(sectionModel.items) 56 | dataSource.apply(snapshot, animatingDifferences: false) 57 | } 58 | 59 | private func provideCell(_ collectionView: UICollectionView, indexPath: IndexPath, item: Item) -> UICollectionViewCell { 60 | let provider = sectionModel.provider 61 | return provider.provideCell(collectionView, indexPath: indexPath, item: item) 62 | } 63 | } 64 | 65 | // MARK: - SectionModel 66 | extension MosaicViewController { 67 | private struct SectionModel { 68 | var provider: MosaicSectionProvider 69 | var section: Section 70 | var items: [Item] 71 | } 72 | 73 | private enum Section { 74 | case main 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /CollectionViewLayoutPatternSample/View/TopAligned/MultiLineTextCell.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class MultiLineTextCell: UICollectionViewCell { 4 | let cellView: MultiLineTextCellView = { 5 | let cellView = MultiLineTextCellView() 6 | cellView.translatesAutoresizingMaskIntoConstraints = false 7 | return cellView 8 | }() 9 | 10 | override init(frame: CGRect) { 11 | super.init(frame: frame) 12 | 13 | contentView.layer.borderWidth = 1 14 | contentView.layer.borderColor = UIColor.label.cgColor 15 | contentView.layer.masksToBounds = true 16 | 17 | contentView.addSubview(cellView) 18 | NSLayoutConstraint.activate([ 19 | cellView.topAnchor.constraint(equalTo: contentView.topAnchor), 20 | cellView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), 21 | cellView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), 22 | cellView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) 23 | ]) 24 | } 25 | 26 | @available(*, unavailable) 27 | required init?(coder aDecoder: NSCoder) { 28 | fatalError("init(coder:) has not been implemented") 29 | } 30 | } 31 | 32 | // MARK: - MultiLineTextCellView 33 | final class MultiLineTextCellView: UIView { 34 | private let stackView: UIStackView = { 35 | let stackView = UIStackView() 36 | stackView.axis = .vertical 37 | stackView.alignment = .fill 38 | stackView.distribution = .fill 39 | stackView.spacing = 4 40 | stackView.translatesAutoresizingMaskIntoConstraints = false 41 | return stackView 42 | }() 43 | 44 | private let fillColorView: UIView = { 45 | let fillColorView = UIView() 46 | fillColorView.backgroundColor = .systemBlue 47 | fillColorView.translatesAutoresizingMaskIntoConstraints = false 48 | fillColorView.heightAnchor.constraint(equalTo: fillColorView.widthAnchor, multiplier: 3/4).isActive = true 49 | return fillColorView 50 | }() 51 | 52 | private let titleLabel: UILabel = { 53 | let titleLabel = UILabel() 54 | titleLabel.font = .systemFont(ofSize: 20) 55 | titleLabel.textColor = .label 56 | titleLabel.textAlignment = .left 57 | titleLabel.numberOfLines = 1 58 | return titleLabel 59 | }() 60 | 61 | private let subtitleLabel: UILabel = { 62 | let subtitleLabel = UILabel() 63 | subtitleLabel.font = .systemFont(ofSize: 14) 64 | subtitleLabel.textColor = .label 65 | subtitleLabel.textAlignment = .left 66 | subtitleLabel.numberOfLines = 0 67 | subtitleLabel.setContentHuggingPriority(.defaultHigh, for: .vertical) 68 | return subtitleLabel 69 | }() 70 | 71 | override init(frame: CGRect) { 72 | super.init(frame: frame) 73 | 74 | stackView.addArrangedSubview(fillColorView) 75 | stackView.addArrangedSubview(titleLabel) 76 | stackView.addArrangedSubview(subtitleLabel) 77 | 78 | addSubview(stackView) 79 | NSLayoutConstraint.activate([ 80 | stackView.topAnchor.constraint(equalTo: topAnchor), 81 | stackView.leadingAnchor.constraint(equalTo: leadingAnchor), 82 | stackView.trailingAnchor.constraint(equalTo: trailingAnchor), 83 | stackView.bottomAnchor.constraint(equalTo: bottomAnchor) 84 | ]) 85 | } 86 | 87 | @available(*, unavailable) 88 | required init?(coder aDecoder: NSCoder) { 89 | fatalError("init(coder:) has not been implemented") 90 | } 91 | 92 | func configure(title: String, subtitle: String) { 93 | titleLabel.text = title 94 | subtitleLabel.text = subtitle 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /CollectionViewLayoutPatternSample/View/TopAligned/TopAlignedCollectionViewFlowLayout.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// Layout to align vertically even if there are multiple `UICollectionViewCell`s with different heights per row 4 | /// 5 | /// - Note: 6 | /// [Collection View Programming Guide for iOS](https://developer.apple.com/library/archive/documentation/WindowsViews/Conceptual/CollectionViewPGforIOS/UsingtheFlowLayout/UsingtheFlowLayout.html) 7 | /// default the margin prevents evenly placed up and down 8 | /// 9 | /// - See Also: https://stackoverflow.com/questions/16837928/uicollection-view-flow-layout-vertical-align 10 | final class TopAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout { 11 | override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { 12 | guard let attributes = super.layoutAttributesForElements(in: rect) else { 13 | return nil 14 | } 15 | 16 | var baseline: CGFloat = 0 17 | var sameLineAttributes = [UICollectionViewLayoutAttributes]() 18 | for attribute in attributes where attribute.representedElementCategory == .cell { 19 | let frame = attribute.frame 20 | let centerY = frame.midY 21 | if abs(centerY - baseline) > 1 { 22 | baseline = centerY 23 | alignToTopForSameLineAttributes(sameLineAttributes) 24 | sameLineAttributes.removeAll() 25 | } 26 | sameLineAttributes.append(attribute) 27 | } 28 | 29 | alignToTopForSameLineAttributes(sameLineAttributes) 30 | return attributes 31 | } 32 | 33 | private func alignToTopForSameLineAttributes(_ attributes: [UICollectionViewLayoutAttributes]) { 34 | guard attributes.count > 1 else { 35 | return 36 | } 37 | 38 | let sorted = attributes.sorted { (lhs: UICollectionViewLayoutAttributes, rhs: UICollectionViewLayoutAttributes) -> Bool in 39 | let rhsHeight = lhs.frame.size.height 40 | let lhsHeight = rhs.frame.size.height 41 | let delta = rhsHeight - lhsHeight 42 | return delta <= 0 43 | } 44 | if let tallest = sorted.last { 45 | for attribute in attributes { 46 | attribute.frame = attribute.frame.offsetBy( 47 | dx: 0, 48 | dy: tallest.frame.origin.y - attribute.frame.origin.y 49 | ) 50 | } 51 | } 52 | } 53 | } 54 | 55 | -------------------------------------------------------------------------------- /CollectionViewLayoutPatternSample/View/TopAligned/TopAlignedMultiHeightItemViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class TopAlignedMultiHeightItemViewController: UIViewController { 4 | private lazy var collectionView: UICollectionView = { 5 | let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) 6 | collectionView.backgroundColor = .systemBackground 7 | collectionView.registerCell(type: MultiLineTextCell.self) 8 | collectionView.delegate = self 9 | return collectionView 10 | }() 11 | 12 | // private let collectionViewLayout: UICollectionViewCompositionalLayout = { 13 | // let itemSize = NSCollectionLayoutSize( 14 | // widthDimension: .fractionalWidth(1), 15 | // heightDimension: .estimated(200) 16 | // ) 17 | // let item = NSCollectionLayoutItem(layoutSize: itemSize) 18 | // 19 | // let groupSize = NSCollectionLayoutSize( 20 | // widthDimension: .fractionalWidth(1), 21 | // heightDimension: .estimated(200) 22 | // ) 23 | // let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 2) 24 | // group.interItemSpacing = .fixed(8) 25 | // 26 | // let section = NSCollectionLayoutSection(group: group) 27 | // section.interGroupSpacing = 8 28 | // section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 12) 29 | // 30 | // let layout = UICollectionViewCompositionalLayout(section: section) 31 | // return layout 32 | // }() 33 | 34 | // private let collectionViewLayout: UICollectionViewFlowLayout = { 35 | // let layout = UICollectionViewFlowLayout() 36 | // layout.minimumInteritemSpacing = 8 37 | // layout.minimumLineSpacing = 8 38 | // layout.sectionInset = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16) 39 | // return layout 40 | // }() 41 | 42 | /// custom `UICollectionViewLayout` 43 | private let collectionViewLayout: TopAlignedCollectionViewFlowLayout = { 44 | let layout = TopAlignedCollectionViewFlowLayout() 45 | layout.minimumInteritemSpacing = 8 46 | layout.minimumLineSpacing = 8 47 | layout.sectionInset = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16) 48 | return layout 49 | }() 50 | 51 | private lazy var dataSource: UICollectionViewDiffableDataSource = { 52 | let dataSource = UICollectionViewDiffableDataSource( 53 | collectionView: collectionView 54 | ) { [weak self] (collectionView, indexPath, item) -> UICollectionViewCell? in 55 | return self?.provideCell(collectionView: collectionView, indexPath: indexPath, item: item) 56 | } 57 | 58 | return dataSource 59 | }() 60 | 61 | private let sectionModel = SectionModel( 62 | section: .main, 63 | items: [ 64 | .init(text: "text"), 65 | .init(text: (0..<3).reduce("") { str, _ in str + "text" + "\n" }), 66 | .init(text: (0..<4).reduce("") { str, _ in str + "text" + "\n" }), 67 | .init(text: "text") 68 | ] 69 | ) 70 | 71 | override func viewDidLoad() { 72 | super.viewDidLoad() 73 | 74 | navigationController?.navigationBar.prefersLargeTitles = true 75 | navigationItem.largeTitleDisplayMode = .never 76 | navigationItem.title = "TopAlignedMultiHeightItem" 77 | 78 | collectionView.translatesAutoresizingMaskIntoConstraints = false 79 | view.addSubview(collectionView) 80 | NSLayoutConstraint.activate([ 81 | collectionView.topAnchor.constraint(equalTo: view.topAnchor), 82 | collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), 83 | collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), 84 | collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor) 85 | ]) 86 | 87 | applyDataSource() 88 | } 89 | 90 | private func applyDataSource() { 91 | var snapshot = NSDiffableDataSourceSnapshot() 92 | snapshot.appendSections([sectionModel.section]) 93 | snapshot.appendItems(sectionModel.items) 94 | dataSource.apply(snapshot, animatingDifferences: false) 95 | } 96 | 97 | private func provideCell(collectionView: UICollectionView, indexPath: IndexPath, item: Item) -> UICollectionViewCell { 98 | let cell = collectionView.dequeueReusableCell(type: MultiLineTextCell.self, for: indexPath) 99 | cell.cellView.configure(title: indexPath.item.description, subtitle: item.text) 100 | return cell 101 | } 102 | } 103 | 104 | // MARK: - UICollectionViewDelegateFlowLayout 105 | extension TopAlignedMultiHeightItemViewController: UICollectionViewDelegateFlowLayout { 106 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 107 | guard let flowLayout = collectionViewLayout as? UICollectionViewFlowLayout else { 108 | return .zero 109 | } 110 | 111 | let itemCountForLine = CGFloat(2) 112 | let margin = (flowLayout.sectionInset.left + flowLayout.sectionInset.right) + (flowLayout.minimumInteritemSpacing * (itemCountForLine - 1)) 113 | let contentWidth = (collectionView.bounds.width - margin) / itemCountForLine 114 | 115 | let item = dataSource.snapshot().itemIdentifiers[indexPath.item] 116 | let cellView = MultiLineTextCellView() 117 | cellView.configure(title: indexPath.item.description, subtitle: item.text) 118 | let preferredSize = cellView.systemLayoutSizeFitting( 119 | .init(width: contentWidth, height: collectionView.bounds.height), 120 | withHorizontalFittingPriority: .required, 121 | verticalFittingPriority: .fittingSizeLevel 122 | ) 123 | 124 | // TODO: cache 125 | let size = CGSize(width: contentWidth, height: preferredSize.height) 126 | return size 127 | } 128 | } 129 | 130 | // MARK: - SectionModel 131 | extension TopAlignedMultiHeightItemViewController { 132 | private struct SectionModel { 133 | var section: Section 134 | var items: [Item] 135 | } 136 | 137 | private enum Section { 138 | case main 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /Images/data_flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/to4iki/collection-view-layout-pattern-sample/54ed5561222fb7f9a60293167151db66c5793929/Images/data_flow.png -------------------------------------------------------------------------------- /Images/detail_layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/to4iki/collection-view-layout-pattern-sample/54ed5561222fb7f9a60293167151db66c5793929/Images/detail_layout.png -------------------------------------------------------------------------------- /Images/grid_layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/to4iki/collection-view-layout-pattern-sample/54ed5561222fb7f9a60293167151db66c5793929/Images/grid_layout.png -------------------------------------------------------------------------------- /Images/list_layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/to4iki/collection-view-layout-pattern-sample/54ed5561222fb7f9a60293167151db66c5793929/Images/list_layout.png -------------------------------------------------------------------------------- /Images/mosaic_layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/to4iki/collection-view-layout-pattern-sample/54ed5561222fb7f9a60293167151db66c5793929/Images/mosaic_layout.png -------------------------------------------------------------------------------- /Images/top_aligned_layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/to4iki/collection-view-layout-pattern-sample/54ed5561222fb7f9a60293167151db66c5793929/Images/top_aligned_layout.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Toshiki Takezawa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # collection-view-layout-pattern-sample 2 | `UICollectionViewLayout` implementation pattern. 3 | 4 | # About 5 | This project is introduced in iOSDC Japan 2021. 6 | sample code for [slide](https://speakerdeck.com/to4iki/kesuniying-zitauicollectionviewfalsereiautoshi-zhuang-patan) 7 | 8 | # Pattern 9 | list | grid | mosaic | topAligned | detail 10 | --- | --- | --- | --- | --- 11 | | | | | 12 | 13 | * only the list pattern is using iOS 14.0+ API 14 | 15 | ### detail patten 16 |

17 | 18 |

19 | 20 | use [DetailSectionProvider](./CollectionViewLayoutPatternSample/View/Detail/DetailSectionProvider.swift) protocol 21 | 22 | ```swift 23 | protocol DetailSectionProvider { 24 | func layoutSection(contentWidth: CGFloat, traitCollection: UITraitCollection) -> NSCollectionLayoutSection 25 | func provideCell(_ collectionView: UICollectionView, indexPath: IndexPath, item: DetailSectionItem) -> UICollectionViewCell 26 | func provideHeaderView(_ collectionView: UICollectionView, indexPath: IndexPath, section: DetailSection) -> UICollectionReusableView? 27 | func provideFooterView(_ collectionView: UICollectionView, indexPath: IndexPath, section: DetailSection) -> UICollectionReusableView? 28 | } 29 | ``` 30 | 31 | ```swift 32 | struct DetailSectionModel { 33 | var provider: DetailSectionProvider 34 | var section: DetailSection 35 | var items: [DetailSectionItem] 36 | } 37 | ``` 38 | 39 | ## Requirements 40 | Requires Xcode12 and iOS 14.0 or later. 41 | --------------------------------------------------------------------------------