├── .gitignore ├── .swiftlint.yml ├── CustomCollectionLayout.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── contents.xcworkspacedata ├── CustomCollectionLayout ├── AppDelegate.swift ├── Base.lproj │ └── LaunchScreen.xib ├── CollectionViewController.swift ├── CollectionViewController.xib ├── ContentCollectionViewCell.swift ├── ContentCollectionViewCell.xib ├── CustomCollectionViewLayout.h ├── CustomCollectionViewLayout.m ├── CustomCollectionViewLayout.swift ├── Images.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json └── Info.plist ├── CustomCollectionLayoutTests ├── CustomCollectionLayoutTests.swift └── Info.plist ├── LICENSE ├── README.md └── customCollectionLayoutDemo.gif /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | .build/ 41 | 42 | # CocoaPods 43 | # 44 | # We recommend against adding the Pods directory to your .gitignore. However 45 | # you should judge for yourself, the pros and cons are mentioned at: 46 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 47 | # 48 | # Pods/ 49 | 50 | # Carthage 51 | # 52 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 53 | # Carthage/Checkouts 54 | 55 | Carthage/Build 56 | 57 | # fastlane 58 | # 59 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 60 | # screenshots whenever they are needed. 61 | # For more information about the recommended setup visit: 62 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 63 | 64 | fastlane/report.xml 65 | fastlane/Preview.html 66 | fastlane/screenshots 67 | fastlane/test_output 68 | 69 | .idea/ -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - todo 3 | 4 | opt_in_rules: # some rules are only opt-in 5 | - control_statement 6 | - empty_count 7 | - trailing_newline 8 | - colon 9 | - comma 10 | 11 | excluded: 12 | - Pods 13 | 14 | force_cast: warning # implicitly 15 | force_try: 16 | severity: warning # explicitly 17 | line_length: 18 | warning: 130 19 | error: 130 20 | -------------------------------------------------------------------------------- /CustomCollectionLayout.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 8259A7E41A3ED3A00027F925 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8259A7E31A3ED3A00027F925 /* AppDelegate.swift */; }; 11 | 8259A7E61A3ED3A00027F925 /* CollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8259A7E51A3ED3A00027F925 /* CollectionViewController.swift */; }; 12 | 8259A7EB1A3ED3A00027F925 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8259A7EA1A3ED3A00027F925 /* Images.xcassets */; }; 13 | 8259A7EE1A3ED3A10027F925 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 8259A7EC1A3ED3A10027F925 /* LaunchScreen.xib */; }; 14 | 8259A7FA1A3ED3A10027F925 /* CustomCollectionLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8259A7F91A3ED3A10027F925 /* CustomCollectionLayoutTests.swift */; }; 15 | 8259A8101A3ED55F0027F925 /* CollectionViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 8259A80F1A3ED55F0027F925 /* CollectionViewController.xib */; }; 16 | 8259A8121A3ED5AC0027F925 /* CustomCollectionViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8259A8111A3ED5AC0027F925 /* CustomCollectionViewLayout.swift */; }; 17 | 827A971E1A603E600055CFA1 /* ContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 827A971C1A603E600055CFA1 /* ContentCollectionViewCell.swift */; }; 18 | 827A971F1A603E600055CFA1 /* ContentCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 827A971D1A603E600055CFA1 /* ContentCollectionViewCell.xib */; }; 19 | /* End PBXBuildFile section */ 20 | 21 | /* Begin PBXContainerItemProxy section */ 22 | 8259A7F41A3ED3A10027F925 /* PBXContainerItemProxy */ = { 23 | isa = PBXContainerItemProxy; 24 | containerPortal = 8259A7D61A3ED3A00027F925 /* Project object */; 25 | proxyType = 1; 26 | remoteGlobalIDString = 8259A7DD1A3ED3A00027F925; 27 | remoteInfo = CustomCollectionLayout; 28 | }; 29 | /* End PBXContainerItemProxy section */ 30 | 31 | /* Begin PBXFileReference section */ 32 | 8259A7DE1A3ED3A00027F925 /* CustomCollectionLayout.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CustomCollectionLayout.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33 | 8259A7E21A3ED3A00027F925 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 34 | 8259A7E31A3ED3A00027F925 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 35 | 8259A7E51A3ED3A00027F925 /* CollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewController.swift; sourceTree = ""; }; 36 | 8259A7EA1A3ED3A00027F925 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 37 | 8259A7ED1A3ED3A10027F925 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 38 | 8259A7F31A3ED3A10027F925 /* CustomCollectionLayoutTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CustomCollectionLayoutTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 39 | 8259A7F81A3ED3A10027F925 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 40 | 8259A7F91A3ED3A10027F925 /* CustomCollectionLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomCollectionLayoutTests.swift; sourceTree = ""; }; 41 | 8259A8041A3ED3B70027F925 /* CustomCollectionViewLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CustomCollectionViewLayout.h; sourceTree = ""; }; 42 | 8259A8051A3ED3B70027F925 /* CustomCollectionViewLayout.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CustomCollectionViewLayout.m; sourceTree = ""; }; 43 | 8259A80F1A3ED55F0027F925 /* CollectionViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = CollectionViewController.xib; sourceTree = ""; }; 44 | 8259A8111A3ED5AC0027F925 /* CustomCollectionViewLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomCollectionViewLayout.swift; sourceTree = ""; }; 45 | 827A971C1A603E600055CFA1 /* ContentCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentCollectionViewCell.swift; sourceTree = ""; }; 46 | 827A971D1A603E600055CFA1 /* ContentCollectionViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ContentCollectionViewCell.xib; sourceTree = ""; }; 47 | /* End PBXFileReference section */ 48 | 49 | /* Begin PBXFrameworksBuildPhase section */ 50 | 8259A7DB1A3ED3A00027F925 /* Frameworks */ = { 51 | isa = PBXFrameworksBuildPhase; 52 | buildActionMask = 2147483647; 53 | files = ( 54 | ); 55 | runOnlyForDeploymentPostprocessing = 0; 56 | }; 57 | 8259A7F01A3ED3A10027F925 /* Frameworks */ = { 58 | isa = PBXFrameworksBuildPhase; 59 | buildActionMask = 2147483647; 60 | files = ( 61 | ); 62 | runOnlyForDeploymentPostprocessing = 0; 63 | }; 64 | /* End PBXFrameworksBuildPhase section */ 65 | 66 | /* Begin PBXGroup section */ 67 | 8259A7D51A3ED3A00027F925 = { 68 | isa = PBXGroup; 69 | children = ( 70 | 8259A7E01A3ED3A00027F925 /* CustomCollectionLayout */, 71 | 8259A7F61A3ED3A10027F925 /* CustomCollectionLayoutTests */, 72 | 8259A7DF1A3ED3A00027F925 /* Products */, 73 | ); 74 | sourceTree = ""; 75 | }; 76 | 8259A7DF1A3ED3A00027F925 /* Products */ = { 77 | isa = PBXGroup; 78 | children = ( 79 | 8259A7DE1A3ED3A00027F925 /* CustomCollectionLayout.app */, 80 | 8259A7F31A3ED3A10027F925 /* CustomCollectionLayoutTests.xctest */, 81 | ); 82 | name = Products; 83 | sourceTree = ""; 84 | }; 85 | 8259A7E01A3ED3A00027F925 /* CustomCollectionLayout */ = { 86 | isa = PBXGroup; 87 | children = ( 88 | 827A97151A603DAB0055CFA1 /* Views */, 89 | 8259A8031A3ED3A70027F925 /* Classes */, 90 | 8259A80E1A3ED5480027F925 /* Controllers */, 91 | 8259A7E31A3ED3A00027F925 /* AppDelegate.swift */, 92 | 8259A7EA1A3ED3A00027F925 /* Images.xcassets */, 93 | 8259A7EC1A3ED3A10027F925 /* LaunchScreen.xib */, 94 | 8259A7E11A3ED3A00027F925 /* Supporting Files */, 95 | ); 96 | path = CustomCollectionLayout; 97 | sourceTree = ""; 98 | }; 99 | 8259A7E11A3ED3A00027F925 /* Supporting Files */ = { 100 | isa = PBXGroup; 101 | children = ( 102 | 8259A7E21A3ED3A00027F925 /* Info.plist */, 103 | ); 104 | name = "Supporting Files"; 105 | sourceTree = ""; 106 | }; 107 | 8259A7F61A3ED3A10027F925 /* CustomCollectionLayoutTests */ = { 108 | isa = PBXGroup; 109 | children = ( 110 | 8259A7F91A3ED3A10027F925 /* CustomCollectionLayoutTests.swift */, 111 | 8259A7F71A3ED3A10027F925 /* Supporting Files */, 112 | ); 113 | path = CustomCollectionLayoutTests; 114 | sourceTree = ""; 115 | }; 116 | 8259A7F71A3ED3A10027F925 /* Supporting Files */ = { 117 | isa = PBXGroup; 118 | children = ( 119 | 8259A7F81A3ED3A10027F925 /* Info.plist */, 120 | ); 121 | name = "Supporting Files"; 122 | sourceTree = ""; 123 | }; 124 | 8259A8031A3ED3A70027F925 /* Classes */ = { 125 | isa = PBXGroup; 126 | children = ( 127 | 8259A8091A3ED47F0027F925 /* CustomCollectionViewLayout */, 128 | ); 129 | name = Classes; 130 | sourceTree = ""; 131 | }; 132 | 8259A8071A3ED3F50027F925 /* ObjectiveC - Not used */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | 8259A8041A3ED3B70027F925 /* CustomCollectionViewLayout.h */, 136 | 8259A8051A3ED3B70027F925 /* CustomCollectionViewLayout.m */, 137 | ); 138 | name = "ObjectiveC - Not used"; 139 | sourceTree = ""; 140 | }; 141 | 8259A8081A3ED46E0027F925 /* Swift */ = { 142 | isa = PBXGroup; 143 | children = ( 144 | 8259A8111A3ED5AC0027F925 /* CustomCollectionViewLayout.swift */, 145 | ); 146 | name = Swift; 147 | sourceTree = ""; 148 | }; 149 | 8259A8091A3ED47F0027F925 /* CustomCollectionViewLayout */ = { 150 | isa = PBXGroup; 151 | children = ( 152 | 8259A8081A3ED46E0027F925 /* Swift */, 153 | 8259A8071A3ED3F50027F925 /* ObjectiveC - Not used */, 154 | ); 155 | name = CustomCollectionViewLayout; 156 | sourceTree = ""; 157 | }; 158 | 8259A80E1A3ED5480027F925 /* Controllers */ = { 159 | isa = PBXGroup; 160 | children = ( 161 | 8259A7E51A3ED3A00027F925 /* CollectionViewController.swift */, 162 | 8259A80F1A3ED55F0027F925 /* CollectionViewController.xib */, 163 | ); 164 | name = Controllers; 165 | sourceTree = ""; 166 | }; 167 | 827A97151A603DAB0055CFA1 /* Views */ = { 168 | isa = PBXGroup; 169 | children = ( 170 | 827A971B1A603E470055CFA1 /* Content Cell */, 171 | ); 172 | name = Views; 173 | sourceTree = ""; 174 | }; 175 | 827A971B1A603E470055CFA1 /* Content Cell */ = { 176 | isa = PBXGroup; 177 | children = ( 178 | 827A971C1A603E600055CFA1 /* ContentCollectionViewCell.swift */, 179 | 827A971D1A603E600055CFA1 /* ContentCollectionViewCell.xib */, 180 | ); 181 | name = "Content Cell"; 182 | sourceTree = ""; 183 | }; 184 | /* End PBXGroup section */ 185 | 186 | /* Begin PBXNativeTarget section */ 187 | 8259A7DD1A3ED3A00027F925 /* CustomCollectionLayout */ = { 188 | isa = PBXNativeTarget; 189 | buildConfigurationList = 8259A7FD1A3ED3A10027F925 /* Build configuration list for PBXNativeTarget "CustomCollectionLayout" */; 190 | buildPhases = ( 191 | 8259A7DA1A3ED3A00027F925 /* Sources */, 192 | 8259A7DB1A3ED3A00027F925 /* Frameworks */, 193 | 8259A7DC1A3ED3A00027F925 /* Resources */, 194 | ); 195 | buildRules = ( 196 | ); 197 | dependencies = ( 198 | ); 199 | name = CustomCollectionLayout; 200 | productName = CustomCollectionLayout; 201 | productReference = 8259A7DE1A3ED3A00027F925 /* CustomCollectionLayout.app */; 202 | productType = "com.apple.product-type.application"; 203 | }; 204 | 8259A7F21A3ED3A10027F925 /* CustomCollectionLayoutTests */ = { 205 | isa = PBXNativeTarget; 206 | buildConfigurationList = 8259A8001A3ED3A10027F925 /* Build configuration list for PBXNativeTarget "CustomCollectionLayoutTests" */; 207 | buildPhases = ( 208 | 8259A7EF1A3ED3A10027F925 /* Sources */, 209 | 8259A7F01A3ED3A10027F925 /* Frameworks */, 210 | 8259A7F11A3ED3A10027F925 /* Resources */, 211 | ); 212 | buildRules = ( 213 | ); 214 | dependencies = ( 215 | 8259A7F51A3ED3A10027F925 /* PBXTargetDependency */, 216 | ); 217 | name = CustomCollectionLayoutTests; 218 | productName = CustomCollectionLayoutTests; 219 | productReference = 8259A7F31A3ED3A10027F925 /* CustomCollectionLayoutTests.xctest */; 220 | productType = "com.apple.product-type.bundle.unit-test"; 221 | }; 222 | /* End PBXNativeTarget section */ 223 | 224 | /* Begin PBXProject section */ 225 | 8259A7D61A3ED3A00027F925 /* Project object */ = { 226 | isa = PBXProject; 227 | attributes = { 228 | LastSwiftMigration = 0700; 229 | LastSwiftUpdateCheck = 0700; 230 | LastUpgradeCheck = 0700; 231 | ORGANIZATIONNAME = brightec; 232 | TargetAttributes = { 233 | 8259A7DD1A3ED3A00027F925 = { 234 | CreatedOnToolsVersion = 6.1.1; 235 | LastSwiftMigration = 0830; 236 | }; 237 | 8259A7F21A3ED3A10027F925 = { 238 | CreatedOnToolsVersion = 6.1.1; 239 | LastSwiftMigration = 0830; 240 | TestTargetID = 8259A7DD1A3ED3A00027F925; 241 | }; 242 | }; 243 | }; 244 | buildConfigurationList = 8259A7D91A3ED3A00027F925 /* Build configuration list for PBXProject "CustomCollectionLayout" */; 245 | compatibilityVersion = "Xcode 3.2"; 246 | developmentRegion = English; 247 | hasScannedForEncodings = 0; 248 | knownRegions = ( 249 | en, 250 | Base, 251 | ); 252 | mainGroup = 8259A7D51A3ED3A00027F925; 253 | productRefGroup = 8259A7DF1A3ED3A00027F925 /* Products */; 254 | projectDirPath = ""; 255 | projectRoot = ""; 256 | targets = ( 257 | 8259A7DD1A3ED3A00027F925 /* CustomCollectionLayout */, 258 | 8259A7F21A3ED3A10027F925 /* CustomCollectionLayoutTests */, 259 | ); 260 | }; 261 | /* End PBXProject section */ 262 | 263 | /* Begin PBXResourcesBuildPhase section */ 264 | 8259A7DC1A3ED3A00027F925 /* Resources */ = { 265 | isa = PBXResourcesBuildPhase; 266 | buildActionMask = 2147483647; 267 | files = ( 268 | 8259A7EE1A3ED3A10027F925 /* LaunchScreen.xib in Resources */, 269 | 8259A8101A3ED55F0027F925 /* CollectionViewController.xib in Resources */, 270 | 827A971F1A603E600055CFA1 /* ContentCollectionViewCell.xib in Resources */, 271 | 8259A7EB1A3ED3A00027F925 /* Images.xcassets in Resources */, 272 | ); 273 | runOnlyForDeploymentPostprocessing = 0; 274 | }; 275 | 8259A7F11A3ED3A10027F925 /* Resources */ = { 276 | isa = PBXResourcesBuildPhase; 277 | buildActionMask = 2147483647; 278 | files = ( 279 | ); 280 | runOnlyForDeploymentPostprocessing = 0; 281 | }; 282 | /* End PBXResourcesBuildPhase section */ 283 | 284 | /* Begin PBXSourcesBuildPhase section */ 285 | 8259A7DA1A3ED3A00027F925 /* Sources */ = { 286 | isa = PBXSourcesBuildPhase; 287 | buildActionMask = 2147483647; 288 | files = ( 289 | 8259A7E61A3ED3A00027F925 /* CollectionViewController.swift in Sources */, 290 | 8259A8121A3ED5AC0027F925 /* CustomCollectionViewLayout.swift in Sources */, 291 | 8259A7E41A3ED3A00027F925 /* AppDelegate.swift in Sources */, 292 | 827A971E1A603E600055CFA1 /* ContentCollectionViewCell.swift in Sources */, 293 | ); 294 | runOnlyForDeploymentPostprocessing = 0; 295 | }; 296 | 8259A7EF1A3ED3A10027F925 /* Sources */ = { 297 | isa = PBXSourcesBuildPhase; 298 | buildActionMask = 2147483647; 299 | files = ( 300 | 8259A7FA1A3ED3A10027F925 /* CustomCollectionLayoutTests.swift in Sources */, 301 | ); 302 | runOnlyForDeploymentPostprocessing = 0; 303 | }; 304 | /* End PBXSourcesBuildPhase section */ 305 | 306 | /* Begin PBXTargetDependency section */ 307 | 8259A7F51A3ED3A10027F925 /* PBXTargetDependency */ = { 308 | isa = PBXTargetDependency; 309 | target = 8259A7DD1A3ED3A00027F925 /* CustomCollectionLayout */; 310 | targetProxy = 8259A7F41A3ED3A10027F925 /* PBXContainerItemProxy */; 311 | }; 312 | /* End PBXTargetDependency section */ 313 | 314 | /* Begin PBXVariantGroup section */ 315 | 8259A7EC1A3ED3A10027F925 /* LaunchScreen.xib */ = { 316 | isa = PBXVariantGroup; 317 | children = ( 318 | 8259A7ED1A3ED3A10027F925 /* Base */, 319 | ); 320 | name = LaunchScreen.xib; 321 | sourceTree = ""; 322 | }; 323 | /* End PBXVariantGroup section */ 324 | 325 | /* Begin XCBuildConfiguration section */ 326 | 8259A7FB1A3ED3A10027F925 /* Debug */ = { 327 | isa = XCBuildConfiguration; 328 | buildSettings = { 329 | ALWAYS_SEARCH_USER_PATHS = NO; 330 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 331 | CLANG_CXX_LIBRARY = "libc++"; 332 | CLANG_ENABLE_MODULES = YES; 333 | CLANG_ENABLE_OBJC_ARC = YES; 334 | CLANG_WARN_BOOL_CONVERSION = YES; 335 | CLANG_WARN_CONSTANT_CONVERSION = YES; 336 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 337 | CLANG_WARN_EMPTY_BODY = YES; 338 | CLANG_WARN_ENUM_CONVERSION = YES; 339 | CLANG_WARN_INT_CONVERSION = YES; 340 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 341 | CLANG_WARN_UNREACHABLE_CODE = YES; 342 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 343 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 344 | COPY_PHASE_STRIP = NO; 345 | ENABLE_STRICT_OBJC_MSGSEND = YES; 346 | ENABLE_TESTABILITY = YES; 347 | GCC_C_LANGUAGE_STANDARD = gnu99; 348 | GCC_DYNAMIC_NO_PIC = NO; 349 | GCC_OPTIMIZATION_LEVEL = 0; 350 | GCC_PREPROCESSOR_DEFINITIONS = ( 351 | "DEBUG=1", 352 | "$(inherited)", 353 | ); 354 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 355 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 356 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 357 | GCC_WARN_UNDECLARED_SELECTOR = YES; 358 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 359 | GCC_WARN_UNUSED_FUNCTION = YES; 360 | GCC_WARN_UNUSED_VARIABLE = YES; 361 | IPHONEOS_DEPLOYMENT_TARGET = 8.1; 362 | MTL_ENABLE_DEBUG_INFO = YES; 363 | ONLY_ACTIVE_ARCH = YES; 364 | SDKROOT = iphoneos; 365 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 366 | }; 367 | name = Debug; 368 | }; 369 | 8259A7FC1A3ED3A10027F925 /* Release */ = { 370 | isa = XCBuildConfiguration; 371 | buildSettings = { 372 | ALWAYS_SEARCH_USER_PATHS = NO; 373 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 374 | CLANG_CXX_LIBRARY = "libc++"; 375 | CLANG_ENABLE_MODULES = YES; 376 | CLANG_ENABLE_OBJC_ARC = YES; 377 | CLANG_WARN_BOOL_CONVERSION = YES; 378 | CLANG_WARN_CONSTANT_CONVERSION = YES; 379 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 380 | CLANG_WARN_EMPTY_BODY = YES; 381 | CLANG_WARN_ENUM_CONVERSION = YES; 382 | CLANG_WARN_INT_CONVERSION = YES; 383 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 384 | CLANG_WARN_UNREACHABLE_CODE = YES; 385 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 386 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 387 | COPY_PHASE_STRIP = YES; 388 | ENABLE_NS_ASSERTIONS = NO; 389 | ENABLE_STRICT_OBJC_MSGSEND = YES; 390 | GCC_C_LANGUAGE_STANDARD = gnu99; 391 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 392 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 393 | GCC_WARN_UNDECLARED_SELECTOR = YES; 394 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 395 | GCC_WARN_UNUSED_FUNCTION = YES; 396 | GCC_WARN_UNUSED_VARIABLE = YES; 397 | IPHONEOS_DEPLOYMENT_TARGET = 8.1; 398 | MTL_ENABLE_DEBUG_INFO = NO; 399 | SDKROOT = iphoneos; 400 | VALIDATE_PRODUCT = YES; 401 | }; 402 | name = Release; 403 | }; 404 | 8259A7FE1A3ED3A10027F925 /* Debug */ = { 405 | isa = XCBuildConfiguration; 406 | buildSettings = { 407 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 408 | INFOPLIST_FILE = CustomCollectionLayout/Info.plist; 409 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 410 | PRODUCT_BUNDLE_IDENTIFIER = "co.uk.brightec.$(PRODUCT_NAME:rfc1034identifier)"; 411 | PRODUCT_NAME = "$(TARGET_NAME)"; 412 | SWIFT_VERSION = 3.0; 413 | }; 414 | name = Debug; 415 | }; 416 | 8259A7FF1A3ED3A10027F925 /* Release */ = { 417 | isa = XCBuildConfiguration; 418 | buildSettings = { 419 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 420 | INFOPLIST_FILE = CustomCollectionLayout/Info.plist; 421 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 422 | PRODUCT_BUNDLE_IDENTIFIER = "co.uk.brightec.$(PRODUCT_NAME:rfc1034identifier)"; 423 | PRODUCT_NAME = "$(TARGET_NAME)"; 424 | SWIFT_VERSION = 3.0; 425 | }; 426 | name = Release; 427 | }; 428 | 8259A8011A3ED3A10027F925 /* Debug */ = { 429 | isa = XCBuildConfiguration; 430 | buildSettings = { 431 | BUNDLE_LOADER = "$(TEST_HOST)"; 432 | FRAMEWORK_SEARCH_PATHS = ( 433 | "$(SDKROOT)/Developer/Library/Frameworks", 434 | "$(inherited)", 435 | ); 436 | GCC_PREPROCESSOR_DEFINITIONS = ( 437 | "DEBUG=1", 438 | "$(inherited)", 439 | ); 440 | INFOPLIST_FILE = CustomCollectionLayoutTests/Info.plist; 441 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 442 | PRODUCT_BUNDLE_IDENTIFIER = "co.uk.brightec.$(PRODUCT_NAME:rfc1034identifier)"; 443 | PRODUCT_NAME = "$(TARGET_NAME)"; 444 | SWIFT_VERSION = 3.0; 445 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CustomCollectionLayout.app/CustomCollectionLayout"; 446 | }; 447 | name = Debug; 448 | }; 449 | 8259A8021A3ED3A10027F925 /* Release */ = { 450 | isa = XCBuildConfiguration; 451 | buildSettings = { 452 | BUNDLE_LOADER = "$(TEST_HOST)"; 453 | FRAMEWORK_SEARCH_PATHS = ( 454 | "$(SDKROOT)/Developer/Library/Frameworks", 455 | "$(inherited)", 456 | ); 457 | INFOPLIST_FILE = CustomCollectionLayoutTests/Info.plist; 458 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 459 | PRODUCT_BUNDLE_IDENTIFIER = "co.uk.brightec.$(PRODUCT_NAME:rfc1034identifier)"; 460 | PRODUCT_NAME = "$(TARGET_NAME)"; 461 | SWIFT_VERSION = 3.0; 462 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CustomCollectionLayout.app/CustomCollectionLayout"; 463 | }; 464 | name = Release; 465 | }; 466 | /* End XCBuildConfiguration section */ 467 | 468 | /* Begin XCConfigurationList section */ 469 | 8259A7D91A3ED3A00027F925 /* Build configuration list for PBXProject "CustomCollectionLayout" */ = { 470 | isa = XCConfigurationList; 471 | buildConfigurations = ( 472 | 8259A7FB1A3ED3A10027F925 /* Debug */, 473 | 8259A7FC1A3ED3A10027F925 /* Release */, 474 | ); 475 | defaultConfigurationIsVisible = 0; 476 | defaultConfigurationName = Release; 477 | }; 478 | 8259A7FD1A3ED3A10027F925 /* Build configuration list for PBXNativeTarget "CustomCollectionLayout" */ = { 479 | isa = XCConfigurationList; 480 | buildConfigurations = ( 481 | 8259A7FE1A3ED3A10027F925 /* Debug */, 482 | 8259A7FF1A3ED3A10027F925 /* Release */, 483 | ); 484 | defaultConfigurationIsVisible = 0; 485 | defaultConfigurationName = Release; 486 | }; 487 | 8259A8001A3ED3A10027F925 /* Build configuration list for PBXNativeTarget "CustomCollectionLayoutTests" */ = { 488 | isa = XCConfigurationList; 489 | buildConfigurations = ( 490 | 8259A8011A3ED3A10027F925 /* Debug */, 491 | 8259A8021A3ED3A10027F925 /* Release */, 492 | ); 493 | defaultConfigurationIsVisible = 0; 494 | defaultConfigurationName = Release; 495 | }; 496 | /* End XCConfigurationList section */ 497 | }; 498 | rootObject = 8259A7D61A3ED3A00027F925 /* Project object */; 499 | } 500 | -------------------------------------------------------------------------------- /CustomCollectionLayout.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CustomCollectionLayout/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // CustomCollectionLayout 4 | // 5 | // Created by JOSE MARTINEZ on 15/12/2014. 6 | // Copyright (c) 2014 brightec. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, 17 | didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 18 | 19 | self.window = UIWindow(frame: UIScreen.main.bounds) 20 | self.window!.backgroundColor = UIColor.white 21 | 22 | // Define root view controller 23 | let mainController: CollectionViewController = CollectionViewController(nibName: "CollectionViewController", bundle: nil) 24 | 25 | self.window!.rootViewController = mainController 26 | self.window!.makeKeyAndVisible() 27 | 28 | return true 29 | } 30 | 31 | func applicationWillResignActive(_ application: UIApplication) { 32 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary 33 | // interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the 34 | // transition to the background state. 35 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this 36 | // method to pause the game. 37 | } 38 | 39 | func applicationDidEnterBackground(_ application: UIApplication) { 40 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state 41 | // information to restore your application to its current state in case it is terminated later. 42 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the 43 | // user quits. 44 | } 45 | 46 | func applicationWillEnterForeground(_ application: UIApplication) { 47 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made 48 | // on entering the background. 49 | } 50 | 51 | func applicationDidBecomeActive(_ application: UIApplication) { 52 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was 53 | // previously in the background, optionally refresh the user interface. 54 | } 55 | 56 | func applicationWillTerminate(_ application: UIApplication) { 57 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /CustomCollectionLayout/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /CustomCollectionLayout/CollectionViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Brightec 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import UIKit 16 | 17 | class CollectionViewController: UIViewController { 18 | 19 | let contentCellIdentifier = "ContentCellIdentifier" 20 | 21 | @IBOutlet weak var collectionView: UICollectionView! 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | 26 | collectionView.register(UINib(nibName: "ContentCollectionViewCell", bundle: nil), 27 | forCellWithReuseIdentifier: contentCellIdentifier) 28 | } 29 | 30 | } 31 | 32 | // MARK: - UICollectionViewDataSource 33 | extension CollectionViewController: UICollectionViewDataSource { 34 | 35 | func numberOfSections(in collectionView: UICollectionView) -> Int { 36 | return 50 37 | } 38 | 39 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 40 | return 8 41 | } 42 | 43 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 44 | // swiftlint:disable force_cast 45 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: contentCellIdentifier, 46 | for: indexPath) as! ContentCollectionViewCell 47 | 48 | if indexPath.section % 2 != 0 { 49 | cell.backgroundColor = UIColor(white: 242/255.0, alpha: 1.0) 50 | } else { 51 | cell.backgroundColor = UIColor.white 52 | } 53 | 54 | if indexPath.section == 0 { 55 | if indexPath.row == 0 { 56 | cell.contentLabel.text = "Date" 57 | } else { 58 | cell.contentLabel.text = "Section" 59 | } 60 | } else { 61 | if indexPath.row == 0 { 62 | cell.contentLabel.text = String(indexPath.section) 63 | } else { 64 | cell.contentLabel.text = "Content" 65 | } 66 | } 67 | 68 | return cell 69 | } 70 | 71 | } 72 | 73 | // MARK: - UICollectionViewDelegate 74 | extension CollectionViewController: UICollectionViewDelegate { 75 | 76 | } 77 | -------------------------------------------------------------------------------- /CustomCollectionLayout/CollectionViewController.xib: -------------------------------------------------------------------------------- 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 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /CustomCollectionLayout/ContentCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentCollectionViewCell.swift 3 | // CustomCollectionLayout 4 | // 5 | // Created by JOSE MARTINEZ on 09/01/2015. 6 | // Copyright (c) 2015 brightec. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ContentCollectionViewCell: UICollectionViewCell { 12 | 13 | @IBOutlet weak var contentLabel: UILabel! 14 | 15 | override func awakeFromNib() { 16 | super.awakeFromNib() 17 | // Initialization code 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /CustomCollectionLayout/ContentCollectionViewCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /CustomCollectionLayout/CustomCollectionViewLayout.h: -------------------------------------------------------------------------------- 1 | // 2 | // CustomCollectionViewLayout.h 3 | // Brightec 4 | // 5 | // Created by JOSE MARTINEZ on 03/09/2014. 6 | // Copyright (c) 2014 Brightec. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | // NOTE: This class is not used in this project - actually it is removed from the target. I added it just in case you need to compare the code between Objective-C and Swift 12 | 13 | @interface CustomCollectionViewLayout : UICollectionViewLayout 14 | @end 15 | -------------------------------------------------------------------------------- /CustomCollectionLayout/CustomCollectionViewLayout.m: -------------------------------------------------------------------------------- 1 | // 2 | // CustomCollectionViewLayout.m 3 | // Brightec 4 | // 5 | // Created by JOSE MARTINEZ on 03/09/2014. 6 | // Copyright (c) 2014 Brightec. All rights reserved. 7 | // 8 | 9 | #import "CustomCollectionViewLayout.h" 10 | 11 | #define NUMBEROFCOLUMNS 8 12 | 13 | @interface CustomCollectionViewLayout () 14 | @property (strong, nonatomic) NSMutableArray *itemAttributes; 15 | @property (strong, nonatomic) NSMutableArray *itemsSize; 16 | @property (nonatomic, assign) CGSize contentSize; 17 | @end 18 | 19 | @implementation CustomCollectionViewLayout 20 | 21 | - (void)prepareLayout 22 | { 23 | if ([self.collectionView numberOfSections] == 0) { 24 | return; 25 | } 26 | 27 | NSUInteger column = 0; // Current column inside row 28 | CGFloat xOffset = 0.0; 29 | CGFloat yOffset = 0.0; 30 | CGFloat contentWidth = 0.0; // To determine the contentSize 31 | CGFloat contentHeight = 0.0; // To determine the contentSize 32 | 33 | if (self.itemAttributes.count > 0) { // We don't enter in this if statement the first time, we enter the following times 34 | for (int section = 0; section < [self.collectionView numberOfSections]; section++) { 35 | NSUInteger numberOfItems = [self.collectionView numberOfItemsInSection:section]; 36 | for (NSUInteger index = 0; index < numberOfItems; index++) { 37 | if (section != 0 && index != 0) { // This is a content cell that shouldn't be sticked 38 | continue; 39 | } 40 | UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:index inSection:section]]; 41 | if (section == 0) { // We stick the first row 42 | CGRect frame = attributes.frame; 43 | frame.origin.y = self.collectionView.contentOffset.y; 44 | attributes.frame = frame; 45 | 46 | } 47 | if (index == 0) { // We stick the first column 48 | CGRect frame = attributes.frame; 49 | frame.origin.x = self.collectionView.contentOffset.x; 50 | attributes.frame = frame; 51 | } 52 | } 53 | } 54 | 55 | return; 56 | } 57 | 58 | // The following code is only executed the first time we prepare the layout 59 | self.itemAttributes = [@[] mutableCopy]; 60 | self.itemsSize = [@[] mutableCopy]; 61 | 62 | // Tip: If we don't know the number of columns we can call the following method and use the NSUInteger object instead of the NUMBEROFCOLUMNS macro 63 | // NSUInteger numberOfItems = [self.collectionView numberOfItemsInSection:section]; 64 | 65 | // We calculate the item size of each column 66 | if (self.itemsSize.count != NUMBEROFCOLUMNS) { 67 | [self calculateItemsSize]; 68 | } 69 | 70 | // We loop through all items 71 | for (int section = 0; section < [self.collectionView numberOfSections]; section++) { 72 | NSMutableArray *sectionAttributes = [@[] mutableCopy]; 73 | for (NSUInteger index = 0; index < NUMBEROFCOLUMNS; index++) { 74 | CGSize itemSize = [self.itemsSize[index] CGSizeValue]; 75 | 76 | // We create the UICollectionViewLayoutAttributes object for each item and add it to our array. 77 | // We will use this later in layoutAttributesForItemAtIndexPath: 78 | NSIndexPath *indexPath = [NSIndexPath indexPathForItem:index inSection:section]; 79 | UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath]; 80 | attributes.frame = CGRectIntegral(CGRectMake(xOffset, yOffset, itemSize.width, itemSize.height)); 81 | 82 | if (section == 0 && index == 0) { 83 | attributes.zIndex = 1024; // Set this value for the first item (Sec0Row0) in order to make it visible over first column and first row 84 | } else if (section == 0 || index == 0) { 85 | attributes.zIndex = 1023; // Set this value for the first row or section in order to set visible over the rest of the items 86 | } 87 | if (section == 0) { 88 | CGRect frame = attributes.frame; 89 | frame.origin.y = self.collectionView.contentOffset.y; 90 | attributes.frame = frame; // Stick to the top 91 | } 92 | if (index == 0) { 93 | CGRect frame = attributes.frame; 94 | frame.origin.x = self.collectionView.contentOffset.x; 95 | attributes.frame = frame; // Stick to the left 96 | } 97 | 98 | [sectionAttributes addObject:attributes]; 99 | 100 | xOffset = xOffset+itemSize.width; 101 | column++; 102 | 103 | // Create a new row if this was the last column 104 | if (column == NUMBEROFCOLUMNS) { 105 | if (xOffset > contentWidth) { 106 | contentWidth = xOffset; 107 | } 108 | 109 | // Reset values 110 | column = 0; 111 | xOffset = 0; 112 | yOffset += itemSize.height; 113 | } 114 | } 115 | [self.itemAttributes addObject:sectionAttributes]; 116 | } 117 | 118 | // Get the last item to calculate the total height of the content 119 | UICollectionViewLayoutAttributes *attributes = [[self.itemAttributes lastObject] lastObject]; 120 | contentHeight = attributes.frame.origin.y+attributes.frame.size.height; 121 | self.contentSize = CGSizeMake(contentWidth, contentHeight); 122 | } 123 | 124 | - (CGSize)collectionViewContentSize 125 | { 126 | return self.contentSize; 127 | } 128 | 129 | - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath 130 | { 131 | return self.itemAttributes[indexPath.section][indexPath.row]; 132 | } 133 | 134 | - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect 135 | { 136 | NSMutableArray *attributes = [@[] mutableCopy]; 137 | for (NSArray *section in self.itemAttributes) { 138 | [attributes addObjectsFromArray:[section filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(UICollectionViewLayoutAttributes *evaluatedObject, NSDictionary *bindings) { 139 | return CGRectIntersectsRect(rect, [evaluatedObject frame]); 140 | }]]]; 141 | } 142 | 143 | return attributes; 144 | } 145 | 146 | - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds 147 | { 148 | return YES; // Set this to YES to call prepareLayout on every scroll 149 | } 150 | 151 | - (CGSize)sizeForItemWithColumnIndex:(NSUInteger)columnIndex 152 | { 153 | NSString *text; 154 | switch (columnIndex) { // This only makes sense if the size of the items should be different 155 | case 0: 156 | text = @"Col 0"; 157 | break; 158 | case 1: 159 | text = @"Col 1"; 160 | break; 161 | case 2: 162 | text = @"Col 2"; 163 | break; 164 | case 3: 165 | text = @"Col 3"; 166 | break; 167 | case 4: 168 | text = @"Col 4"; 169 | break; 170 | case 5: 171 | text = @"Col 5"; 172 | break; 173 | case 6: 174 | text = @"Col 6"; 175 | break; 176 | case 7: 177 | text = @"Col 7"; 178 | break; 179 | 180 | default: 181 | break; 182 | } 183 | CGSize size = [text sizeWithAttributes: @{NSFontAttributeName:[UIFont fontWithName:@"HelveticaNeue" size:15]}]; 184 | if (columnIndex == 0) { 185 | size.width += 12; // In our design the first column should be the widest one 186 | } 187 | return CGSizeMake([@(size.width + 9) floatValue], 30); // Extra space of 9px for all the items 188 | } 189 | 190 | - (void)calculateItemsSize 191 | { 192 | for (NSUInteger index = 0; index < NUMBEROFCOLUMNS; index++) { 193 | if (self.itemsSize.count <= index) { 194 | CGSize itemSize = [self sizeForItemWithColumnIndex:index]; 195 | NSValue *itemSizeValue = [NSValue valueWithCGSize:itemSize]; 196 | [self.itemsSize addObject:itemSizeValue]; 197 | } 198 | } 199 | } 200 | 201 | @end 202 | -------------------------------------------------------------------------------- /CustomCollectionLayout/CustomCollectionViewLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomCollectionViewLayout.swift 3 | // CustomCollectionLayout 4 | // 5 | // Created by JOSE MARTINEZ on 15/12/2014. 6 | // Copyright (c) 2014 brightec. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class CustomCollectionViewLayout: UICollectionViewLayout { 12 | 13 | let numberOfColumns = 8 14 | var shouldPinFirstColumn = true 15 | var shouldPinFirstRow = true 16 | 17 | var itemAttributes = [[UICollectionViewLayoutAttributes]]() 18 | var itemsSize = [CGSize]() 19 | var contentSize: CGSize = .zero 20 | 21 | override func prepare() { 22 | guard let collectionView = collectionView else { 23 | return 24 | } 25 | 26 | if collectionView.numberOfSections == 0 { 27 | return 28 | } 29 | 30 | if itemAttributes.count != collectionView.numberOfSections { 31 | generateItemAttributes(collectionView: collectionView) 32 | return 33 | } 34 | 35 | for section in 0.. UICollectionViewLayoutAttributes? { 63 | return itemAttributes[indexPath.section][indexPath.row] 64 | } 65 | 66 | override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { 67 | var attributes = [UICollectionViewLayoutAttributes]() 68 | for section in itemAttributes { 69 | let filteredArray = section.filter { obj -> Bool in 70 | return rect.intersects(obj.frame) 71 | } 72 | 73 | attributes.append(contentsOf: filteredArray) 74 | } 75 | 76 | return attributes 77 | } 78 | 79 | override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { 80 | return true 81 | } 82 | 83 | } 84 | 85 | // MARK: - Helpers 86 | extension CustomCollectionViewLayout { 87 | 88 | func generateItemAttributes(collectionView: UICollectionView) { 89 | if itemsSize.count != numberOfColumns { 90 | calculateItemSizes() 91 | } 92 | 93 | var column = 0 94 | var xOffset: CGFloat = 0 95 | var yOffset: CGFloat = 0 96 | var contentWidth: CGFloat = 0 97 | 98 | itemAttributes = [] 99 | 100 | for section in 0.. contentWidth { 135 | contentWidth = xOffset 136 | } 137 | 138 | column = 0 139 | xOffset = 0 140 | yOffset += itemSize.height 141 | } 142 | } 143 | 144 | itemAttributes.append(sectionAttributes) 145 | } 146 | 147 | if let attributes = itemAttributes.last?.last { 148 | contentSize = CGSize(width: contentWidth, height: attributes.frame.maxY) 149 | } 150 | } 151 | 152 | func calculateItemSizes() { 153 | itemsSize = [] 154 | 155 | for index in 0.. CGSize { 161 | var text: NSString 162 | 163 | switch columnIndex { 164 | case 0: 165 | text = "MMM-99" 166 | 167 | default: 168 | text = "Content" 169 | } 170 | 171 | let size: CGSize = text.size(attributes: [NSFontAttributeName: UIFont.systemFont(ofSize: 14.0)]) 172 | let width: CGFloat = size.width + 16 173 | return CGSize(width: width, height: 30) 174 | } 175 | 176 | } 177 | -------------------------------------------------------------------------------- /CustomCollectionLayout/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /CustomCollectionLayout/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /CustomCollectionLayoutTests/CustomCollectionLayoutTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomCollectionLayoutTests.swift 3 | // CustomCollectionLayoutTests 4 | // 5 | // Created by JOSE MARTINEZ on 15/12/2014. 6 | // Copyright (c) 2014 brightec. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import XCTest 11 | 12 | class CustomCollectionLayoutTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | XCTAssert(true, "Pass") 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /CustomCollectionLayoutTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 brightec 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 | CustomCollectionViewLayout 2 | ========================== 3 | 4 | Custom layout for a collection view using horizontal and vertical scrolling with sticky rows and columns 5 | Written in both Swift and Objective-C. 6 | Full example added in Swift. 7 | 8 | ![alt tag](https://github.com/brightec/CustomCollectionViewLayout/blob/master/customCollectionLayoutDemo.gif) 9 | 10 | Tutorial on Brightec blog 11 | -------------------------------------------------------------------------------- /customCollectionLayoutDemo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightec/CustomCollectionViewLayout/3d0f6580ecae965175b6f34f4ff14e431c5f812b/customCollectionLayoutDemo.gif --------------------------------------------------------------------------------