├── .gitignore ├── CollectionViewStickyHeaders.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── contents.xcworkspacedata ├── CollectionViewStickyHeaders ├── AppDelegate.swift ├── Base.lproj │ ├── LaunchScreen.xib │ └── Main.storyboard ├── CollectionViewController.swift ├── HeaderView.swift ├── Images.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Info.plist └── StickyHeaderFlowLayout.swift └── README.markdown /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | .DS_Store 3 | 4 | # Xcode 5 | # 6 | build/ 7 | *.pbxuser 8 | !default.pbxuser 9 | *.mode1v3 10 | !default.mode1v3 11 | *.mode2v3 12 | !default.mode2v3 13 | *.perspectivev3 14 | !default.perspectivev3 15 | xcuserdata 16 | *.xccheckout 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | *.xcuserstate 22 | 23 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control? 24 | Pods/ 25 | Attached/InfoPlist.h -------------------------------------------------------------------------------- /CollectionViewStickyHeaders.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | B5601F691A6131F8009BA1DA /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5601F681A6131F8009BA1DA /* AppDelegate.swift */; }; 11 | B5601F6B1A6131F8009BA1DA /* CollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5601F6A1A6131F8009BA1DA /* CollectionViewController.swift */; }; 12 | B5601F6E1A6131F8009BA1DA /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B5601F6C1A6131F8009BA1DA /* Main.storyboard */; }; 13 | B5601F701A6131F8009BA1DA /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B5601F6F1A6131F8009BA1DA /* Images.xcassets */; }; 14 | B5601F731A6131F8009BA1DA /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = B5601F711A6131F8009BA1DA /* LaunchScreen.xib */; }; 15 | B5601F891A6133EE009BA1DA /* StickyHeaderFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5601F881A6133EE009BA1DA /* StickyHeaderFlowLayout.swift */; }; 16 | B5601F8B1A6138B9009BA1DA /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5601F8A1A6138B9009BA1DA /* HeaderView.swift */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXFileReference section */ 20 | B5601F631A6131F8009BA1DA /* CollectionViewStickyHeaders.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CollectionViewStickyHeaders.app; sourceTree = BUILT_PRODUCTS_DIR; }; 21 | B5601F671A6131F8009BA1DA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 22 | B5601F681A6131F8009BA1DA /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 23 | B5601F6A1A6131F8009BA1DA /* CollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewController.swift; sourceTree = ""; }; 24 | B5601F6D1A6131F8009BA1DA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 25 | B5601F6F1A6131F8009BA1DA /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 26 | B5601F721A6131F8009BA1DA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 27 | B5601F881A6133EE009BA1DA /* StickyHeaderFlowLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StickyHeaderFlowLayout.swift; sourceTree = ""; }; 28 | B5601F8A1A6138B9009BA1DA /* HeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeaderView.swift; sourceTree = ""; }; 29 | /* End PBXFileReference section */ 30 | 31 | /* Begin PBXFrameworksBuildPhase section */ 32 | B5601F601A6131F8009BA1DA /* Frameworks */ = { 33 | isa = PBXFrameworksBuildPhase; 34 | buildActionMask = 2147483647; 35 | files = ( 36 | ); 37 | runOnlyForDeploymentPostprocessing = 0; 38 | }; 39 | /* End PBXFrameworksBuildPhase section */ 40 | 41 | /* Begin PBXGroup section */ 42 | B5601F5A1A6131F8009BA1DA = { 43 | isa = PBXGroup; 44 | children = ( 45 | B5601F651A6131F8009BA1DA /* CollectionViewStickyHeaders */, 46 | B5601F641A6131F8009BA1DA /* Products */, 47 | ); 48 | sourceTree = ""; 49 | }; 50 | B5601F641A6131F8009BA1DA /* Products */ = { 51 | isa = PBXGroup; 52 | children = ( 53 | B5601F631A6131F8009BA1DA /* CollectionViewStickyHeaders.app */, 54 | ); 55 | name = Products; 56 | sourceTree = ""; 57 | }; 58 | B5601F651A6131F8009BA1DA /* CollectionViewStickyHeaders */ = { 59 | isa = PBXGroup; 60 | children = ( 61 | B5601F681A6131F8009BA1DA /* AppDelegate.swift */, 62 | B5601F6A1A6131F8009BA1DA /* CollectionViewController.swift */, 63 | B5601F8A1A6138B9009BA1DA /* HeaderView.swift */, 64 | B5601F881A6133EE009BA1DA /* StickyHeaderFlowLayout.swift */, 65 | B5601F6C1A6131F8009BA1DA /* Main.storyboard */, 66 | B5601F6F1A6131F8009BA1DA /* Images.xcassets */, 67 | B5601F711A6131F8009BA1DA /* LaunchScreen.xib */, 68 | B5601F661A6131F8009BA1DA /* Supporting Files */, 69 | ); 70 | path = CollectionViewStickyHeaders; 71 | sourceTree = ""; 72 | }; 73 | B5601F661A6131F8009BA1DA /* Supporting Files */ = { 74 | isa = PBXGroup; 75 | children = ( 76 | B5601F671A6131F8009BA1DA /* Info.plist */, 77 | ); 78 | name = "Supporting Files"; 79 | sourceTree = ""; 80 | }; 81 | /* End PBXGroup section */ 82 | 83 | /* Begin PBXNativeTarget section */ 84 | B5601F621A6131F8009BA1DA /* CollectionViewStickyHeaders */ = { 85 | isa = PBXNativeTarget; 86 | buildConfigurationList = B5601F821A6131F8009BA1DA /* Build configuration list for PBXNativeTarget "CollectionViewStickyHeaders" */; 87 | buildPhases = ( 88 | B5601F5F1A6131F8009BA1DA /* Sources */, 89 | B5601F601A6131F8009BA1DA /* Frameworks */, 90 | B5601F611A6131F8009BA1DA /* Resources */, 91 | ); 92 | buildRules = ( 93 | ); 94 | dependencies = ( 95 | ); 96 | name = CollectionViewStickyHeaders; 97 | productName = CollectionViewStickyHeaders; 98 | productReference = B5601F631A6131F8009BA1DA /* CollectionViewStickyHeaders.app */; 99 | productType = "com.apple.product-type.application"; 100 | }; 101 | /* End PBXNativeTarget section */ 102 | 103 | /* Begin PBXProject section */ 104 | B5601F5B1A6131F8009BA1DA /* Project object */ = { 105 | isa = PBXProject; 106 | attributes = { 107 | LastUpgradeCheck = 0610; 108 | ORGANIZATIONNAME = "Dative Studios"; 109 | TargetAttributes = { 110 | B5601F621A6131F8009BA1DA = { 111 | CreatedOnToolsVersion = 6.1.1; 112 | }; 113 | }; 114 | }; 115 | buildConfigurationList = B5601F5E1A6131F8009BA1DA /* Build configuration list for PBXProject "CollectionViewStickyHeaders" */; 116 | compatibilityVersion = "Xcode 3.2"; 117 | developmentRegion = English; 118 | hasScannedForEncodings = 0; 119 | knownRegions = ( 120 | en, 121 | Base, 122 | ); 123 | mainGroup = B5601F5A1A6131F8009BA1DA; 124 | productRefGroup = B5601F641A6131F8009BA1DA /* Products */; 125 | projectDirPath = ""; 126 | projectRoot = ""; 127 | targets = ( 128 | B5601F621A6131F8009BA1DA /* CollectionViewStickyHeaders */, 129 | ); 130 | }; 131 | /* End PBXProject section */ 132 | 133 | /* Begin PBXResourcesBuildPhase section */ 134 | B5601F611A6131F8009BA1DA /* Resources */ = { 135 | isa = PBXResourcesBuildPhase; 136 | buildActionMask = 2147483647; 137 | files = ( 138 | B5601F6E1A6131F8009BA1DA /* Main.storyboard in Resources */, 139 | B5601F731A6131F8009BA1DA /* LaunchScreen.xib in Resources */, 140 | B5601F701A6131F8009BA1DA /* Images.xcassets in Resources */, 141 | ); 142 | runOnlyForDeploymentPostprocessing = 0; 143 | }; 144 | /* End PBXResourcesBuildPhase section */ 145 | 146 | /* Begin PBXSourcesBuildPhase section */ 147 | B5601F5F1A6131F8009BA1DA /* Sources */ = { 148 | isa = PBXSourcesBuildPhase; 149 | buildActionMask = 2147483647; 150 | files = ( 151 | B5601F6B1A6131F8009BA1DA /* CollectionViewController.swift in Sources */, 152 | B5601F691A6131F8009BA1DA /* AppDelegate.swift in Sources */, 153 | B5601F891A6133EE009BA1DA /* StickyHeaderFlowLayout.swift in Sources */, 154 | B5601F8B1A6138B9009BA1DA /* HeaderView.swift in Sources */, 155 | ); 156 | runOnlyForDeploymentPostprocessing = 0; 157 | }; 158 | /* End PBXSourcesBuildPhase section */ 159 | 160 | /* Begin PBXVariantGroup section */ 161 | B5601F6C1A6131F8009BA1DA /* Main.storyboard */ = { 162 | isa = PBXVariantGroup; 163 | children = ( 164 | B5601F6D1A6131F8009BA1DA /* Base */, 165 | ); 166 | name = Main.storyboard; 167 | sourceTree = ""; 168 | }; 169 | B5601F711A6131F8009BA1DA /* LaunchScreen.xib */ = { 170 | isa = PBXVariantGroup; 171 | children = ( 172 | B5601F721A6131F8009BA1DA /* Base */, 173 | ); 174 | name = LaunchScreen.xib; 175 | sourceTree = ""; 176 | }; 177 | /* End PBXVariantGroup section */ 178 | 179 | /* Begin XCBuildConfiguration section */ 180 | B5601F801A6131F8009BA1DA /* Debug */ = { 181 | isa = XCBuildConfiguration; 182 | buildSettings = { 183 | ALWAYS_SEARCH_USER_PATHS = NO; 184 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 185 | CLANG_CXX_LIBRARY = "libc++"; 186 | CLANG_ENABLE_MODULES = YES; 187 | CLANG_ENABLE_OBJC_ARC = YES; 188 | CLANG_WARN_BOOL_CONVERSION = YES; 189 | CLANG_WARN_CONSTANT_CONVERSION = YES; 190 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 191 | CLANG_WARN_EMPTY_BODY = YES; 192 | CLANG_WARN_ENUM_CONVERSION = YES; 193 | CLANG_WARN_INT_CONVERSION = YES; 194 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 195 | CLANG_WARN_UNREACHABLE_CODE = YES; 196 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 197 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 198 | COPY_PHASE_STRIP = NO; 199 | ENABLE_STRICT_OBJC_MSGSEND = YES; 200 | GCC_C_LANGUAGE_STANDARD = gnu99; 201 | GCC_DYNAMIC_NO_PIC = NO; 202 | GCC_OPTIMIZATION_LEVEL = 0; 203 | GCC_PREPROCESSOR_DEFINITIONS = ( 204 | "DEBUG=1", 205 | "$(inherited)", 206 | ); 207 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 208 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 209 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 210 | GCC_WARN_UNDECLARED_SELECTOR = YES; 211 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 212 | GCC_WARN_UNUSED_FUNCTION = YES; 213 | GCC_WARN_UNUSED_VARIABLE = YES; 214 | IPHONEOS_DEPLOYMENT_TARGET = 8.1; 215 | MTL_ENABLE_DEBUG_INFO = YES; 216 | ONLY_ACTIVE_ARCH = YES; 217 | SDKROOT = iphoneos; 218 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 219 | }; 220 | name = Debug; 221 | }; 222 | B5601F811A6131F8009BA1DA /* Release */ = { 223 | isa = XCBuildConfiguration; 224 | buildSettings = { 225 | ALWAYS_SEARCH_USER_PATHS = NO; 226 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 227 | CLANG_CXX_LIBRARY = "libc++"; 228 | CLANG_ENABLE_MODULES = YES; 229 | CLANG_ENABLE_OBJC_ARC = YES; 230 | CLANG_WARN_BOOL_CONVERSION = YES; 231 | CLANG_WARN_CONSTANT_CONVERSION = YES; 232 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 233 | CLANG_WARN_EMPTY_BODY = YES; 234 | CLANG_WARN_ENUM_CONVERSION = YES; 235 | CLANG_WARN_INT_CONVERSION = YES; 236 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 237 | CLANG_WARN_UNREACHABLE_CODE = YES; 238 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 239 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 240 | COPY_PHASE_STRIP = YES; 241 | ENABLE_NS_ASSERTIONS = NO; 242 | ENABLE_STRICT_OBJC_MSGSEND = YES; 243 | GCC_C_LANGUAGE_STANDARD = gnu99; 244 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 245 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 246 | GCC_WARN_UNDECLARED_SELECTOR = YES; 247 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 248 | GCC_WARN_UNUSED_FUNCTION = YES; 249 | GCC_WARN_UNUSED_VARIABLE = YES; 250 | IPHONEOS_DEPLOYMENT_TARGET = 8.1; 251 | MTL_ENABLE_DEBUG_INFO = NO; 252 | SDKROOT = iphoneos; 253 | VALIDATE_PRODUCT = YES; 254 | }; 255 | name = Release; 256 | }; 257 | B5601F831A6131F8009BA1DA /* Debug */ = { 258 | isa = XCBuildConfiguration; 259 | buildSettings = { 260 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 261 | INFOPLIST_FILE = CollectionViewStickyHeaders/Info.plist; 262 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 263 | PRODUCT_NAME = "$(TARGET_NAME)"; 264 | }; 265 | name = Debug; 266 | }; 267 | B5601F841A6131F8009BA1DA /* Release */ = { 268 | isa = XCBuildConfiguration; 269 | buildSettings = { 270 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 271 | INFOPLIST_FILE = CollectionViewStickyHeaders/Info.plist; 272 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 273 | PRODUCT_NAME = "$(TARGET_NAME)"; 274 | }; 275 | name = Release; 276 | }; 277 | /* End XCBuildConfiguration section */ 278 | 279 | /* Begin XCConfigurationList section */ 280 | B5601F5E1A6131F8009BA1DA /* Build configuration list for PBXProject "CollectionViewStickyHeaders" */ = { 281 | isa = XCConfigurationList; 282 | buildConfigurations = ( 283 | B5601F801A6131F8009BA1DA /* Debug */, 284 | B5601F811A6131F8009BA1DA /* Release */, 285 | ); 286 | defaultConfigurationIsVisible = 0; 287 | defaultConfigurationName = Release; 288 | }; 289 | B5601F821A6131F8009BA1DA /* Build configuration list for PBXNativeTarget "CollectionViewStickyHeaders" */ = { 290 | isa = XCConfigurationList; 291 | buildConfigurations = ( 292 | B5601F831A6131F8009BA1DA /* Debug */, 293 | B5601F841A6131F8009BA1DA /* Release */, 294 | ); 295 | defaultConfigurationIsVisible = 0; 296 | }; 297 | /* End XCConfigurationList section */ 298 | }; 299 | rootObject = B5601F5B1A6131F8009BA1DA /* Project object */; 300 | } 301 | -------------------------------------------------------------------------------- /CollectionViewStickyHeaders.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CollectionViewStickyHeaders/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Pete Callaway on 10/01/2015. 3 | // Copyright (c) 2015 Dative Studios. All rights reserved. 4 | // 5 | 6 | import UIKit 7 | 8 | @UIApplicationMain 9 | class AppDelegate: UIResponder, UIApplicationDelegate { 10 | 11 | var window: UIWindow? 12 | } 13 | 14 | -------------------------------------------------------------------------------- /CollectionViewStickyHeaders/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 | -------------------------------------------------------------------------------- /CollectionViewStickyHeaders/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 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /CollectionViewStickyHeaders/CollectionViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Pete Callaway on 10/01/2015. 3 | // Copyright (c) 2015 Dative Studios. All rights reserved. 4 | // 5 | 6 | import UIKit 7 | 8 | class CollectionViewController: UICollectionViewController { 9 | 10 | override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int { 11 | return 10 12 | } 13 | 14 | override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 15 | return 8 16 | } 17 | 18 | override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { 19 | return collectionView.dequeueReusableCellWithReuseIdentifier("TESTCELL", forIndexPath: indexPath) as UICollectionViewCell 20 | } 21 | 22 | override func collectionView(collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionReusableView { 23 | if kind == UICollectionElementKindSectionHeader { 24 | let headerView = collectionView.dequeueReusableSupplementaryViewOfKind(kind, withReuseIdentifier: "TESTHEADER", forIndexPath: indexPath) as HeaderView 25 | headerView.label?.text = "Section \(indexPath.section)" 26 | 27 | return headerView 28 | } 29 | else { 30 | assert(false, "Unsupported supplementary view kind") 31 | return UICollectionReusableView() 32 | } 33 | } 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /CollectionViewStickyHeaders/HeaderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Pete Callaway on 10/01/2015. 3 | // Copyright (c) 2015 Dative Studios. All rights reserved. 4 | // 5 | 6 | import UIKit 7 | 8 | class HeaderView: UICollectionReusableView { 9 | 10 | @IBOutlet weak var label: UILabel? 11 | 12 | 13 | class var reuseIdentifier: String { 14 | return "HeaderView" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /CollectionViewStickyHeaders/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 | } -------------------------------------------------------------------------------- /CollectionViewStickyHeaders/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | com.dativestudios.$(PRODUCT_NAME:rfc1034identifier) 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 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /CollectionViewStickyHeaders/StickyHeaderFlowLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Pete Callaway on 10/01/2015. 3 | // Copyright (c) 2015 Dative Studios. All rights reserved. 4 | // 5 | 6 | import UIKit 7 | 8 | 9 | class StickyHeaderFlowLayout: UICollectionViewFlowLayout { 10 | 11 | 12 | override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool { 13 | // Return true so we're asked for layout attributes as the content is scrolled 14 | return true 15 | } 16 | 17 | 18 | 19 | 20 | 21 | override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? { 22 | // Get the layout attributes for a standard UICollectionViewFlowLayout 23 | var elementsLayoutAttributes = super.layoutAttributesForElementsInRect(rect) as? [UICollectionViewLayoutAttributes] 24 | if elementsLayoutAttributes == nil { 25 | return nil 26 | } 27 | 28 | 29 | // Define a struct we can use to store optional layout attributes in a dictionary 30 | struct HeaderAttributes { 31 | var layoutAttributes: UICollectionViewLayoutAttributes? 32 | } 33 | var visibleSectionHeaderLayoutAttributes = [Int : HeaderAttributes]() 34 | 35 | 36 | // Loop through the layout attributes we have 37 | for (index, layoutAttributes) in enumerate(elementsLayoutAttributes!) { 38 | let section = layoutAttributes.indexPath.section 39 | 40 | switch layoutAttributes.representedElementCategory { 41 | case .SupplementaryView: 42 | // If this is a set of layout attributes for a section header, replace them with modified attributes 43 | if layoutAttributes.representedElementKind == UICollectionElementKindSectionHeader { 44 | let newLayoutAttributes = layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionHeader, atIndexPath: layoutAttributes.indexPath) 45 | elementsLayoutAttributes![index] = newLayoutAttributes 46 | 47 | // Store the layout attributes in the dictionary so we know they've been dealt with 48 | visibleSectionHeaderLayoutAttributes[section] = HeaderAttributes(layoutAttributes: newLayoutAttributes) 49 | } 50 | 51 | case .Cell: 52 | // Check if this is a cell for a section we've not dealt with yet 53 | if visibleSectionHeaderLayoutAttributes[section] == nil { 54 | // Stored a struct for this cell's section so we can can fill it out later if needed 55 | visibleSectionHeaderLayoutAttributes[section] = HeaderAttributes(layoutAttributes: nil) 56 | } 57 | 58 | case .DecorationView: 59 | break 60 | } 61 | } 62 | 63 | // Loop through the sections we've found 64 | for (section, headerAttributes) in visibleSectionHeaderLayoutAttributes { 65 | // If the header for this section hasn't been set up, do it now 66 | if headerAttributes.layoutAttributes == nil { 67 | let newAttributes = layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionHeader, atIndexPath: NSIndexPath(forItem: 0, inSection: section)) 68 | elementsLayoutAttributes!.append(newAttributes) 69 | } 70 | } 71 | 72 | return elementsLayoutAttributes 73 | } 74 | 75 | 76 | 77 | 78 | 79 | override func layoutAttributesForSupplementaryViewOfKind(elementKind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! { 80 | // Get the layout attributes for a standard flow layout 81 | let attributes = super.layoutAttributesForSupplementaryViewOfKind(elementKind, atIndexPath: indexPath) 82 | 83 | // If this is a header, we should tweak it's attributes 84 | if elementKind == UICollectionElementKindSectionHeader { 85 | if let fullSectionFrame = frameForSection(indexPath.section) { 86 | let minimumY = max(collectionView!.contentOffset.y + collectionView!.contentInset.top, fullSectionFrame.origin.y) 87 | let maximumY = CGRectGetMaxY(fullSectionFrame) - headerReferenceSize.height - collectionView!.contentInset.bottom 88 | 89 | attributes.frame = CGRect(x: 0, y: min(minimumY, maximumY), width: collectionView!.bounds.size.width, height: headerReferenceSize.height) 90 | attributes.zIndex = 1 91 | } 92 | } 93 | 94 | return attributes 95 | } 96 | 97 | 98 | 99 | 100 | 101 | // MARK: Private helper methods 102 | 103 | private func frameForSection(section: Int) -> CGRect? { 104 | 105 | // Sanity check 106 | let numberOfItems = collectionView!.numberOfItemsInSection(section) 107 | if numberOfItems == 0 { 108 | return nil 109 | } 110 | 111 | // Get the index paths for the first and last cell in the section 112 | let firstIndexPath = NSIndexPath(forRow: 0, inSection: section) 113 | let lastIndexPath = numberOfItems == 0 ? firstIndexPath : NSIndexPath(forRow: numberOfItems - 1, inSection: section) 114 | 115 | // Work out the top of the first cell and bottom of the last cell 116 | var firstCellTop = layoutAttributesForItemAtIndexPath(firstIndexPath).frame.origin.y 117 | let lastCellBottom = CGRectGetMaxY(layoutAttributesForItemAtIndexPath(lastIndexPath).frame) 118 | 119 | // Build the frame for the section 120 | var frame = CGRectZero 121 | 122 | frame.size.width = collectionView!.bounds.size.width 123 | frame.origin.y = firstCellTop 124 | frame.size.height = lastCellBottom - firstCellTop 125 | 126 | // Increase the frame to allow space for the header 127 | frame.origin.y -= headerReferenceSize.height 128 | frame.size.height += headerReferenceSize.height 129 | 130 | // Increase the frame to allow space for an section insets 131 | frame.origin.y -= sectionInset.top 132 | frame.size.height += sectionInset.top 133 | 134 | frame.size.height += sectionInset.bottom 135 | 136 | return frame 137 | } 138 | 139 | } 140 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | This is the sample project to accompany the blog post '[Collection view sticky headers](http://dativestudios.com/blog/2015/01/10/collection_view_sticky_headers/)'. --------------------------------------------------------------------------------