├── .gitignore ├── LICENSE ├── LXMWaterfallLayout.podspec ├── LXMWaterfallLayout.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── LXMWaterfallLayout ├── AppDelegate.swift ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── CommonView │ ├── TestCollectionViewCell.swift │ ├── TestCollectionViewCell.xib │ ├── TestSectionView.swift │ └── TestSectionView.xib ├── DemoListViewController.swift ├── DemoViewControllers │ ├── DemoBaseViewController.swift │ ├── FlowViewController.swift │ ├── HeaderFooterViewController.swift │ ├── HorizontalMenuViewController.swift │ ├── MyTestViewController.swift │ ├── SpecifiedWidthWaterfallViewController.swift │ └── WaterfallViewController.swift ├── Info.plist ├── LXMWaterfallLayout-Bridging-Header.h ├── LXMWaterfallLayout │ ├── LXMHeaderFooterFlowLayout.swift │ ├── LXMHorizontalMenuLayout.swift │ ├── LXMLayoutHeaderFooterProtocol.swift │ └── LXMWaterfallLayout.swift ├── MyTestLayout.swift ├── TestObject.h ├── TestObject.m └── TestViewController.swift ├── README.md └── ScreenShots └── LXMWaterfallLayout.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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 luxiaoming 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 | -------------------------------------------------------------------------------- /LXMWaterfallLayout.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "LXMWaterfallLayout" 3 | s.version = "1.0.6" 4 | s.summary = "A collectionViewLayout layout cells like waterfall, which add the missing collectionViewHeader and collectionViewFooter" 5 | s.description = <<-DESC 6 | A collectionViewLayout layout cells like waterfall, which add the missing collectionViewHeader and collectionViewFooter. 7 | 8 | LXMWaterfallLayout is inspired by CHTCollectionViewWaterfallLayout, and made several improvements to make it easier to use. It is subclass of UICollectionViewLayout and it's usage is just like UICollectionViewFlowLayout. 9 | DESC 10 | s.homepage = "https://github.com/Phelthas/LXMWaterfallLayout" 11 | s.license = "MIT" 12 | s.author = { "Phelthas" => "billthas@gmail.com" } 13 | s.platform = :ios, "8.0" 14 | s.swift_version = "5.0" 15 | s.source = { :git => "https://github.com/Phelthas/LXMWaterfallLayout.git", :tag => "1.0.6" } 16 | s.source_files = "LXMWaterfallLayout/LXMWaterfallLayout/*.swift" 17 | s.exclude_files = "Classes/Exclude" 18 | # s.public_header_files = "OJASwiftKitDemo/OJASwiftKit/**/*.swift" 19 | # s.resources = "Resources/*.png" 20 | s.frameworks = "Foundation", "UIKit" 21 | # s.libraries = "iconv", "xml2" 22 | s.requires_arc = true 23 | # s.xcconfig = { "HEADER_SEARCH_PATHS" => "$(SDKROOT)/usr/include/libxml2" } 24 | # s.dependency "JSONKit", "~> 1.4" 25 | 26 | end 27 | -------------------------------------------------------------------------------- /LXMWaterfallLayout.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 438100CA1F4D760700ABC6BA /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438100C91F4D760700ABC6BA /* AppDelegate.swift */; }; 11 | 438100CF1F4D760700ABC6BA /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 438100CD1F4D760700ABC6BA /* Main.storyboard */; }; 12 | 438100D11F4D760700ABC6BA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 438100D01F4D760700ABC6BA /* Assets.xcassets */; }; 13 | 438100D41F4D760700ABC6BA /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 438100D21F4D760700ABC6BA /* LaunchScreen.storyboard */; }; 14 | 439423621F514EC900E0A098 /* DemoListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439423611F514EC900E0A098 /* DemoListViewController.swift */; }; 15 | 4394236B1F51542400E0A098 /* TestCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439423641F51542400E0A098 /* TestCollectionViewCell.swift */; }; 16 | 4394236C1F51542400E0A098 /* TestCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 439423651F51542400E0A098 /* TestCollectionViewCell.xib */; }; 17 | 4394236D1F51542400E0A098 /* TestSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439423661F51542400E0A098 /* TestSectionView.swift */; }; 18 | 4394236E1F51542400E0A098 /* TestSectionView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 439423671F51542400E0A098 /* TestSectionView.xib */; }; 19 | 439423721F51547C00E0A098 /* DemoBaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439423711F51547C00E0A098 /* DemoBaseViewController.swift */; }; 20 | 439423741F51550F00E0A098 /* WaterfallViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439423731F51550F00E0A098 /* WaterfallViewController.swift */; }; 21 | 439423761F51573500E0A098 /* FlowViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439423751F51573500E0A098 /* FlowViewController.swift */; }; 22 | 439423781F51591C00E0A098 /* HeaderFooterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439423771F51591C00E0A098 /* HeaderFooterViewController.swift */; }; 23 | 4394237C1F516BAB00E0A098 /* LXMLayoutHeaderFooterProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4394237B1F516BAB00E0A098 /* LXMLayoutHeaderFooterProtocol.swift */; }; 24 | 4394237E1F5183B100E0A098 /* TestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4394237D1F5183B100E0A098 /* TestViewController.swift */; }; 25 | 43A4643F2622F7F7007A418D /* SpecifiedWidthWaterfallViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43A4643E2622F7F7007A418D /* SpecifiedWidthWaterfallViewController.swift */; }; 26 | 43D57B051F5FCEC500541E5C /* MyTestLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43D57B041F5FCEC500541E5C /* MyTestLayout.swift */; }; 27 | 43D57B071F5FD7B000541E5C /* MyTestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43D57B061F5FD7B000541E5C /* MyTestViewController.swift */; }; 28 | 43D591FB1F53B7E700592CF9 /* TestObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 43D591FA1F53B7E700592CF9 /* TestObject.m */; }; 29 | 43E64B681F5010E500C6391C /* LXMWaterfallLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43E64B671F5010E500C6391C /* LXMWaterfallLayout.swift */; }; 30 | 43E842A71F5699C5007AD5DB /* LXMHeaderFooterFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43E842A61F5699C5007AD5DB /* LXMHeaderFooterFlowLayout.swift */; }; 31 | 43FB9E7626EE320D009B78FF /* LXMHorizontalMenuLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43FB9E7526EE320D009B78FF /* LXMHorizontalMenuLayout.swift */; }; 32 | 43FB9E7826EE5AA9009B78FF /* HorizontalMenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43FB9E7726EE5AA9009B78FF /* HorizontalMenuViewController.swift */; }; 33 | /* End PBXBuildFile section */ 34 | 35 | /* Begin PBXFileReference section */ 36 | 438100C61F4D760700ABC6BA /* LXMWaterfallLayout.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LXMWaterfallLayout.app; sourceTree = BUILT_PRODUCTS_DIR; }; 37 | 438100C91F4D760700ABC6BA /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 38 | 438100CE1F4D760700ABC6BA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 39 | 438100D01F4D760700ABC6BA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 40 | 438100D31F4D760700ABC6BA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 41 | 438100D51F4D760700ABC6BA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 42 | 439423611F514EC900E0A098 /* DemoListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DemoListViewController.swift; sourceTree = ""; }; 43 | 439423641F51542400E0A098 /* TestCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestCollectionViewCell.swift; sourceTree = ""; }; 44 | 439423651F51542400E0A098 /* TestCollectionViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = TestCollectionViewCell.xib; sourceTree = ""; }; 45 | 439423661F51542400E0A098 /* TestSectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestSectionView.swift; sourceTree = ""; }; 46 | 439423671F51542400E0A098 /* TestSectionView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = TestSectionView.xib; sourceTree = ""; }; 47 | 439423711F51547C00E0A098 /* DemoBaseViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DemoBaseViewController.swift; sourceTree = ""; }; 48 | 439423731F51550F00E0A098 /* WaterfallViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WaterfallViewController.swift; sourceTree = ""; }; 49 | 439423751F51573500E0A098 /* FlowViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlowViewController.swift; sourceTree = ""; }; 50 | 439423771F51591C00E0A098 /* HeaderFooterViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeaderFooterViewController.swift; sourceTree = ""; }; 51 | 4394237B1F516BAB00E0A098 /* LXMLayoutHeaderFooterProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LXMLayoutHeaderFooterProtocol.swift; sourceTree = ""; }; 52 | 4394237D1F5183B100E0A098 /* TestViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestViewController.swift; sourceTree = ""; }; 53 | 43A4643E2622F7F7007A418D /* SpecifiedWidthWaterfallViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecifiedWidthWaterfallViewController.swift; sourceTree = ""; }; 54 | 43D57B041F5FCEC500541E5C /* MyTestLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MyTestLayout.swift; sourceTree = ""; }; 55 | 43D57B061F5FD7B000541E5C /* MyTestViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MyTestViewController.swift; sourceTree = ""; }; 56 | 43D591F81F53B7E700592CF9 /* LXMWaterfallLayout-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "LXMWaterfallLayout-Bridging-Header.h"; sourceTree = ""; }; 57 | 43D591F91F53B7E700592CF9 /* TestObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TestObject.h; sourceTree = ""; }; 58 | 43D591FA1F53B7E700592CF9 /* TestObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TestObject.m; sourceTree = ""; }; 59 | 43E64B671F5010E500C6391C /* LXMWaterfallLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LXMWaterfallLayout.swift; sourceTree = ""; }; 60 | 43E842A61F5699C5007AD5DB /* LXMHeaderFooterFlowLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LXMHeaderFooterFlowLayout.swift; sourceTree = ""; }; 61 | 43FB9E7526EE320D009B78FF /* LXMHorizontalMenuLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LXMHorizontalMenuLayout.swift; sourceTree = ""; }; 62 | 43FB9E7726EE5AA9009B78FF /* HorizontalMenuViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HorizontalMenuViewController.swift; sourceTree = ""; }; 63 | /* End PBXFileReference section */ 64 | 65 | /* Begin PBXFrameworksBuildPhase section */ 66 | 438100C31F4D760700ABC6BA /* Frameworks */ = { 67 | isa = PBXFrameworksBuildPhase; 68 | buildActionMask = 2147483647; 69 | files = ( 70 | ); 71 | runOnlyForDeploymentPostprocessing = 0; 72 | }; 73 | /* End PBXFrameworksBuildPhase section */ 74 | 75 | /* Begin PBXGroup section */ 76 | 438100BD1F4D760700ABC6BA = { 77 | isa = PBXGroup; 78 | children = ( 79 | 438100C81F4D760700ABC6BA /* LXMWaterfallLayout */, 80 | 438100C71F4D760700ABC6BA /* Products */, 81 | ); 82 | sourceTree = ""; 83 | }; 84 | 438100C71F4D760700ABC6BA /* Products */ = { 85 | isa = PBXGroup; 86 | children = ( 87 | 438100C61F4D760700ABC6BA /* LXMWaterfallLayout.app */, 88 | ); 89 | name = Products; 90 | sourceTree = ""; 91 | }; 92 | 438100C81F4D760700ABC6BA /* LXMWaterfallLayout */ = { 93 | isa = PBXGroup; 94 | children = ( 95 | 438100C91F4D760700ABC6BA /* AppDelegate.swift */, 96 | 439423611F514EC900E0A098 /* DemoListViewController.swift */, 97 | 4394237D1F5183B100E0A098 /* TestViewController.swift */, 98 | 43D57B041F5FCEC500541E5C /* MyTestLayout.swift */, 99 | 43D591F91F53B7E700592CF9 /* TestObject.h */, 100 | 43D591FA1F53B7E700592CF9 /* TestObject.m */, 101 | 439423631F51542400E0A098 /* CommonView */, 102 | 439423681F51542400E0A098 /* DemoViewControllers */, 103 | 43E64B661F5010E500C6391C /* LXMWaterfallLayout */, 104 | 438100CD1F4D760700ABC6BA /* Main.storyboard */, 105 | 438100D01F4D760700ABC6BA /* Assets.xcassets */, 106 | 438100D21F4D760700ABC6BA /* LaunchScreen.storyboard */, 107 | 438100D51F4D760700ABC6BA /* Info.plist */, 108 | 43D591F81F53B7E700592CF9 /* LXMWaterfallLayout-Bridging-Header.h */, 109 | ); 110 | path = LXMWaterfallLayout; 111 | sourceTree = ""; 112 | }; 113 | 439423631F51542400E0A098 /* CommonView */ = { 114 | isa = PBXGroup; 115 | children = ( 116 | 439423641F51542400E0A098 /* TestCollectionViewCell.swift */, 117 | 439423651F51542400E0A098 /* TestCollectionViewCell.xib */, 118 | 439423661F51542400E0A098 /* TestSectionView.swift */, 119 | 439423671F51542400E0A098 /* TestSectionView.xib */, 120 | ); 121 | path = CommonView; 122 | sourceTree = ""; 123 | }; 124 | 439423681F51542400E0A098 /* DemoViewControllers */ = { 125 | isa = PBXGroup; 126 | children = ( 127 | 439423711F51547C00E0A098 /* DemoBaseViewController.swift */, 128 | 439423731F51550F00E0A098 /* WaterfallViewController.swift */, 129 | 43A4643E2622F7F7007A418D /* SpecifiedWidthWaterfallViewController.swift */, 130 | 439423771F51591C00E0A098 /* HeaderFooterViewController.swift */, 131 | 439423751F51573500E0A098 /* FlowViewController.swift */, 132 | 43FB9E7726EE5AA9009B78FF /* HorizontalMenuViewController.swift */, 133 | 43D57B061F5FD7B000541E5C /* MyTestViewController.swift */, 134 | ); 135 | path = DemoViewControllers; 136 | sourceTree = ""; 137 | }; 138 | 43E64B661F5010E500C6391C /* LXMWaterfallLayout */ = { 139 | isa = PBXGroup; 140 | children = ( 141 | 4394237B1F516BAB00E0A098 /* LXMLayoutHeaderFooterProtocol.swift */, 142 | 43E842A61F5699C5007AD5DB /* LXMHeaderFooterFlowLayout.swift */, 143 | 43E64B671F5010E500C6391C /* LXMWaterfallLayout.swift */, 144 | 43FB9E7526EE320D009B78FF /* LXMHorizontalMenuLayout.swift */, 145 | ); 146 | path = LXMWaterfallLayout; 147 | sourceTree = ""; 148 | }; 149 | /* End PBXGroup section */ 150 | 151 | /* Begin PBXNativeTarget section */ 152 | 438100C51F4D760700ABC6BA /* LXMWaterfallLayout */ = { 153 | isa = PBXNativeTarget; 154 | buildConfigurationList = 438100D81F4D760700ABC6BA /* Build configuration list for PBXNativeTarget "LXMWaterfallLayout" */; 155 | buildPhases = ( 156 | 438100C21F4D760700ABC6BA /* Sources */, 157 | 438100C31F4D760700ABC6BA /* Frameworks */, 158 | 438100C41F4D760700ABC6BA /* Resources */, 159 | ); 160 | buildRules = ( 161 | ); 162 | dependencies = ( 163 | ); 164 | name = LXMWaterfallLayout; 165 | productName = LXMWaterfallLayout; 166 | productReference = 438100C61F4D760700ABC6BA /* LXMWaterfallLayout.app */; 167 | productType = "com.apple.product-type.application"; 168 | }; 169 | /* End PBXNativeTarget section */ 170 | 171 | /* Begin PBXProject section */ 172 | 438100BE1F4D760700ABC6BA /* Project object */ = { 173 | isa = PBXProject; 174 | attributes = { 175 | LastSwiftUpdateCheck = 0830; 176 | LastUpgradeCheck = 1020; 177 | ORGANIZATIONNAME = duowan; 178 | TargetAttributes = { 179 | 438100C51F4D760700ABC6BA = { 180 | CreatedOnToolsVersion = 8.3.3; 181 | DevelopmentTeam = 993SNGFP2R; 182 | LastSwiftMigration = 1020; 183 | ProvisioningStyle = Automatic; 184 | }; 185 | }; 186 | }; 187 | buildConfigurationList = 438100C11F4D760700ABC6BA /* Build configuration list for PBXProject "LXMWaterfallLayout" */; 188 | compatibilityVersion = "Xcode 3.2"; 189 | developmentRegion = en; 190 | hasScannedForEncodings = 0; 191 | knownRegions = ( 192 | en, 193 | Base, 194 | ); 195 | mainGroup = 438100BD1F4D760700ABC6BA; 196 | productRefGroup = 438100C71F4D760700ABC6BA /* Products */; 197 | projectDirPath = ""; 198 | projectRoot = ""; 199 | targets = ( 200 | 438100C51F4D760700ABC6BA /* LXMWaterfallLayout */, 201 | ); 202 | }; 203 | /* End PBXProject section */ 204 | 205 | /* Begin PBXResourcesBuildPhase section */ 206 | 438100C41F4D760700ABC6BA /* Resources */ = { 207 | isa = PBXResourcesBuildPhase; 208 | buildActionMask = 2147483647; 209 | files = ( 210 | 438100D41F4D760700ABC6BA /* LaunchScreen.storyboard in Resources */, 211 | 4394236E1F51542400E0A098 /* TestSectionView.xib in Resources */, 212 | 4394236C1F51542400E0A098 /* TestCollectionViewCell.xib in Resources */, 213 | 438100D11F4D760700ABC6BA /* Assets.xcassets in Resources */, 214 | 438100CF1F4D760700ABC6BA /* Main.storyboard in Resources */, 215 | ); 216 | runOnlyForDeploymentPostprocessing = 0; 217 | }; 218 | /* End PBXResourcesBuildPhase section */ 219 | 220 | /* Begin PBXSourcesBuildPhase section */ 221 | 438100C21F4D760700ABC6BA /* Sources */ = { 222 | isa = PBXSourcesBuildPhase; 223 | buildActionMask = 2147483647; 224 | files = ( 225 | 43D57B071F5FD7B000541E5C /* MyTestViewController.swift in Sources */, 226 | 438100CA1F4D760700ABC6BA /* AppDelegate.swift in Sources */, 227 | 4394236B1F51542400E0A098 /* TestCollectionViewCell.swift in Sources */, 228 | 43A4643F2622F7F7007A418D /* SpecifiedWidthWaterfallViewController.swift in Sources */, 229 | 43D591FB1F53B7E700592CF9 /* TestObject.m in Sources */, 230 | 439423781F51591C00E0A098 /* HeaderFooterViewController.swift in Sources */, 231 | 4394237C1F516BAB00E0A098 /* LXMLayoutHeaderFooterProtocol.swift in Sources */, 232 | 43FB9E7826EE5AA9009B78FF /* HorizontalMenuViewController.swift in Sources */, 233 | 439423761F51573500E0A098 /* FlowViewController.swift in Sources */, 234 | 4394236D1F51542400E0A098 /* TestSectionView.swift in Sources */, 235 | 439423741F51550F00E0A098 /* WaterfallViewController.swift in Sources */, 236 | 43E64B681F5010E500C6391C /* LXMWaterfallLayout.swift in Sources */, 237 | 439423721F51547C00E0A098 /* DemoBaseViewController.swift in Sources */, 238 | 4394237E1F5183B100E0A098 /* TestViewController.swift in Sources */, 239 | 43E842A71F5699C5007AD5DB /* LXMHeaderFooterFlowLayout.swift in Sources */, 240 | 43FB9E7626EE320D009B78FF /* LXMHorizontalMenuLayout.swift in Sources */, 241 | 439423621F514EC900E0A098 /* DemoListViewController.swift in Sources */, 242 | 43D57B051F5FCEC500541E5C /* MyTestLayout.swift in Sources */, 243 | ); 244 | runOnlyForDeploymentPostprocessing = 0; 245 | }; 246 | /* End PBXSourcesBuildPhase section */ 247 | 248 | /* Begin PBXVariantGroup section */ 249 | 438100CD1F4D760700ABC6BA /* Main.storyboard */ = { 250 | isa = PBXVariantGroup; 251 | children = ( 252 | 438100CE1F4D760700ABC6BA /* Base */, 253 | ); 254 | name = Main.storyboard; 255 | sourceTree = ""; 256 | }; 257 | 438100D21F4D760700ABC6BA /* LaunchScreen.storyboard */ = { 258 | isa = PBXVariantGroup; 259 | children = ( 260 | 438100D31F4D760700ABC6BA /* Base */, 261 | ); 262 | name = LaunchScreen.storyboard; 263 | sourceTree = ""; 264 | }; 265 | /* End PBXVariantGroup section */ 266 | 267 | /* Begin XCBuildConfiguration section */ 268 | 438100D61F4D760700ABC6BA /* Debug */ = { 269 | isa = XCBuildConfiguration; 270 | buildSettings = { 271 | ALWAYS_SEARCH_USER_PATHS = NO; 272 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 273 | CLANG_ANALYZER_NONNULL = YES; 274 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 275 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 276 | CLANG_CXX_LIBRARY = "libc++"; 277 | CLANG_ENABLE_MODULES = YES; 278 | CLANG_ENABLE_OBJC_ARC = YES; 279 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 280 | CLANG_WARN_BOOL_CONVERSION = YES; 281 | CLANG_WARN_COMMA = YES; 282 | CLANG_WARN_CONSTANT_CONVERSION = YES; 283 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 284 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 285 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 286 | CLANG_WARN_EMPTY_BODY = YES; 287 | CLANG_WARN_ENUM_CONVERSION = YES; 288 | CLANG_WARN_INFINITE_RECURSION = YES; 289 | CLANG_WARN_INT_CONVERSION = YES; 290 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 291 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 292 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 293 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 294 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 295 | CLANG_WARN_STRICT_PROTOTYPES = YES; 296 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 297 | CLANG_WARN_UNREACHABLE_CODE = YES; 298 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 299 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 300 | COPY_PHASE_STRIP = NO; 301 | DEBUG_INFORMATION_FORMAT = dwarf; 302 | ENABLE_STRICT_OBJC_MSGSEND = YES; 303 | ENABLE_TESTABILITY = YES; 304 | GCC_C_LANGUAGE_STANDARD = gnu99; 305 | GCC_DYNAMIC_NO_PIC = NO; 306 | GCC_NO_COMMON_BLOCKS = YES; 307 | GCC_OPTIMIZATION_LEVEL = 0; 308 | GCC_PREPROCESSOR_DEFINITIONS = ( 309 | "DEBUG=1", 310 | "$(inherited)", 311 | ); 312 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 313 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 314 | GCC_WARN_UNDECLARED_SELECTOR = YES; 315 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 316 | GCC_WARN_UNUSED_FUNCTION = YES; 317 | GCC_WARN_UNUSED_VARIABLE = YES; 318 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 319 | MTL_ENABLE_DEBUG_INFO = YES; 320 | ONLY_ACTIVE_ARCH = YES; 321 | SDKROOT = iphoneos; 322 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 323 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 324 | }; 325 | name = Debug; 326 | }; 327 | 438100D71F4D760700ABC6BA /* Release */ = { 328 | isa = XCBuildConfiguration; 329 | buildSettings = { 330 | ALWAYS_SEARCH_USER_PATHS = NO; 331 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 332 | CLANG_ANALYZER_NONNULL = YES; 333 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 334 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 335 | CLANG_CXX_LIBRARY = "libc++"; 336 | CLANG_ENABLE_MODULES = YES; 337 | CLANG_ENABLE_OBJC_ARC = YES; 338 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 339 | CLANG_WARN_BOOL_CONVERSION = YES; 340 | CLANG_WARN_COMMA = YES; 341 | CLANG_WARN_CONSTANT_CONVERSION = YES; 342 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 343 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 344 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 345 | CLANG_WARN_EMPTY_BODY = YES; 346 | CLANG_WARN_ENUM_CONVERSION = YES; 347 | CLANG_WARN_INFINITE_RECURSION = YES; 348 | CLANG_WARN_INT_CONVERSION = YES; 349 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 350 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 351 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 352 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 353 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 354 | CLANG_WARN_STRICT_PROTOTYPES = YES; 355 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 356 | CLANG_WARN_UNREACHABLE_CODE = YES; 357 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 358 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 359 | COPY_PHASE_STRIP = NO; 360 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 361 | ENABLE_NS_ASSERTIONS = NO; 362 | ENABLE_STRICT_OBJC_MSGSEND = YES; 363 | GCC_C_LANGUAGE_STANDARD = gnu99; 364 | GCC_NO_COMMON_BLOCKS = YES; 365 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 366 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 367 | GCC_WARN_UNDECLARED_SELECTOR = YES; 368 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 369 | GCC_WARN_UNUSED_FUNCTION = YES; 370 | GCC_WARN_UNUSED_VARIABLE = YES; 371 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 372 | MTL_ENABLE_DEBUG_INFO = NO; 373 | SDKROOT = iphoneos; 374 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 375 | VALIDATE_PRODUCT = YES; 376 | }; 377 | name = Release; 378 | }; 379 | 438100D91F4D760700ABC6BA /* Debug */ = { 380 | isa = XCBuildConfiguration; 381 | buildSettings = { 382 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 383 | CLANG_ENABLE_MODULES = YES; 384 | DEVELOPMENT_TEAM = 993SNGFP2R; 385 | INFOPLIST_FILE = LXMWaterfallLayout/Info.plist; 386 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 387 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 388 | PRODUCT_BUNDLE_IDENTIFIER = com.ouj.LXMWaterfallLayout; 389 | PRODUCT_NAME = "$(TARGET_NAME)"; 390 | SWIFT_OBJC_BRIDGING_HEADER = "LXMWaterfallLayout/LXMWaterfallLayout-Bridging-Header.h"; 391 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 392 | SWIFT_VERSION = 5.0; 393 | }; 394 | name = Debug; 395 | }; 396 | 438100DA1F4D760700ABC6BA /* Release */ = { 397 | isa = XCBuildConfiguration; 398 | buildSettings = { 399 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 400 | CLANG_ENABLE_MODULES = YES; 401 | DEVELOPMENT_TEAM = 993SNGFP2R; 402 | INFOPLIST_FILE = LXMWaterfallLayout/Info.plist; 403 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 404 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 405 | PRODUCT_BUNDLE_IDENTIFIER = com.ouj.LXMWaterfallLayout; 406 | PRODUCT_NAME = "$(TARGET_NAME)"; 407 | SWIFT_OBJC_BRIDGING_HEADER = "LXMWaterfallLayout/LXMWaterfallLayout-Bridging-Header.h"; 408 | SWIFT_VERSION = 5.0; 409 | }; 410 | name = Release; 411 | }; 412 | /* End XCBuildConfiguration section */ 413 | 414 | /* Begin XCConfigurationList section */ 415 | 438100C11F4D760700ABC6BA /* Build configuration list for PBXProject "LXMWaterfallLayout" */ = { 416 | isa = XCConfigurationList; 417 | buildConfigurations = ( 418 | 438100D61F4D760700ABC6BA /* Debug */, 419 | 438100D71F4D760700ABC6BA /* Release */, 420 | ); 421 | defaultConfigurationIsVisible = 0; 422 | defaultConfigurationName = Release; 423 | }; 424 | 438100D81F4D760700ABC6BA /* Build configuration list for PBXNativeTarget "LXMWaterfallLayout" */ = { 425 | isa = XCConfigurationList; 426 | buildConfigurations = ( 427 | 438100D91F4D760700ABC6BA /* Debug */, 428 | 438100DA1F4D760700ABC6BA /* Release */, 429 | ); 430 | defaultConfigurationIsVisible = 0; 431 | defaultConfigurationName = Release; 432 | }; 433 | /* End XCConfigurationList section */ 434 | }; 435 | rootObject = 438100BE1F4D760700ABC6BA /* Project object */; 436 | } 437 | -------------------------------------------------------------------------------- /LXMWaterfallLayout.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LXMWaterfallLayout.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LXMWaterfallLayout/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // LXMWaterfallLayout 4 | // 5 | // Created by luxiaoming on 2017/8/23. 6 | // Copyright © 2017年 duowan. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /LXMWaterfallLayout/Assets.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 | } -------------------------------------------------------------------------------- /LXMWaterfallLayout/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 | 27 | 28 | -------------------------------------------------------------------------------- /LXMWaterfallLayout/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 | -------------------------------------------------------------------------------- /LXMWaterfallLayout/CommonView/TestCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestCollectionViewCell.swift 3 | // LXMWaterfallLayout 4 | // 5 | // Created by luxiaoming on 2017/8/24. 6 | // Copyright © 2017年 duowan. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | 12 | let TestCollectionViewCellIdentifier: String = "TestCollectionViewCell" 13 | 14 | class TestCollectionViewCell: UICollectionViewCell { 15 | 16 | @IBOutlet weak var nameLabel: UILabel! 17 | 18 | override func awakeFromNib() { 19 | super.awakeFromNib() 20 | // Initialization code 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /LXMWaterfallLayout/CommonView/TestCollectionViewCell.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 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /LXMWaterfallLayout/CommonView/TestSectionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestSectionView.swift 3 | // LXMWaterfallLayout 4 | // 5 | // Created by luxiaoming on 2017/8/24. 6 | // Copyright © 2017年 duowan. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | let TestSectionViewIdentifier: String = "TestSectionView" 12 | 13 | class TestSectionView: UICollectionReusableView { 14 | 15 | @IBOutlet weak var nameLabel: UILabel! 16 | 17 | 18 | override func awakeFromNib() { 19 | super.awakeFromNib() 20 | // Initialization code 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /LXMWaterfallLayout/CommonView/TestSectionView.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /LXMWaterfallLayout/DemoListViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DemoListViewController.swift 3 | // LXMWaterfallLayout 4 | // 5 | // Created by luxiaoming on 2017/8/26. 6 | // Copyright © 2017年 duowan. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | private let UITableViewCellReuseIdentifier: String = "UITableViewCell" 12 | 13 | class DemoListViewController: UIViewController { 14 | 15 | fileprivate lazy var tableView: UITableView = { 16 | let tableView = UITableView(frame: self.view.bounds) 17 | tableView.register(UITableViewCell.classForCoder(), forCellReuseIdentifier: UITableViewCellReuseIdentifier) 18 | tableView.delegate = self 19 | tableView.dataSource = self 20 | return tableView 21 | }() 22 | 23 | fileprivate var dataArray: [String] = ["LXMWaterfallLayout", 24 | "LXMHeaderFooterFlowLayout", 25 | "Horizontal", 26 | "MyTestLayout", 27 | "UICollectionViewFlowLayout", 28 | "SpecifiedWidthWaterfallViewController", 29 | "HorizontalMenuViewController", 30 | "Test"] 31 | 32 | } 33 | 34 | // MARK: - Lifecycle 35 | extension DemoListViewController { 36 | 37 | override func viewDidLoad() { 38 | super.viewDidLoad() 39 | self.view.addSubview(tableView) 40 | 41 | } 42 | 43 | override func didReceiveMemoryWarning() { 44 | super.didReceiveMemoryWarning() 45 | // Dispose of any resources that can be recreated. 46 | } 47 | } 48 | 49 | 50 | // MARK: - UITableViewDataSource 51 | extension DemoListViewController: UITableViewDataSource { 52 | 53 | 54 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 55 | return self.dataArray.count 56 | } 57 | 58 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 59 | let cell = tableView.dequeueReusableCell(withIdentifier: UITableViewCellReuseIdentifier, for: indexPath) 60 | cell.textLabel?.text = dataArray[indexPath.row] 61 | return cell 62 | } 63 | 64 | } 65 | 66 | 67 | // MARK: - UITableViewDelegate 68 | extension DemoListViewController: UITableViewDelegate { 69 | 70 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 71 | 72 | var viewController = DemoBaseViewController() 73 | if indexPath.row == 0 { 74 | viewController = WaterfallViewController() 75 | } else if indexPath.row == 1 { 76 | viewController = HeaderFooterViewController() 77 | } else if indexPath.row == 2 { 78 | viewController = HeaderFooterViewController() 79 | (viewController as! HeaderFooterViewController).scrollDirection = .horizontal 80 | } else if indexPath.row == 3 { 81 | viewController = MyTestViewController() 82 | } else if indexPath.row == 4 { 83 | viewController = FlowViewController() 84 | } else if indexPath.row == 5 { 85 | viewController = SpecifiedWidthWaterfallViewController() 86 | } else if indexPath.row == 6 { 87 | viewController = HorizontalMenuViewController() 88 | } 89 | else { 90 | viewController = TestViewController() 91 | } 92 | 93 | self.navigationController?.pushViewController(viewController, animated: true) 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /LXMWaterfallLayout/DemoViewControllers/DemoBaseViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DemoBaseViewController.swift 3 | // LXMWaterfallLayout 4 | // 5 | // Created by luxiaoming on 2017/8/26. 6 | // Copyright © 2017年 duowan. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class DemoBaseViewController: UIViewController { 12 | 13 | lazy var collectionView: UICollectionView = { 14 | let layout = UICollectionViewFlowLayout() 15 | let collectionView = UICollectionView(frame: self.view.bounds, collectionViewLayout: layout) 16 | collectionView.backgroundColor = UIColor.white 17 | collectionView.delegate = self 18 | collectionView.dataSource = self 19 | let nib = UINib(nibName: "TestCollectionViewCell", bundle: nil) 20 | collectionView.register(nib, forCellWithReuseIdentifier: TestCollectionViewCellIdentifier) 21 | return collectionView 22 | }() 23 | 24 | 25 | var dataArray: [String] = { 26 | var dataArray = [String]() 27 | for i in 0 ..< 20 { 28 | dataArray.append("\(i)") 29 | } 30 | return dataArray 31 | }() 32 | 33 | var sizeArray: [CGSize] = { 34 | var sizeArray = [CGSize]() 35 | for i in 0 ..< 20 { 36 | sizeArray.append(CGSize(width: 100, height: 100 + i % 5 * 10)) 37 | } 38 | return sizeArray 39 | }() 40 | 41 | } 42 | 43 | 44 | // MARK: - Lifecycle 45 | extension DemoBaseViewController { 46 | 47 | override func viewDidLoad() { 48 | super.viewDidLoad() 49 | self.view.addSubview(collectionView) 50 | } 51 | 52 | override func viewDidLayoutSubviews() { 53 | super.viewDidLayoutSubviews() 54 | collectionView.frame = self.view.bounds 55 | } 56 | 57 | override func didReceiveMemoryWarning() { 58 | super.didReceiveMemoryWarning() 59 | // Dispose of any resources that can be recreated. 60 | } 61 | } 62 | 63 | 64 | // MARK: - UICollectionViewDataSource 65 | extension DemoBaseViewController: UICollectionViewDataSource { 66 | 67 | func numberOfSections(in collectionView: UICollectionView) -> Int { 68 | return 3 69 | } 70 | 71 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 72 | if section == 2 { 73 | return 5 74 | } else if section == 1 { 75 | return 10 76 | } else { 77 | return dataArray.count 78 | } 79 | } 80 | 81 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 82 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TestCollectionViewCellIdentifier, for: indexPath) as! TestCollectionViewCell 83 | cell.nameLabel.text = "(\(indexPath.section),\(dataArray[indexPath.item]))" 84 | cell.backgroundColor = UIColor.orange 85 | return cell 86 | } 87 | 88 | 89 | } 90 | 91 | 92 | // MARK: - UICollectionViewDelegate 93 | extension DemoBaseViewController: UICollectionViewDelegate { 94 | 95 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 96 | print("didSelectItemAt \(indexPath)") 97 | 98 | } 99 | } 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /LXMWaterfallLayout/DemoViewControllers/FlowViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlowViewController.swift 3 | // LXMWaterfallLayout 4 | // 5 | // Created by luxiaoming on 2017/8/26. 6 | // Copyright © 2017年 duowan. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class FlowViewController: DemoBaseViewController { 12 | 13 | } 14 | 15 | 16 | // MARK: - Lifecycle 17 | extension FlowViewController { 18 | 19 | override func viewDidLoad() { 20 | super.viewDidLoad() 21 | 22 | let layout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout 23 | layout.minimumInteritemSpacing = 20 24 | layout.minimumLineSpacing = 5 25 | layout.sectionInset = UIEdgeInsets(top: 10, left: 20, bottom: 30, right: 40) 26 | layout.scrollDirection = .horizontal 27 | 28 | let sectionNib = UINib(nibName: "TestSectionView", bundle: nil) 29 | collectionView.register(sectionNib, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader , withReuseIdentifier: TestSectionViewIdentifier) 30 | collectionView.register(sectionNib, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: TestSectionViewIdentifier) 31 | 32 | collectionView.contentInset = UIEdgeInsets(top: 10, left: 20, bottom: 30, right: 40) 33 | } 34 | 35 | override func didReceiveMemoryWarning() { 36 | super.didReceiveMemoryWarning() 37 | // Dispose of any resources that can be recreated. 38 | } 39 | } 40 | 41 | 42 | // MARK: - override 43 | extension FlowViewController { 44 | 45 | func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { 46 | if kind == UICollectionView.elementKindSectionHeader { 47 | let sectionView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: TestSectionViewIdentifier, for: indexPath) as! TestSectionView 48 | sectionView.backgroundColor = UIColor.red 49 | sectionView.nameLabel.text = "sectionHeader \(indexPath.section)" 50 | return sectionView 51 | 52 | } else if kind == UICollectionView.elementKindSectionFooter { 53 | let sectionView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: TestSectionViewIdentifier, for: indexPath) as! TestSectionView 54 | sectionView.backgroundColor = UIColor.blue 55 | sectionView.nameLabel.text = "sectionFooter \(indexPath.section)" 56 | return sectionView 57 | } else { 58 | return UICollectionReusableView() 59 | } 60 | } 61 | } 62 | 63 | 64 | extension FlowViewController: UICollectionViewDelegateFlowLayout { 65 | 66 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 67 | return sizeArray[indexPath.item] 68 | } 69 | 70 | // func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { 71 | // if section == 0 { 72 | // return 20 73 | // } else if section == 1 { 74 | // return 10 75 | // } else { 76 | // return 0 77 | // } 78 | // } 79 | 80 | // func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { 81 | // if section == 0 { 82 | // return 20 83 | // } else if section == 1 { 84 | // return 10 85 | // } else { 86 | // return 0 87 | // } 88 | // } 89 | 90 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { 91 | if section == 0 { 92 | return CGSize(width: 100, height: 50) 93 | } else if section == 1 { 94 | return CGSize(width: 100, height: 100) 95 | } else { 96 | return CGSize.zero 97 | } 98 | 99 | } 100 | 101 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { 102 | if section == 0 { 103 | return CGSize(width: 100, height: 30) 104 | } else if section == 1 { 105 | return CGSize(width: 100, height: 50) 106 | } else { 107 | return CGSize.zero 108 | } 109 | } 110 | 111 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { 112 | if section == 0 { 113 | return UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) 114 | } else if section == 1 { 115 | return UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) 116 | } else { 117 | return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) 118 | } 119 | } 120 | 121 | 122 | } 123 | -------------------------------------------------------------------------------- /LXMWaterfallLayout/DemoViewControllers/HeaderFooterViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HeaderFooterViewController.swift 3 | // LXMWaterfallLayout 4 | // 5 | // Created by luxiaoming on 2017/8/26. 6 | // Copyright © 2017年 duowan. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class HeaderFooterViewController: DemoBaseViewController { 12 | var scrollDirection: UICollectionView.ScrollDirection = .vertical 13 | } 14 | 15 | // MARK: - Lifecycle 16 | extension HeaderFooterViewController { 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | 21 | // let layout = UICollectionViewFlowLayout() 22 | let layout = LXMHeaderFooterFlowLayout() 23 | layout.minimumInteritemSpacing = 20 24 | layout.minimumLineSpacing = 5 25 | layout.sectionInset = UIEdgeInsets(top: 10, left: 20, bottom: 30, right: 40) 26 | layout.collectionViewHeaderHeight = 200 27 | layout.collectionViewFooterHeight = 300 28 | layout.scrollDirection = self.scrollDirection 29 | 30 | self.collectionView.collectionViewLayout = layout 31 | 32 | let sectionNib = UINib(nibName: "TestSectionView", bundle: nil) 33 | collectionView.register(sectionNib, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader , withReuseIdentifier: TestSectionViewIdentifier) 34 | collectionView.register(sectionNib, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: TestSectionViewIdentifier) 35 | collectionView.register(sectionNib, forSupplementaryViewOfKind: LXMCollectionElementKindHeader , withReuseIdentifier: TestSectionViewIdentifier) 36 | collectionView.register(sectionNib, forSupplementaryViewOfKind: LXMCollectionElementKindFooter, withReuseIdentifier: TestSectionViewIdentifier) 37 | 38 | // collectionView.contentInset = UIEdgeInsets(top: 10, left: 20, bottom: 30, right: 40) 39 | 40 | 41 | 42 | let item = UIBarButtonItem(barButtonSystemItem: .refresh, target: self, action: #selector(changeAlignment)) 43 | self.navigationItem.rightBarButtonItem = item 44 | } 45 | 46 | @objc func changeAlignment() { 47 | if let layout = self.collectionView.collectionViewLayout as? LXMHeaderFooterFlowLayout { 48 | 49 | if self.scrollDirection == .vertical { 50 | if layout.horiziontalAlignment == .left { 51 | layout.horiziontalAlignment = .right 52 | } else if layout.horiziontalAlignment == .right { 53 | layout.horiziontalAlignment = .center 54 | } else if layout.horiziontalAlignment == .center { 55 | layout.horiziontalAlignment = .justified 56 | } else if layout.horiziontalAlignment == .justified { 57 | layout.horiziontalAlignment = .none 58 | } else if layout.horiziontalAlignment == .none { 59 | layout.horiziontalAlignment = .left 60 | } 61 | 62 | } else { 63 | if layout.verticalAlignment == .top { 64 | layout.verticalAlignment = .bottom 65 | } else if layout.verticalAlignment == .bottom { 66 | layout.verticalAlignment = .center 67 | } else if layout.verticalAlignment == .center { 68 | layout.verticalAlignment = .justified 69 | } else if layout.verticalAlignment == .justified { 70 | layout.verticalAlignment = .none 71 | } else if layout.verticalAlignment == .none { 72 | layout.verticalAlignment = .top 73 | } 74 | } 75 | self.collectionView.reloadData() 76 | 77 | } 78 | } 79 | 80 | override func didReceiveMemoryWarning() { 81 | super.didReceiveMemoryWarning() 82 | // Dispose of any resources that can be recreated. 83 | } 84 | } 85 | 86 | extension HeaderFooterViewController { 87 | func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { 88 | if kind == UICollectionView.elementKindSectionHeader { 89 | let sectionView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: TestSectionViewIdentifier, for: indexPath) as! TestSectionView 90 | sectionView.backgroundColor = UIColor.red 91 | sectionView.nameLabel.text = "sectionHeader \(indexPath.section)" 92 | return sectionView 93 | 94 | } else if kind == UICollectionView.elementKindSectionFooter { 95 | let sectionView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: TestSectionViewIdentifier, for: indexPath) as! TestSectionView 96 | sectionView.backgroundColor = UIColor.blue 97 | sectionView.nameLabel.text = "sectionFooter \(indexPath.section)" 98 | return sectionView 99 | } else if kind == LXMCollectionElementKindHeader { 100 | let sectionView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: TestSectionViewIdentifier, for: indexPath) as! TestSectionView 101 | sectionView.backgroundColor = UIColor.yellow 102 | sectionView.nameLabel.text = "collectionViewHeader" 103 | return sectionView 104 | } else if kind == LXMCollectionElementKindFooter { 105 | let sectionView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: TestSectionViewIdentifier, for: indexPath) as! TestSectionView 106 | sectionView.backgroundColor = UIColor.green 107 | sectionView.nameLabel.text = "collectionViewFooter" 108 | return sectionView 109 | } else { 110 | return UICollectionReusableView() 111 | } 112 | } 113 | } 114 | 115 | extension HeaderFooterViewController: UICollectionViewDelegateFlowLayout { 116 | 117 | // func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 118 | // return sizeArray[indexPath.item] 119 | // } 120 | 121 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { 122 | if section == 0 { 123 | return CGSize(width: 100, height: 30) 124 | } else if section == 1 { 125 | return CGSize(width: 100, height: 100) 126 | } else { 127 | return CGSize.zero 128 | } 129 | 130 | } 131 | 132 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { 133 | if section == 0 { 134 | return CGSize(width: 100, height: 30) 135 | } else if section == 1 { 136 | return CGSize(width: 100, height: 50) 137 | } else { 138 | return CGSize.zero 139 | } 140 | } 141 | 142 | } 143 | 144 | extension HeaderFooterViewController { 145 | 146 | 147 | } 148 | -------------------------------------------------------------------------------- /LXMWaterfallLayout/DemoViewControllers/HorizontalMenuViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HorizontalMenuViewController.swift 3 | // LXMWaterfallLayout 4 | // 5 | // Created by billthaslu on 2021/9/12. 6 | // Copyright © 2021 duowan. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class HorizontalMenuViewController: DemoBaseViewController { 12 | 13 | 14 | } 15 | 16 | 17 | // MARK: - Lifecycle 18 | extension HorizontalMenuViewController { 19 | 20 | override func viewDidLoad() { 21 | super.viewDidLoad() 22 | 23 | let layout = LXMHorizontalMenuLayout() 24 | layout.interitemSpacing = 10 25 | layout.sectionInset = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20) 26 | self.collectionView.setCollectionViewLayout(layout, animated: false) 27 | self.collectionView.showsHorizontalScrollIndicator = false 28 | 29 | self.dataArray = ["电影", "电视剧", "动画", "短视频", "漫画", "游戏"] 30 | } 31 | 32 | override func viewDidLayoutSubviews() { 33 | super.viewDidLayoutSubviews() 34 | self.collectionView.frame = CGRect(x: 0, y: 200, width: self.view.frame.width, height: 44) 35 | } 36 | 37 | } 38 | 39 | 40 | // MARK: - override 41 | extension HorizontalMenuViewController { 42 | 43 | override func numberOfSections(in collectionView: UICollectionView) -> Int { 44 | return 1 45 | } 46 | 47 | override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 48 | // return 1 49 | // return 2 50 | // return 3 51 | return 4 52 | return self.dataArray.count 53 | } 54 | 55 | override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 56 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TestCollectionViewCellIdentifier, for: indexPath) as! TestCollectionViewCell 57 | cell.nameLabel.text = self.dataArray[indexPath.item] 58 | cell.nameLabel.textAlignment = .center 59 | cell.backgroundColor = UIColor.orange 60 | return cell 61 | } 62 | } 63 | 64 | 65 | extension HorizontalMenuViewController: LXMHorizontalMenuLayoutDelegate { 66 | 67 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: LXMHorizontalMenuLayout, sizeForItemAt index: Int) -> CGSize { 68 | return CGSize(width: 100, height: 44) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /LXMWaterfallLayout/DemoViewControllers/MyTestViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MyTestViewController.swift 3 | // LXMWaterfallLayout 4 | // 5 | // Created by luxiaoming on 2017/9/6. 6 | // Copyright © 2017年 duowan. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class MyTestViewController: DemoBaseViewController { 12 | 13 | } 14 | 15 | 16 | // MARK: - Lifecycle 17 | extension MyTestViewController { 18 | 19 | override func viewDidLoad() { 20 | super.viewDidLoad() 21 | 22 | let layout = MyTestLayout() 23 | layout.minimumInteritemSpacing = 20 24 | layout.minimumLineSpacing = 5 25 | layout.sectionInset = UIEdgeInsets(top: 10, left: 20, bottom: 30, right: 40) 26 | layout.collectionViewHeaderHeight = 200 27 | layout.collectionViewFooterHeight = 300 28 | 29 | self.collectionView.collectionViewLayout = layout 30 | 31 | let sectionNib = UINib(nibName: "TestSectionView", bundle: nil) 32 | collectionView.register(sectionNib, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader , withReuseIdentifier: TestSectionViewIdentifier) 33 | collectionView.register(sectionNib, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: TestSectionViewIdentifier) 34 | collectionView.register(sectionNib, forSupplementaryViewOfKind: LXMCollectionElementKindHeader , withReuseIdentifier: TestSectionViewIdentifier) 35 | collectionView.register(sectionNib, forSupplementaryViewOfKind: LXMCollectionElementKindFooter, withReuseIdentifier: TestSectionViewIdentifier) 36 | 37 | // collectionView.contentInset = UIEdgeInsets(top: 10, left: 20, bottom: 30, right: 40) 38 | 39 | 40 | } 41 | 42 | override func didReceiveMemoryWarning() { 43 | super.didReceiveMemoryWarning() 44 | // Dispose of any resources that can be recreated. 45 | } 46 | } 47 | 48 | extension MyTestViewController { 49 | func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { 50 | if kind == UICollectionView.elementKindSectionHeader { 51 | let sectionView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: TestSectionViewIdentifier, for: indexPath) as! TestSectionView 52 | sectionView.backgroundColor = UIColor.red 53 | sectionView.nameLabel.text = "sectionHeader \(indexPath.section)" 54 | return sectionView 55 | 56 | } else if kind == UICollectionView.elementKindSectionFooter { 57 | let sectionView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: TestSectionViewIdentifier, for: indexPath) as! TestSectionView 58 | sectionView.backgroundColor = UIColor.blue 59 | sectionView.nameLabel.text = "sectionFooter \(indexPath.section)" 60 | return sectionView 61 | } else if kind == LXMCollectionElementKindHeader { 62 | let sectionView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: TestSectionViewIdentifier, for: indexPath) as! TestSectionView 63 | sectionView.backgroundColor = UIColor.yellow 64 | sectionView.nameLabel.text = "collectionViewHeader" 65 | return sectionView 66 | } else if kind == LXMCollectionElementKindFooter { 67 | let sectionView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: TestSectionViewIdentifier, for: indexPath) as! TestSectionView 68 | sectionView.backgroundColor = UIColor.green 69 | sectionView.nameLabel.text = "collectionViewFooter" 70 | return sectionView 71 | } else { 72 | return UICollectionReusableView() 73 | } 74 | } 75 | } 76 | 77 | extension MyTestViewController: UICollectionViewDelegateFlowLayout { 78 | 79 | // func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 80 | // return sizeArray[indexPath.item] 81 | // } 82 | 83 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { 84 | if section == 0 { 85 | return CGSize(width: 100, height: 30) 86 | } else if section == 1 { 87 | return CGSize(width: 100, height: 100) 88 | } else { 89 | return CGSize.zero 90 | } 91 | 92 | } 93 | 94 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { 95 | if section == 0 { 96 | return CGSize(width: 100, height: 30) 97 | } else if section == 1 { 98 | return CGSize(width: 100, height: 50) 99 | } else { 100 | return CGSize.zero 101 | } 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /LXMWaterfallLayout/DemoViewControllers/SpecifiedWidthWaterfallViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SpecifiedWidthWaterfallViewController.swift 3 | // LXMWaterfallLayout 4 | // 5 | // Created by billthaslu on 2021/4/11. 6 | // Copyright © 2021 duowan. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SpecifiedWidthWaterfallViewController: DemoBaseViewController { 12 | 13 | } 14 | 15 | // MARK: - Lifecycle 16 | extension SpecifiedWidthWaterfallViewController { 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | 21 | 22 | let layout = LXMWaterfallLayout() 23 | layout.columnCount = 3 24 | layout.minimumColumnSpacing = 5 25 | layout.minimumInteritemSpacing = 20 26 | layout.sectionInset = UIEdgeInsets(top: 10, left: 20, bottom: 30, right: 40) 27 | layout.sectionHeaderHeight = 100 28 | layout.sectionFooterHeight = 50 29 | layout.collectionViewHeaderHeight = 200 30 | layout.collectionViewFooterHeight = 300 31 | 32 | self.collectionView.collectionViewLayout = layout 33 | 34 | let sectionNib = UINib(nibName: "TestSectionView", bundle: nil) 35 | collectionView.register(sectionNib, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader , withReuseIdentifier: TestSectionViewIdentifier) 36 | collectionView.register(sectionNib, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: TestSectionViewIdentifier) 37 | collectionView.register(sectionNib, forSupplementaryViewOfKind: LXMCollectionElementKindHeader , withReuseIdentifier: TestSectionViewIdentifier) 38 | collectionView.register(sectionNib, forSupplementaryViewOfKind: LXMCollectionElementKindFooter, withReuseIdentifier: TestSectionViewIdentifier) 39 | 40 | collectionView.contentInset = UIEdgeInsets(top: 10, left: 20, bottom: 30, right: 40) 41 | 42 | } 43 | 44 | override func didReceiveMemoryWarning() { 45 | super.didReceiveMemoryWarning() 46 | // Dispose of any resources that can be recreated. 47 | } 48 | } 49 | 50 | 51 | 52 | // MARK: - override 53 | extension SpecifiedWidthWaterfallViewController { 54 | 55 | func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { 56 | if kind == UICollectionView.elementKindSectionHeader { 57 | let sectionView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: TestSectionViewIdentifier, for: indexPath) as! TestSectionView 58 | sectionView.backgroundColor = UIColor.red 59 | sectionView.nameLabel.text = "sectionHeader \(indexPath.section)" 60 | return sectionView 61 | 62 | } else if kind == UICollectionView.elementKindSectionFooter { 63 | let sectionView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: TestSectionViewIdentifier, for: indexPath) as! TestSectionView 64 | sectionView.backgroundColor = UIColor.blue 65 | sectionView.nameLabel.text = "sectionFooter \(indexPath.section)" 66 | return sectionView 67 | } else if kind == LXMCollectionElementKindHeader { 68 | let sectionView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: TestSectionViewIdentifier, for: indexPath) as! TestSectionView 69 | sectionView.backgroundColor = UIColor.yellow 70 | sectionView.nameLabel.text = "collectionViewHeader" 71 | return sectionView 72 | } else if kind == LXMCollectionElementKindFooter { 73 | let sectionView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: TestSectionViewIdentifier, for: indexPath) as! TestSectionView 74 | sectionView.backgroundColor = UIColor.green 75 | sectionView.nameLabel.text = "collectionViewFooter" 76 | return sectionView 77 | } else { 78 | return UICollectionReusableView() 79 | } 80 | } 81 | } 82 | 83 | 84 | extension SpecifiedWidthWaterfallViewController: LXMWaterfallLayoutDelegate { 85 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: LXMWaterfallLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 86 | return sizeArray[indexPath.item] 87 | } 88 | 89 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: LXMWaterfallLayout, columnWidthAtSection section: Int, columnIndex: Int) -> CGFloat { 90 | if columnIndex == 0 { 91 | return 120; 92 | } else if columnIndex == 1 { 93 | return 80; 94 | } else { 95 | return 50; 96 | } 97 | } 98 | 99 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: LXMWaterfallLayout, numberOfColumnsAt section: Int) -> Int { 100 | if section == 0 { 101 | return 3 102 | } else if section == 1 { 103 | return 2 104 | } else { 105 | return 1 106 | } 107 | } 108 | 109 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: LXMWaterfallLayout, minimumColumnSpacingForSectionAt section: Int) -> CGFloat { 110 | if section == 0 { 111 | return 20 112 | } else if section == 1 { 113 | return 10 114 | } else { 115 | return 30 116 | } 117 | } 118 | 119 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: LXMWaterfallLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { 120 | if section == 0 { 121 | return 20 122 | } else if section == 1 { 123 | return 10 124 | } else { 125 | return 30 126 | } 127 | } 128 | 129 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: LXMWaterfallLayout, heightForSectionHeaderInSection section: Int) -> CGFloat { 130 | if section == 0 { 131 | return 100 132 | } else if section == 1 { 133 | return 50 134 | } else { 135 | return 0 136 | } 137 | } 138 | 139 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: LXMWaterfallLayout, heightForSectionFooterInSection section: Int) -> CGFloat { 140 | if section == 0 { 141 | return 50 142 | } else if section == 1 { 143 | return 20 144 | } else { 145 | return 0 146 | } 147 | } 148 | 149 | 150 | 151 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: LXMWaterfallLayout, insetForSectionAt section: Int) -> UIEdgeInsets { 152 | if section == 0 { 153 | return UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) 154 | } else if section == 1 { 155 | return UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) 156 | } else { 157 | return UIEdgeInsets(top: 30, left: 30, bottom: 30, right: 30) 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /LXMWaterfallLayout/DemoViewControllers/WaterfallViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WaterfallViewController.swift 3 | // LXMWaterfallLayout 4 | // 5 | // Created by luxiaoming on 2017/8/26. 6 | // Copyright © 2017年 duowan. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class WaterfallViewController: DemoBaseViewController { 12 | 13 | } 14 | 15 | // MARK: - Lifecycle 16 | extension WaterfallViewController { 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | 21 | 22 | let layout = LXMWaterfallLayout() 23 | layout.columnCount = 3 24 | layout.minimumColumnSpacing = 5 25 | layout.minimumInteritemSpacing = 20 26 | layout.sectionInset = UIEdgeInsets(top: 10, left: 20, bottom: 30, right: 40) 27 | layout.sectionHeaderHeight = 100 28 | layout.sectionFooterHeight = 50 29 | layout.collectionViewHeaderHeight = 200 30 | layout.collectionViewFooterHeight = 300 31 | 32 | self.collectionView.collectionViewLayout = layout 33 | 34 | let sectionNib = UINib(nibName: "TestSectionView", bundle: nil) 35 | collectionView.register(sectionNib, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader , withReuseIdentifier: TestSectionViewIdentifier) 36 | collectionView.register(sectionNib, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: TestSectionViewIdentifier) 37 | collectionView.register(sectionNib, forSupplementaryViewOfKind: LXMCollectionElementKindHeader , withReuseIdentifier: TestSectionViewIdentifier) 38 | collectionView.register(sectionNib, forSupplementaryViewOfKind: LXMCollectionElementKindFooter, withReuseIdentifier: TestSectionViewIdentifier) 39 | 40 | collectionView.contentInset = UIEdgeInsets(top: 10, left: 20, bottom: 30, right: 40) 41 | 42 | } 43 | 44 | override func didReceiveMemoryWarning() { 45 | super.didReceiveMemoryWarning() 46 | // Dispose of any resources that can be recreated. 47 | } 48 | } 49 | 50 | 51 | 52 | // MARK: - override 53 | extension WaterfallViewController { 54 | 55 | func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { 56 | if kind == UICollectionView.elementKindSectionHeader { 57 | let sectionView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: TestSectionViewIdentifier, for: indexPath) as! TestSectionView 58 | sectionView.backgroundColor = UIColor.red 59 | sectionView.nameLabel.text = "sectionHeader \(indexPath.section)" 60 | return sectionView 61 | 62 | } else if kind == UICollectionView.elementKindSectionFooter { 63 | let sectionView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: TestSectionViewIdentifier, for: indexPath) as! TestSectionView 64 | sectionView.backgroundColor = UIColor.blue 65 | sectionView.nameLabel.text = "sectionFooter \(indexPath.section)" 66 | return sectionView 67 | } else if kind == LXMCollectionElementKindHeader { 68 | let sectionView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: TestSectionViewIdentifier, for: indexPath) as! TestSectionView 69 | sectionView.backgroundColor = UIColor.yellow 70 | sectionView.nameLabel.text = "collectionViewHeader" 71 | return sectionView 72 | } else if kind == LXMCollectionElementKindFooter { 73 | let sectionView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: TestSectionViewIdentifier, for: indexPath) as! TestSectionView 74 | sectionView.backgroundColor = UIColor.green 75 | sectionView.nameLabel.text = "collectionViewFooter" 76 | return sectionView 77 | } else { 78 | return UICollectionReusableView() 79 | } 80 | } 81 | } 82 | 83 | 84 | extension WaterfallViewController: LXMWaterfallLayoutDelegate { 85 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: LXMWaterfallLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 86 | return sizeArray[indexPath.item] 87 | } 88 | 89 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: LXMWaterfallLayout, numberOfColumnsAt section: Int) -> Int { 90 | if section == 0 { 91 | return 3 92 | } else if section == 1 { 93 | return 2 94 | } else { 95 | return 1 96 | } 97 | } 98 | 99 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: LXMWaterfallLayout, minimumColumnSpacingForSectionAt section: Int) -> CGFloat { 100 | if section == 0 { 101 | return 20 102 | } else if section == 1 { 103 | return 10 104 | } else { 105 | return 30 106 | } 107 | } 108 | 109 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: LXMWaterfallLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { 110 | if section == 0 { 111 | return 20 112 | } else if section == 1 { 113 | return 10 114 | } else { 115 | return 30 116 | } 117 | } 118 | 119 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: LXMWaterfallLayout, heightForSectionHeaderInSection section: Int) -> CGFloat { 120 | if section == 0 { 121 | return 100 122 | } else if section == 1 { 123 | return 50 124 | } else { 125 | return 0 126 | } 127 | } 128 | 129 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: LXMWaterfallLayout, heightForSectionFooterInSection section: Int) -> CGFloat { 130 | if section == 0 { 131 | return 50 132 | } else if section == 1 { 133 | return 20 134 | } else { 135 | return 0 136 | } 137 | } 138 | 139 | 140 | 141 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: LXMWaterfallLayout, insetForSectionAt section: Int) -> UIEdgeInsets { 142 | if section == 0 { 143 | return UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) 144 | } else if section == 1 { 145 | return UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) 146 | } else { 147 | return UIEdgeInsets(top: 30, left: 30, bottom: 30, right: 30) 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /LXMWaterfallLayout/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 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /LXMWaterfallLayout/LXMWaterfallLayout-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | #import "TestObject.h" 6 | -------------------------------------------------------------------------------- /LXMWaterfallLayout/LXMWaterfallLayout/LXMHeaderFooterFlowLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LXMHeaderFooterFlowLayout.swift 3 | // LXMWaterfallLayout 4 | // 5 | // Created by luxiaoming on 2017/8/29. 6 | // Copyright © 2017年 duowan. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public enum LXMLayoutHorizontalAlignment { 12 | 13 | case left // Visually left aligned 14 | 15 | case center // Visually centered 16 | 17 | case right // Visually right aligned 18 | 19 | case justified // Fully-justified, it is different from UICollectionViewFlowLayout default behavior, which will layout the cell in last line one by one with minimumInteritemSpacing if there is not enough cell. 20 | 21 | case none // use UICollectionViewFlowLayout's alignment 22 | } 23 | 24 | public enum LXMLayoutVerticalAlignment { 25 | 26 | case top // Visually top aligned 27 | 28 | case center // Visually centered 29 | 30 | case bottom // Visually bottom aligned 31 | 32 | case justified // Fully-justified, it is different from UICollectionViewFlowLayout default behavior, which will layout the cell in last line one by one with minimumInteritemSpacing if there is not enough cell. 33 | 34 | case none // use UICollectionViewFlowLayout's alignment 35 | } 36 | 37 | open class LXMHeaderFooterFlowLayout: UICollectionViewFlowLayout, LXMLayoutHeaderFooterProtocol { 38 | 39 | /// Notice: horiziontalAlignment does nothing when scrollDirection == .horizontal 40 | open var horiziontalAlignment: LXMLayoutHorizontalAlignment = .none 41 | 42 | /// Notice: verticalAlignment does nothing when scrollDirection == .vertical 43 | open var verticalAlignment: LXMLayoutVerticalAlignment = .none 44 | } 45 | 46 | extension LXMHeaderFooterFlowLayout { 47 | open override func prepare() { 48 | super.prepare() 49 | 50 | guard let collectionView = self.collectionView else { return } 51 | let numberOfSections = collectionView.numberOfSections 52 | if numberOfSections <= 0 { 53 | return 54 | } 55 | 56 | self.sectionItemAttributesDict = [Int : [UICollectionViewLayoutAttributes]]() 57 | self.sectionHeaderAttributesDict = [Int : UICollectionViewLayoutAttributes]() 58 | self.sectionFooterAttributesDict = [Int : UICollectionViewLayoutAttributes]() 59 | self.collectionViewHeaderAttributes = nil 60 | self.collectionViewFooterAttributes = nil 61 | self.allAttributesArray = [UICollectionViewLayoutAttributes]() 62 | 63 | 64 | for section in 0 ..< numberOfSections { 65 | var attributesArray = [UICollectionViewLayoutAttributes]() 66 | for index in 0 ..< collectionView.numberOfItems(inSection: section) { 67 | let indexPath = IndexPath(item: index, section: section) 68 | var itemAttributes: UICollectionViewLayoutAttributes? 69 | itemAttributes = super.layoutAttributesForItem(at: indexPath)?.copy() as? UICollectionViewLayoutAttributes 70 | itemAttributes = self.updateAttributesForHeaderAndFooter(attributes: itemAttributes) 71 | attributesArray.append(itemAttributes) 72 | } 73 | self.sectionItemAttributesDict?[section] = attributesArray.isEmpty ? nil : attributesArray 74 | self.allAttributesArray?.append(contentsOf: attributesArray) 75 | 76 | var headerAttributes = super.layoutAttributesForSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, at: IndexPath(item: 0, section: section))?.copy() as? UICollectionViewLayoutAttributes 77 | headerAttributes = self.updateAttributesForHeaderAndFooter(attributes: headerAttributes) 78 | self.sectionHeaderAttributesDict?[section] = headerAttributes 79 | 80 | var footerAttributes = super.layoutAttributesForSupplementaryView(ofKind: UICollectionView.elementKindSectionFooter, at: IndexPath(item: 0, section: section))?.copy() as? UICollectionViewLayoutAttributes 81 | footerAttributes = self.updateAttributesForHeaderAndFooter(attributes: footerAttributes) 82 | self.sectionFooterAttributesDict?[section] = footerAttributes 83 | } 84 | 85 | self.allAttributesArray = self.updateAttributesForAlignment(attributesArray: self.allAttributesArray) 86 | self.allAttributesArray?.append(contentsOf: self.sectionHeaderAttributesDict?.values) 87 | self.allAttributesArray?.append(contentsOf: self.sectionFooterAttributesDict?.values) 88 | self.allAttributesArray?.append(self.collectionViewHeaderAttributes) 89 | self.allAttributesArray?.append(self.collectionViewFooterAttributes) 90 | 91 | self.clearAttributesIfEmpty() 92 | } 93 | 94 | 95 | open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { 96 | //注意,这里直接调用super.layoutAttributesForElements(in rect: CGRect)在修改里面UICollectionViewLayoutAttributes的方法是不行的,系统应该是有做缓存,当所有UICollectionViewLayoutAttributes存在以后,系统就不会再调用此方法。这里因为多了header,所有item的位置都会相应下移,可能会使缓存的与返回的item有重复,导致位置错误 97 | 98 | return self.allAttributesArray?.filter({ (attributes) -> Bool in 99 | return attributes.frame.intersects(rect) 100 | }) 101 | } 102 | 103 | 104 | open override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { 105 | return self.sectionItemAttributesDict?[indexPath.section]?[indexPath.item] 106 | } 107 | 108 | 109 | open override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { 110 | if elementKind == LXMCollectionElementKindHeader { 111 | return self.collectionViewHeaderAttributes 112 | } else if elementKind == LXMCollectionElementKindFooter { 113 | return self.collectionViewFooterAttributes 114 | } else if elementKind == UICollectionView.elementKindSectionHeader { 115 | return self.sectionHeaderAttributesDict?[indexPath.section] 116 | } else if elementKind == UICollectionView.elementKindSectionFooter { 117 | return self.sectionFooterAttributesDict?[indexPath.section] 118 | } else { 119 | return super.layoutAttributesForSupplementaryView(ofKind: elementKind, at: indexPath) 120 | } 121 | } 122 | 123 | 124 | /// 注意:一定要用super.collectionViewContentSize返回的宽度,系统貌似根据contentInset做了优化计算 125 | open override var collectionViewContentSize: CGSize { 126 | return updateContentSizeForHeaderAndFooter(contentSize: super.collectionViewContentSize) 127 | } 128 | 129 | 130 | open override func shouldInvalidateLayout (forBoundsChange newBounds: CGRect) -> Bool { 131 | if let collectionView = self.collectionView { 132 | if newBounds.width != collectionView.bounds.width || 133 | newBounds.height != collectionView.bounds.height { 134 | return true 135 | } 136 | } 137 | return false 138 | } 139 | } 140 | 141 | // MARK: - PrivateMethod 142 | private extension LXMHeaderFooterFlowLayout { 143 | 144 | func groupedArray(attributesArray: [UICollectionViewLayoutAttributes]) -> [[UICollectionViewLayoutAttributes]] { 145 | var groupArray = [[UICollectionViewLayoutAttributes]]() 146 | var currentSection: Int = -1 147 | var currentValue: CGFloat = CGFloat.greatestFiniteMagnitude 148 | for attributes in attributesArray { 149 | if self.scrollDirection == .vertical { 150 | if attributes.center.y == currentValue { 151 | groupArray[currentSection].append(attributes) 152 | } else { 153 | currentSection += 1 154 | groupArray.append([UICollectionViewLayoutAttributes]()) 155 | groupArray[currentSection].append(attributes) 156 | currentValue = attributes.center.y 157 | } 158 | } else { 159 | if attributes.center.x == currentValue { 160 | groupArray[currentSection].append(attributes) 161 | } else { 162 | currentSection += 1 163 | groupArray.append([UICollectionViewLayoutAttributes]()) 164 | groupArray[currentSection].append(attributes) 165 | currentValue = attributes.center.x 166 | } 167 | } 168 | 169 | 170 | } 171 | return groupArray 172 | } 173 | 174 | func updateAttributesForAlignment(attributesArray: [UICollectionViewLayoutAttributes]?) -> [UICollectionViewLayoutAttributes]? { 175 | if self.scrollDirection == .vertical && self.horiziontalAlignment == .none { 176 | return attributesArray 177 | } 178 | if self.scrollDirection == .horizontal && self.verticalAlignment == .none { 179 | return attributesArray 180 | } 181 | 182 | guard let attributesArray = attributesArray else { return nil } 183 | let groupArray = self.groupedArray(attributesArray: attributesArray) 184 | var resultArray = [UICollectionViewLayoutAttributes]() 185 | for lineArray in groupArray { 186 | 187 | if self.scrollDirection == .vertical { 188 | if self.horiziontalAlignment == .left { 189 | if var currentItem = lineArray.first { 190 | if currentItem.representedElementKind != nil { 191 | resultArray.append(currentItem) 192 | continue 193 | } 194 | 195 | currentItem.frame.origin.x = insetForSection(at: currentItem.indexPath.section).left 196 | resultArray.append(currentItem) 197 | for i in 1 ..< lineArray.count { 198 | let attributes = lineArray[i] 199 | attributes.frame.origin.x = currentItem.frame.maxX + minimumInteritemSpacingForSection(at: currentItem.indexPath.section) 200 | resultArray.append(attributes) 201 | currentItem = attributes 202 | } 203 | } 204 | } 205 | else if self.horiziontalAlignment == .right { 206 | if var currentItem = lineArray.last { 207 | if currentItem.representedElementKind != nil { 208 | resultArray.append(currentItem) 209 | continue 210 | } 211 | let rightSpacing = insetForSection(at: currentItem.indexPath.section).right 212 | 213 | currentItem.frame.origin.x = self.collectionViewContentSize.width - rightSpacing - currentItem.frame.width 214 | resultArray.append(currentItem) 215 | 216 | for i in 1 ..< lineArray.count { 217 | let attributes = lineArray[lineArray.count - 1 - i] 218 | attributes.frame.origin.x = currentItem.frame.minX - minimumInteritemSpacingForSection(at: currentItem.indexPath.section) - attributes.frame.width 219 | resultArray.append(attributes) 220 | currentItem = attributes 221 | } 222 | } 223 | } 224 | else if self.horiziontalAlignment == .center { 225 | 226 | if var currentItem = lineArray.first { 227 | if currentItem.representedElementKind != nil { 228 | resultArray.append(currentItem) 229 | continue 230 | } 231 | let inset = insetForSection(at: currentItem.indexPath.section) 232 | let totalItemWidth = lineArray.reduce(0, { (result, attributes) in 233 | return result + attributes.frame.width 234 | }) 235 | let itemSpacing = minimumInteritemSpacingForSection(at: currentItem.indexPath.section) 236 | let sapcing = (self.collectionViewContentSize.width - inset.left - inset.right - totalItemWidth - (itemSpacing * CGFloat(lineArray.count - 1))) / 2 237 | 238 | currentItem.frame.origin.x = inset.left + sapcing 239 | resultArray.append(currentItem) 240 | for i in 1 ..< lineArray.count { 241 | let attributes = lineArray[i] 242 | attributes.frame.origin.x = currentItem.frame.maxX + itemSpacing 243 | resultArray.append(attributes) 244 | currentItem = attributes 245 | } 246 | } 247 | } 248 | else if self.horiziontalAlignment == .justified { 249 | 250 | if var currentItem = lineArray.first { 251 | if currentItem.representedElementKind != nil { 252 | resultArray.append(currentItem) 253 | continue 254 | } 255 | let inset = insetForSection(at: currentItem.indexPath.section) 256 | let totalItemWidth = lineArray.reduce(0, { (result, attributes) in 257 | return result + attributes.frame.width 258 | }) 259 | 260 | currentItem.frame.origin.x = inset.left 261 | resultArray.append(currentItem) 262 | if lineArray.count == 1 { 263 | continue 264 | } 265 | 266 | let sapcing = (self.collectionViewContentSize.width - inset.left - inset.right - totalItemWidth) / CGFloat(lineArray.count - 1) 267 | 268 | for i in 1 ..< lineArray.count { 269 | let attributes = lineArray[i] 270 | attributes.frame.origin.x = currentItem.frame.maxX + sapcing 271 | resultArray.append(attributes) 272 | currentItem = attributes 273 | } 274 | } 275 | } 276 | } 277 | else { 278 | if self.verticalAlignment == .top { 279 | if var currentItem = lineArray.first { 280 | if currentItem.representedElementKind != nil { 281 | resultArray.append(currentItem) 282 | continue 283 | } 284 | 285 | currentItem.frame.origin.y = insetForSection(at: currentItem.indexPath.section).top 286 | resultArray.append(currentItem) 287 | for i in 1 ..< lineArray.count { 288 | let attributes = lineArray[i] 289 | attributes.frame.origin.y = currentItem.frame.maxY + minimumLineSpacingForSection(at: currentItem.indexPath.section) 290 | resultArray.append(attributes) 291 | currentItem = attributes 292 | } 293 | } 294 | } 295 | else if self.verticalAlignment == .bottom { 296 | if var currentItem = lineArray.last { 297 | if currentItem.representedElementKind != nil { 298 | resultArray.append(currentItem) 299 | continue 300 | } 301 | let bottomSpacing = insetForSection(at: currentItem.indexPath.section).bottom 302 | 303 | currentItem.frame.origin.y = self.collectionViewContentSize.height - bottomSpacing - currentItem.frame.height 304 | resultArray.append(currentItem) 305 | 306 | for i in 1 ..< lineArray.count { 307 | let attributes = lineArray[lineArray.count - 1 - i] 308 | attributes.frame.origin.y = currentItem.frame.minY - minimumLineSpacingForSection(at: currentItem.indexPath.section) - attributes.frame.height 309 | resultArray.append(attributes) 310 | currentItem = attributes 311 | } 312 | } 313 | } 314 | else if self.verticalAlignment == .center { 315 | 316 | if var currentItem = lineArray.first { 317 | if currentItem.representedElementKind != nil { 318 | resultArray.append(currentItem) 319 | continue 320 | } 321 | let inset = insetForSection(at: currentItem.indexPath.section) 322 | let totalItemHeight = lineArray.reduce(0, { (result, attributes) in 323 | return result + attributes.frame.height 324 | }) 325 | let lineSpacing = minimumLineSpacingForSection(at: currentItem.indexPath.section) 326 | let sapcing = (self.collectionViewContentSize.height - inset.top - inset.bottom - totalItemHeight - (lineSpacing * CGFloat(lineArray.count - 1))) / 2 327 | 328 | currentItem.frame.origin.y = inset.top + sapcing 329 | resultArray.append(currentItem) 330 | for i in 1 ..< lineArray.count { 331 | let attributes = lineArray[i] 332 | attributes.frame.origin.y = currentItem.frame.maxY + lineSpacing 333 | resultArray.append(attributes) 334 | currentItem = attributes 335 | } 336 | } 337 | } 338 | else if self.verticalAlignment == .justified { 339 | 340 | if var currentItem = lineArray.first { 341 | if currentItem.representedElementKind != nil { 342 | resultArray.append(currentItem) 343 | continue 344 | } 345 | let inset = insetForSection(at: currentItem.indexPath.section) 346 | let totalItemHeight = lineArray.reduce(0, { (result, attributes) in 347 | return result + attributes.frame.height 348 | }) 349 | 350 | currentItem.frame.origin.y = inset.top 351 | resultArray.append(currentItem) 352 | if lineArray.count == 1 { 353 | continue 354 | } 355 | 356 | let sapcing = (self.collectionViewContentSize.height - inset.top - inset.bottom - totalItemHeight) / CGFloat(lineArray.count - 1) 357 | 358 | for i in 1 ..< lineArray.count { 359 | let attributes = lineArray[i] 360 | attributes.frame.origin.y = currentItem.frame.maxY + sapcing 361 | resultArray.append(attributes) 362 | currentItem = attributes 363 | } 364 | } 365 | } 366 | } 367 | 368 | } 369 | return resultArray 370 | 371 | } 372 | 373 | 374 | } 375 | 376 | 377 | 378 | 379 | -------------------------------------------------------------------------------- /LXMWaterfallLayout/LXMWaterfallLayout/LXMHorizontalMenuLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LXMHorizontalMenuLayout.swift 3 | // LXMWaterfallLayout 4 | // 5 | // Created by billthaslu on 2021/9/12. 6 | // Copyright © 2021 duowan. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @objc public protocol LXMHorizontalMenuLayoutDelegate : UICollectionViewDelegate { 12 | 13 | @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: LXMHorizontalMenuLayout, sizeForItemAt index: Int) -> CGSize 14 | } 15 | 16 | open class LXMHorizontalMenuLayout: UICollectionViewLayout { 17 | 18 | open var interitemSpacing: CGFloat = 10; 19 | 20 | open var itemSize: CGSize = CGSize(width: 50, height: 50) 21 | 22 | open var sectionInset: UIEdgeInsets = UIEdgeInsets.zero 23 | 24 | fileprivate weak var delegate: LXMHorizontalMenuLayoutDelegate? { 25 | return self.collectionView?.delegate as? LXMHorizontalMenuLayoutDelegate 26 | } 27 | 28 | /// 所有item总宽度,不包含间距 29 | fileprivate var itemTotalWidth: CGFloat = 0 30 | 31 | /// item正常排布时的contentWidth,包含间距和SectionInset 32 | fileprivate var normalContentWidth: CGFloat = 0 33 | 34 | /// item正常排布时的contentHeight,包含间距和SectionInset 35 | fileprivate var normalContentHeight: CGFloat = 0 36 | 37 | /// 所有item的Attributes数组 38 | fileprivate var itemAttributesArray = [UICollectionViewLayoutAttributes]() 39 | 40 | } 41 | 42 | /// MARK: - override 43 | extension LXMHorizontalMenuLayout { 44 | 45 | open override func prepare() { 46 | super.prepare() 47 | guard let sectionCount = self.collectionView?.numberOfSections, 48 | sectionCount == 1 else { 49 | assert(false, "目前仅支持 1 个section") 50 | return 51 | } 52 | guard let itemCount = self.collectionView?.numberOfItems(inSection: 0) else { 53 | return 54 | } 55 | 56 | self.itemAttributesArray.removeAll() 57 | self.normalContentWidth = self.sectionInset.left 58 | self.normalContentHeight = 0 59 | self.itemTotalWidth = 0 60 | for index in 0 ..< itemCount { 61 | let itemAttributes = UICollectionViewLayoutAttributes(forCellWith: IndexPath(item: index, section: 0)) 62 | let itemSize = self.delegateItemSizeForCellAtIndex(index: index) 63 | itemAttributes.frame = CGRect(x: self.normalContentWidth, y: self.sectionInset.top, width: itemSize.width, height: itemSize.height) 64 | self.itemAttributesArray.append(itemAttributes) 65 | self.normalContentWidth += itemSize.width; 66 | self.itemTotalWidth += itemSize.width; 67 | if index != itemCount - 1 { 68 | self.normalContentWidth += self.interitemSpacing 69 | } 70 | if itemSize.height > self.normalContentHeight { 71 | self.normalContentHeight = itemSize.height 72 | } 73 | } 74 | self.normalContentWidth += self.sectionInset.right; 75 | self.normalContentHeight += self.sectionInset.top + self.sectionInset.bottom 76 | 77 | if self.shouldAdjustItemFrame() { 78 | self.updateAttributesForEvenLayout() 79 | } 80 | } 81 | 82 | open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { 83 | let array = self.itemAttributesArray.filter { attributes in 84 | return rect.intersects(attributes.frame) 85 | } 86 | return array 87 | } 88 | 89 | open override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { 90 | return itemAttributesArray[indexPath.item] 91 | } 92 | 93 | open override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { 94 | assert(false, "暂时不支持SupplementaryView") 95 | return nil 96 | } 97 | 98 | open override func layoutAttributesForDecorationView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { 99 | assert(false, "暂时不支持DecorationView") 100 | return nil 101 | } 102 | 103 | open override var collectionViewContentSize: CGSize { 104 | if self.shouldAdjustItemFrame() { 105 | return CGSize(width: self.collectionView?.bounds.width ?? 0, height: self.normalContentHeight) 106 | } else { 107 | return CGSize(width: self.normalContentWidth, height: self.normalContentHeight) 108 | } 109 | } 110 | 111 | open override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { 112 | guard let size = self.collectionView?.bounds.size else { return false } 113 | return size.equalTo(newBounds.size) 114 | } 115 | 116 | } 117 | 118 | // MARK: - PrivateMethod 119 | private extension LXMHorizontalMenuLayout { 120 | 121 | func delegateItemSizeForCellAtIndex(index: Int) -> CGSize { 122 | if let collectionView = self.collectionView, 123 | let itemSize = self.delegate?.collectionView?(collectionView, layout: self, sizeForItemAt: index) { 124 | return itemSize 125 | } 126 | return self.itemSize 127 | } 128 | 129 | func shouldAdjustItemFrame() -> Bool { 130 | if let width = self.collectionView?.bounds.size.width, 131 | width > self.normalContentWidth { 132 | return true 133 | } 134 | return false 135 | } 136 | 137 | func evenLayoutSpacing() -> CGFloat { 138 | guard let itemCount = self.collectionView?.numberOfItems(inSection: 0), 139 | let width = self.collectionView?.bounds.size.width else { 140 | return 0 141 | } 142 | let spacing = floor((width - self.itemTotalWidth) / CGFloat((itemCount + 1))) 143 | return spacing 144 | } 145 | 146 | func updateAttributesForEvenLayout() { 147 | guard let sectionCount = self.collectionView?.numberOfSections, 148 | sectionCount == 1 else { 149 | assert(false, "目前仅支持 1 个section") 150 | return 151 | } 152 | let itemCount = self.itemAttributesArray.count 153 | let spacing = self.evenLayoutSpacing() 154 | var offsetX = spacing 155 | for i in 0 ..< itemCount { 156 | let itemAttributes = self.itemAttributesArray[i] 157 | var frame = itemAttributes.frame 158 | frame.origin.x = offsetX 159 | itemAttributes.frame = frame 160 | offsetX += itemAttributes.frame.size.width + spacing; 161 | } 162 | } 163 | 164 | } 165 | 166 | -------------------------------------------------------------------------------- /LXMWaterfallLayout/LXMWaterfallLayout/LXMLayoutHeaderFooterProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LXMLayoutHeaderFooterProtocol.swift 3 | // LXMWaterfallLayout 4 | // 5 | // Created by luxiaoming on 2017/8/26. 6 | // Copyright © 2017年 duowan. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | 13 | public let LXMCollectionElementKindHeader: String = "LXMCollectionElementKindHeader" 14 | 15 | public let LXMCollectionElementKindFooter: String = "LXMCollectionElementKindFooter" 16 | 17 | 18 | /// 此协议声明成只有类可以遵守,因为本来就是给UICollectionViewLayout用的 19 | /// 里面有修改self属性的方法,如果结构体等值类型也可以遵守该协议的话,会复杂很多 20 | /// 具体原因见:https://www.bignerdranch.com/blog/protocol-oriented-problems-and-the-immutable-self-error/ 21 | public protocol LXMLayoutHeaderFooterProtocol: class { 22 | 23 | var collectionViewHeaderHeight: CGFloat { get set } 24 | 25 | var collectionViewFooterHeight: CGFloat { get set } 26 | 27 | /// 如果collectionViewHeaderHeight > 0 则有默认值,否则为nil 28 | /// 可以通过赋值nil来重新生成默认值 29 | var collectionViewHeaderAttributes: UICollectionViewLayoutAttributes? { get set } 30 | 31 | /// 如果collectionViewFooterHeight > 0 则有默认值,否则为nil 32 | /// 可以通过赋值nil来重新生成默认值 33 | var collectionViewFooterAttributes: UICollectionViewLayoutAttributes? { get set } 34 | 35 | } 36 | 37 | 38 | 39 | private var kLXMCollectionViewHeaderHeightKey: String = "kLXMCollectionViewHeaderHeightKey" 40 | private var kLXMCollectionViewFooterHeightKey: String = "kLXMCollectionViewFooterHeightKey" 41 | private var kLXMCollectionViewHeaderAttributesKey: String = "kLXMCollectionViewHeaderAttributesKey" 42 | private var kLXMCollectionViewFooterAttributesKey: String = "kLXMCollectionViewFooterAttributesKey" 43 | 44 | public extension LXMLayoutHeaderFooterProtocol where Self: UICollectionViewLayout { 45 | 46 | var collectionViewHeaderHeight: CGFloat { 47 | set { 48 | assert(newValue >= 0, "collectionViewHeaderHeight must be equal or greater than 0 !!!") 49 | let value = NSNumber(value: Float(newValue)) 50 | objc_setAssociatedObject(self, &kLXMCollectionViewHeaderHeightKey, value, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 51 | } 52 | get { 53 | if let value = objc_getAssociatedObject(self, &kLXMCollectionViewHeaderHeightKey) as? NSNumber { 54 | return CGFloat(value.floatValue) 55 | } 56 | return 0 57 | } 58 | } 59 | 60 | var collectionViewFooterHeight: CGFloat { 61 | set { 62 | assert(newValue >= 0, "collectionViewFooterHeight must be equal or greater than 0 !!!") 63 | let value = NSNumber(value: Float(newValue)) 64 | objc_setAssociatedObject(self, &kLXMCollectionViewFooterHeightKey, value, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 65 | } 66 | get { 67 | if let value = objc_getAssociatedObject(self, &kLXMCollectionViewFooterHeightKey) as? NSNumber { 68 | return CGFloat(value.floatValue) 69 | } 70 | return 0 71 | } 72 | } 73 | 74 | var collectionViewHeaderAttributes: UICollectionViewLayoutAttributes? { 75 | set { 76 | objc_setAssociatedObject(self, &kLXMCollectionViewHeaderAttributesKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 77 | } 78 | get { 79 | if let attributes = objc_getAssociatedObject(self, &kLXMCollectionViewHeaderAttributesKey) as? UICollectionViewLayoutAttributes { 80 | return attributes 81 | } else { 82 | if self.collectionViewHeaderHeight > 0 { 83 | let attributes = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: LXMCollectionElementKindHeader, with: IndexPath()) 84 | attributes.frame = CGRect(x: 0, 85 | y: 0, 86 | width: self.collectionViewContentSize.width, 87 | height: self.collectionViewHeaderHeight) 88 | if let layout = self as? UICollectionViewFlowLayout, layout.scrollDirection == .horizontal { 89 | attributes.frame = CGRect(x: 0, 90 | y: 0, 91 | width: self.collectionViewHeaderHeight, 92 | height: self.collectionViewContentSize.height) 93 | } 94 | self.collectionViewHeaderAttributes = attributes 95 | return attributes 96 | } 97 | return nil 98 | } 99 | } 100 | } 101 | 102 | var collectionViewFooterAttributes: UICollectionViewLayoutAttributes? { 103 | set { 104 | objc_setAssociatedObject(self, &kLXMCollectionViewFooterAttributesKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 105 | } 106 | get { 107 | if let attributes = objc_getAssociatedObject(self, &kLXMCollectionViewFooterAttributesKey) as? UICollectionViewLayoutAttributes { 108 | return attributes 109 | } else { 110 | if self.collectionViewFooterHeight > 0 { 111 | let attributes = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: LXMCollectionElementKindFooter, with: IndexPath()) 112 | attributes.frame = CGRect(x: 0, 113 | y: self.collectionViewContentSize.height - self.collectionViewFooterHeight, 114 | width: self.collectionViewContentSize.width, 115 | height: self.collectionViewFooterHeight) 116 | if let layout = self as? UICollectionViewFlowLayout, layout.scrollDirection == .horizontal { 117 | attributes.frame = CGRect(x: self.collectionViewContentSize.width - self.collectionViewFooterHeight, 118 | y: 0, 119 | width: self.collectionViewFooterHeight, 120 | height: self.collectionViewContentSize.height) 121 | } 122 | self.collectionViewFooterAttributes = attributes 123 | return attributes 124 | } 125 | return nil 126 | } 127 | } 128 | } 129 | 130 | 131 | 132 | func updateAttributesForHeaderAndFooter(attributes: UICollectionViewLayoutAttributes?) -> UICollectionViewLayoutAttributes? { 133 | if let result = attributes?.copy() as? UICollectionViewLayoutAttributes { 134 | 135 | if let layout = self as? UICollectionViewFlowLayout, layout.scrollDirection == .horizontal { 136 | result.frame.origin.x += self.collectionViewHeaderHeight 137 | } else { 138 | result.frame.origin.y += self.collectionViewHeaderHeight 139 | } 140 | 141 | return result 142 | } 143 | return nil 144 | 145 | } 146 | 147 | func updateContentSizeForHeaderAndFooter(contentSize: CGSize) -> CGSize { 148 | var size = contentSize 149 | if let layout = self as? UICollectionViewFlowLayout, layout.scrollDirection == .horizontal { 150 | size.width += self.collectionViewHeaderHeight + self.collectionViewFooterHeight 151 | } else { 152 | size.height += self.collectionViewHeaderHeight + self.collectionViewFooterHeight 153 | } 154 | return size 155 | } 156 | 157 | 158 | 159 | 160 | } 161 | 162 | 163 | 164 | private var kLXMCollectionViewSectionItemAttributesDictKey = "kLXMCollectionViewSectionItemAttributesDictKey" 165 | private var kLXMCollectionViewSectionHeaderAttributesDictKey = "kLXMCollectionViewSectionHeaderAttributesDictKey" 166 | private var kLXMCollectionViewSectionFooterAttributesDictKey = "kLXMCollectionViewSectionFooterAttributesDictKey" 167 | private var kLXMCollectionViewAllAttributesArrayKey = "kLXMCollectionViewAllAttributesArrayKey" 168 | 169 | public extension UICollectionViewLayout { 170 | 171 | /// 保存每个section中每个item的Attributes的字典,key是section 172 | var sectionItemAttributesDict: [Int : [UICollectionViewLayoutAttributes]]? { 173 | set { 174 | objc_setAssociatedObject(self, &kLXMCollectionViewSectionItemAttributesDictKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 175 | } 176 | get { 177 | return objc_getAssociatedObject(self, &kLXMCollectionViewSectionItemAttributesDictKey) as? [Int : [UICollectionViewLayoutAttributes]] 178 | } 179 | } 180 | 181 | 182 | // 保存sectionHeader的attributes的字典,key是section 183 | var sectionHeaderAttributesDict: [Int : UICollectionViewLayoutAttributes]? { 184 | set { 185 | objc_setAssociatedObject(self, &kLXMCollectionViewSectionHeaderAttributesDictKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 186 | } 187 | get { 188 | return objc_getAssociatedObject(self, &kLXMCollectionViewSectionHeaderAttributesDictKey) as? [Int : UICollectionViewLayoutAttributes] 189 | } 190 | } 191 | 192 | /// 保存sectionFooter的attributes的字典,key是section 193 | var sectionFooterAttributesDict: [Int : UICollectionViewLayoutAttributes]? { 194 | set { 195 | objc_setAssociatedObject(self, &kLXMCollectionViewSectionFooterAttributesDictKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 196 | } 197 | get { 198 | return objc_getAssociatedObject(self, &kLXMCollectionViewSectionFooterAttributesDictKey) as? [Int : UICollectionViewLayoutAttributes] 199 | } 200 | } 201 | 202 | /// 所有item的attributes的数组,包括cell和SectionHeader,SectionFooter, collectionViewHeader, collectionViewFooter 203 | var allAttributesArray: [UICollectionViewLayoutAttributes]? { 204 | set { 205 | objc_setAssociatedObject(self, &kLXMCollectionViewAllAttributesArrayKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 206 | } 207 | get { 208 | return objc_getAssociatedObject(self, &kLXMCollectionViewAllAttributesArrayKey) as? [UICollectionViewLayoutAttributes] 209 | } 210 | } 211 | 212 | func clearAttributesIfEmpty() { 213 | if let dict = self.sectionItemAttributesDict, dict.isEmpty { 214 | self.sectionItemAttributesDict = nil 215 | } 216 | if let dict = self.sectionHeaderAttributesDict, dict.isEmpty { 217 | self.sectionHeaderAttributesDict = nil 218 | } 219 | if let dict = self.sectionFooterAttributesDict, dict.isEmpty { 220 | self.sectionFooterAttributesDict = nil 221 | } 222 | if let dict = self.allAttributesArray, dict.isEmpty { 223 | self.allAttributesArray = nil 224 | } 225 | } 226 | 227 | } 228 | 229 | 230 | 231 | // MARK: - Tool 232 | extension Array { 233 | public mutating func append(_ newElement: Element?) { 234 | if let newElement = newElement { 235 | self.append(newElement) 236 | } 237 | } 238 | 239 | public mutating func append(contentsOf newElements: S?) where S : Sequence, S.Iterator.Element == Element { 240 | if let newElements = newElements { 241 | self.append(contentsOf: newElements) 242 | } 243 | } 244 | 245 | public mutating func insert(_ newElement: Element?, at i: Int) { 246 | if let newElement = newElement { 247 | self.insert(newElement, at: i) 248 | } 249 | } 250 | } 251 | 252 | 253 | // MARK: - Tool 254 | public extension UICollectionViewFlowLayout { 255 | 256 | fileprivate var delegate: UICollectionViewDelegateFlowLayout? { 257 | return self.collectionView?.delegate as? UICollectionViewDelegateFlowLayout 258 | } 259 | 260 | func sizeForItem(atIndexPath indexPath: IndexPath) -> CGSize { 261 | if let collectionView = self.collectionView { 262 | return self.delegate?.collectionView?(collectionView, layout: self, sizeForItemAt: indexPath) ?? self.itemSize 263 | } 264 | return self.itemSize 265 | } 266 | 267 | func insetForSection(at section: Int) -> UIEdgeInsets { 268 | if let collectionView = self.collectionView { 269 | return self.delegate?.collectionView?(collectionView, layout: self, insetForSectionAt: section) ?? self.sectionInset 270 | } 271 | return self.sectionInset 272 | } 273 | 274 | func minimumLineSpacingForSection(at section: Int) -> CGFloat { 275 | if let collectionView = self.collectionView { 276 | return self.delegate?.collectionView?(collectionView, layout: self, minimumLineSpacingForSectionAt: section) ?? self.minimumLineSpacing 277 | } 278 | return self.minimumLineSpacing 279 | } 280 | 281 | func minimumInteritemSpacingForSection(at section: Int) -> CGFloat { 282 | if let collectionView = self.collectionView { 283 | return self.delegate?.collectionView?(collectionView, layout: self, minimumInteritemSpacingForSectionAt: section) ?? self.minimumInteritemSpacing 284 | } 285 | return self.minimumInteritemSpacing 286 | } 287 | 288 | func referenceSizeForHeaderInSection(section: Int) -> CGSize { 289 | if let collectionView = self.collectionView { 290 | return self.delegate?.collectionView?(collectionView, layout: self, referenceSizeForHeaderInSection: section) ?? self.headerReferenceSize 291 | } 292 | return self.headerReferenceSize 293 | } 294 | 295 | func referenceSizeForFooterInSection(section: Int) -> CGSize { 296 | if let collectionView = self.collectionView { 297 | return self.delegate?.collectionView?(collectionView, layout: self, referenceSizeForFooterInSection: section) ?? self.footerReferenceSize 298 | } 299 | return self.footerReferenceSize 300 | } 301 | 302 | } 303 | 304 | -------------------------------------------------------------------------------- /LXMWaterfallLayout/LXMWaterfallLayout/LXMWaterfallLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LXMWaterfallLayout.swift 3 | // LXMWaterfallLayout 4 | // 5 | // Created by luxiaoming on 2017/8/23. 6 | // Copyright © 2017年 duowan. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @objc public protocol LXMWaterfallLayoutDelegate: UICollectionViewDelegate { 12 | 13 | /// 请求delegate每个item的大小,如果没有实现该方法,那itemSize将是计算出来的边长为columnWidth的正方形 14 | /// 注意:如果返回的宽度小于计算出来的columnWidth,那实际显示的大小是将itemSize按比例缩放到“宽==columnWidth”的大小 15 | @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: LXMWaterfallLayout, sizeForItemAt indexPath: IndexPath) -> CGSize 16 | 17 | @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: LXMWaterfallLayout, numberOfColumnsAt section: Int) -> Int 18 | 19 | /// 每一列column的具体宽度,如果没有实现或者返回的值小于等于0, 那默认的columnWidth根据列数和间距平均分配 20 | /// 注意:需要保证每个section的column总宽度要小于等于collectionViewContentSize.width 21 | @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: LXMWaterfallLayout, columnWidthAtSection section: Int, columnIndex: Int) -> CGFloat 22 | 23 | @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: LXMWaterfallLayout, minimumColumnSpacingForSectionAt section: Int) -> CGFloat 24 | 25 | @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: LXMWaterfallLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat 26 | 27 | @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: LXMWaterfallLayout, heightForSectionHeaderInSection section: Int) -> CGFloat 28 | 29 | @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: LXMWaterfallLayout, heightForSectionFooterInSection section: Int) -> CGFloat 30 | 31 | @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: LXMWaterfallLayout, insetForSectionAt section: Int) -> UIEdgeInsets 32 | 33 | } 34 | 35 | open class LXMWaterfallLayout: UICollectionViewLayout, LXMLayoutHeaderFooterProtocol { 36 | 37 | open var columnCount: Int = 2 38 | 39 | open var minimumColumnSpacing: CGFloat = 10 40 | 41 | open var minimumInteritemSpacing: CGFloat = 10 42 | 43 | open var sectionHeaderHeight: CGFloat = 0 44 | 45 | open var sectionFooterHeight: CGFloat = 0 46 | 47 | open var sectionInset: UIEdgeInsets = UIEdgeInsets.zero 48 | 49 | fileprivate weak var delegate: LXMWaterfallLayoutDelegate? { 50 | return self.collectionView?.delegate as? LXMWaterfallLayoutDelegate 51 | } 52 | 53 | fileprivate var contentHeight: CGFloat = 0 54 | 55 | /// 这是保存每个section中每个column高度的二维数组,这里用二维数组而不是字典,主要是为了省去字典取值造成的可选绑定 56 | /// 注意:是column的高度,而不是某个具体cell的高度 57 | fileprivate var columnHeights = [[CGFloat]]() 58 | 59 | 60 | /// 这是保存每个section中每个column的offsetX的二维数组,这里用二维数组而不是字典,主要是为了省去字典取值造成的可选绑定 61 | /// 注意:是column的offsetX,而不是某个具体column的宽度或者高度 62 | fileprivate var columnOffsetXs = [[CGFloat]]() 63 | 64 | 65 | } 66 | 67 | 68 | // MARK: - override 69 | extension LXMWaterfallLayout { 70 | 71 | open override func prepare() { 72 | super.prepare() 73 | guard let collectionView = self.collectionView else { return } 74 | let collectionViewWidth = self.collectionViewContentSize.width 75 | let numberOfSections = collectionView.numberOfSections 76 | if numberOfSections <= 0 { 77 | return 78 | } 79 | 80 | self.contentHeight = 0 81 | self.columnHeights.removeAll() 82 | self.sectionItemAttributesDict = [Int : [UICollectionViewLayoutAttributes]]() 83 | self.sectionHeaderAttributesDict = [Int : UICollectionViewLayoutAttributes]() 84 | self.sectionFooterAttributesDict = [Int : UICollectionViewLayoutAttributes]() 85 | self.allAttributesArray = [UICollectionViewLayoutAttributes]() 86 | self.collectionViewHeaderAttributes = nil 87 | self.collectionViewFooterAttributes = nil 88 | 89 | 90 | for section in 0 ..< numberOfSections { 91 | 92 | //初始化columnHeights这个二维数组,全部赋值0 93 | var columnHeightArray = [CGFloat]() 94 | let columnCount = self.columnCount(atSection: section) 95 | for _ in 0 ..< columnCount { 96 | columnHeightArray.append(0) 97 | } 98 | self.columnHeights.append(columnHeightArray) 99 | 100 | let inset = self.sectionInset(atSection: section) 101 | let columnSpacing = self.minimumColumnSpacing(atSection: section) 102 | var offsetX = inset.left 103 | var offsetXArray = [CGFloat]() 104 | offsetXArray.append(offsetX) 105 | for i in 0 ..< columnCount - 1 { 106 | let columnWidth = self.columnWidth(atSection: section, columnIndex: i) 107 | offsetX += columnWidth + columnSpacing; 108 | offsetXArray.append(offsetX) 109 | } 110 | self.columnOffsetXs.append(offsetXArray) 111 | 112 | //初始化sectionItemAttributesDict 113 | var attributesArray = [UICollectionViewLayoutAttributes]() 114 | let itemCount = collectionView.numberOfItems(inSection: section) 115 | for _ in 0 ..< itemCount { 116 | attributesArray.append(UICollectionViewLayoutAttributes()) 117 | } 118 | self.sectionItemAttributesDict?[section] = attributesArray 119 | } 120 | 121 | contentHeight = self.collectionViewHeaderHeight 122 | 123 | for section in 0 ..< numberOfSections { 124 | 125 | let itemSpacing = self.minimumInteritemSpacing(atSection: section) 126 | let inset = self.sectionInset(atSection: section) 127 | 128 | //sectionHeader 129 | let sectionHeaderHeight = self.sectionHeaderHeight(atSection: section) 130 | if sectionHeaderHeight > 0 { 131 | let attributes = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, with: IndexPath(item: 0, section: section)) 132 | attributes.frame = CGRect(x: 0, y: contentHeight, width: collectionViewWidth, height: sectionHeaderHeight) 133 | self.sectionHeaderAttributesDict?[section] = attributes 134 | contentHeight = attributes.frame.maxY 135 | self.allAttributesArray?.append(attributes) 136 | } 137 | 138 | let itemCount = collectionView.numberOfItems(inSection: section) 139 | 140 | contentHeight += inset.top 141 | 142 | //sectionItem 143 | 144 | for index in 0 ..< itemCount { 145 | let indexPath = IndexPath(item: index, section: section) 146 | let columnIndex = self.nextColumnIndexForItem(atIndexPath: indexPath) 147 | let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath) 148 | let offsetX = self.columnOffsetXs[section][columnIndex] 149 | var offsetY = self.columnHeights[section][columnIndex] 150 | if offsetY != 0 { //注意,如果offsetY == 0说明是该列第一个,不用加spacing 151 | offsetY += itemSpacing 152 | } 153 | offsetY += contentHeight 154 | 155 | let size = self.itemSize(atIndexPath: indexPath, columnIndex: columnIndex) 156 | attributes.frame = CGRect(x: offsetX, y: offsetY, width: size.width, height: size.height) 157 | self.columnHeights[section][columnIndex] = attributes.frame.maxY - contentHeight 158 | self.sectionItemAttributesDict?[indexPath.section]?[indexPath.item] = attributes 159 | self.allAttributesArray?.append(attributes) 160 | } 161 | 162 | if let maxColumnHeight = self.columnHeights[section].max() { 163 | contentHeight += maxColumnHeight 164 | } 165 | 166 | contentHeight += inset.bottom 167 | 168 | //sectionFooter 169 | let sectionFooterHeight = self.sectionFooterHeight(atSection: section) 170 | if sectionFooterHeight > 0 { 171 | let attributes = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, with: IndexPath(item: 0, section: section)) 172 | attributes.frame = CGRect(x: 0, y: contentHeight, width: collectionViewWidth, height: sectionFooterHeight) 173 | self.sectionFooterAttributesDict?[section] = attributes 174 | contentHeight = attributes.frame.maxY 175 | self.allAttributesArray?.append(attributes) 176 | } 177 | 178 | } 179 | contentHeight += self.collectionViewFooterHeight 180 | self.allAttributesArray?.append(self.collectionViewHeaderAttributes) 181 | self.allAttributesArray?.append(self.collectionViewFooterAttributes) 182 | 183 | self.clearAttributesIfEmpty() 184 | 185 | } 186 | 187 | open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { 188 | 189 | 190 | /// 用这几句加上 shouldInvalidateLayout(forBoundsChange newBounds: CGRect)方法配合就可以实现悬浮header,但是貌似意义不大,跟直接add个view上去效果一样 191 | // if let collectionView = self.collectionView, let headerAttributes = self.collectionViewHeaderAttributes { 192 | // let currentOffset = collectionView.contentOffset.y + collectionView.contentInset.top 193 | // headerAttributes.frame.origin.y = currentOffset 194 | // headerAttributes.zIndex = 1 195 | // if currentOffset < 0 { 196 | // headerAttributes.frame.size.height = headerAttributes.frame.height - currentOffset 197 | // } 198 | // } 199 | 200 | 201 | /// 用这几句加上 shouldInvalidateLayout(forBoundsChange newBounds: CGRect)方法配合就可以实现 跟着滑动放大的header,但是跟上面一样,会导致每次滑动都要重新计算所有的layout,效率很低 202 | // if let collectionView = self.collectionView, 203 | // let headerAttributes = self.collectionViewHeaderAttributes { 204 | // let currentOffset = collectionView.contentOffset.y + collectionView.contentInset.top 205 | // if currentOffset < 0 { 206 | // headerAttributes.frame.origin.y = currentOffset 207 | // headerAttributes.frame.size.height = self.collectionViewHeaderHeight - currentOffset 208 | // } 209 | // 210 | // } 211 | 212 | 213 | let resultArray = self.allAttributesArray?.filter { (tempAttributes) -> Bool in 214 | return tempAttributes.frame.intersects(rect) 215 | } 216 | return resultArray 217 | 218 | } 219 | 220 | 221 | open override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { 222 | if let attributesArray = self.sectionItemAttributesDict?[indexPath.section], 223 | indexPath.item < attributesArray.count { 224 | return attributesArray[indexPath.item] 225 | } 226 | return nil 227 | } 228 | 229 | 230 | open override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { 231 | if elementKind == UICollectionView.elementKindSectionHeader { 232 | return self.sectionHeaderAttributesDict?[indexPath.section] 233 | } else if elementKind == UICollectionView.elementKindSectionFooter { 234 | return self.sectionFooterAttributesDict?[indexPath.section] 235 | } else if elementKind == LXMCollectionElementKindHeader { 236 | return self.collectionViewHeaderAttributes 237 | } else if elementKind == LXMCollectionElementKindFooter { 238 | return self.collectionViewFooterAttributes 239 | } else { 240 | return nil 241 | } 242 | 243 | } 244 | 245 | 246 | open override var collectionViewContentSize: CGSize { 247 | 248 | /// 注意:contentSize是跟contentInset有关的,但是在计算Attributes的时候,不用计算contentInset,貌似系统已经计算过了 249 | if let collectionView = self.collectionView { 250 | return CGSize(width: collectionView.bounds.width - collectionView.contentInset.left - collectionView.contentInset.right, height: self.contentHeight) 251 | } 252 | return CGSize.zero 253 | } 254 | 255 | 256 | open override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { 257 | if let collectionView = self.collectionView { 258 | if newBounds.width != collectionView.bounds.width || 259 | newBounds.height != collectionView.bounds.height { 260 | return true 261 | } 262 | } 263 | return false 264 | } 265 | 266 | } 267 | 268 | // MARK: - PrivateMethod 269 | private extension LXMWaterfallLayout { 270 | 271 | func columnCount(atSection section: Int) -> Int { 272 | if let collectionView = self.collectionView, 273 | let count = self.delegate?.collectionView?(collectionView, layout: self, numberOfColumnsAt: section) { 274 | assert(count > 0, "columnCount must be greater than 0 !!!") 275 | return count 276 | } else { 277 | return self.columnCount 278 | } 279 | } 280 | 281 | func minimumColumnSpacing(atSection section: Int) -> CGFloat { 282 | if let collectionView = self.collectionView, 283 | let spacing = self.delegate?.collectionView?(collectionView, layout: self, minimumColumnSpacingForSectionAt: section) { 284 | assert(spacing >= 0, "minimumColumnSpacing must be equal or greater than 0 !!!") 285 | return spacing 286 | } else { 287 | return self.minimumColumnSpacing 288 | } 289 | } 290 | 291 | func minimumInteritemSpacing(atSection section: Int) -> CGFloat { 292 | if let collectionView = self.collectionView, 293 | let spacing = self.delegate?.collectionView?(collectionView, layout: self, minimumInteritemSpacingForSectionAt: section) { 294 | assert(spacing >= 0, "minimumInteritemSpacing must be equal or greater than 0 !!!") 295 | return spacing 296 | } else { 297 | return self.minimumInteritemSpacing 298 | } 299 | } 300 | 301 | func sectionHeaderHeight(atSection section: Int) -> CGFloat { 302 | if let collectionView = self.collectionView, 303 | let height = self.delegate?.collectionView?(collectionView, layout: self, heightForSectionHeaderInSection: section) { 304 | assert(height >= 0, "sectionHeaderHeight must be equal or greater than 0 !!!") 305 | return height 306 | } else { 307 | return self.sectionHeaderHeight 308 | } 309 | } 310 | 311 | func sectionFooterHeight(atSection section: Int) -> CGFloat { 312 | if let collectionView = self.collectionView, 313 | let height = self.delegate?.collectionView?(collectionView, layout: self, heightForSectionFooterInSection: section) { 314 | assert(height >= 0, "sectionFooterHeight must be equal or greater than 0 !!!") 315 | return height 316 | } else { 317 | return self.sectionFooterHeight 318 | } 319 | } 320 | 321 | func sectionInset(atSection section: Int) -> UIEdgeInsets { 322 | if let collectionView = self.collectionView, 323 | let sectionInset = self.delegate?.collectionView?(collectionView, layout: self, insetForSectionAt: section) { 324 | return sectionInset 325 | } else { 326 | return self.sectionInset 327 | } 328 | } 329 | 330 | 331 | func nextColumnIndexForItem(atIndexPath indexPath: IndexPath) -> Int { 332 | //这里是直接按 最短优先 来排列,其他的方式感觉意义不大 333 | var index: Int = 0 334 | var shortestHeight = CGFloat.greatestFiniteMagnitude 335 | for (location, tempHeight) in self.columnHeights[indexPath.section].enumerated() { 336 | if shortestHeight > tempHeight { 337 | shortestHeight = tempHeight 338 | index = location 339 | } 340 | } 341 | return index 342 | 343 | } 344 | 345 | 346 | func scaledSize(forOriginalSize size: CGSize, limitWidth: CGFloat) -> CGSize { 347 | if size.width <= 0.01 || size.height <= 0.01 || limitWidth <= 0.01 { 348 | return CGSize.zero 349 | } 350 | let width = floor(limitWidth) 351 | let height = ceil(width * size.height / size.width) 352 | return CGSize(width: width, height: height) 353 | } 354 | 355 | } 356 | 357 | // MARK: - PublicMethod 358 | public extension LXMWaterfallLayout { 359 | 360 | // func columnWidth(atSection section: Int) -> CGFloat { 361 | // let count = self.columnCount(atSection: section) 362 | // let sectionInset = self.sectionInset(atSection: section) 363 | // let spacing = self.minimumColumnSpacing(atSection: section) 364 | // let width = self.collectionViewContentSize.width - sectionInset.left - sectionInset.right 365 | // if count > 1 { 366 | // return floor((width - CGFloat(count - 1) * spacing) / CGFloat(count)) 367 | // } else { 368 | // return width 369 | // } 370 | // } 371 | // 372 | // 373 | // func itemSize(atIndexPath indexPath: IndexPath) -> CGSize { 374 | // if let collectionView = self.collectionView, 375 | // let itemSize = self.delegate?.collectionView?(collectionView, layout: self, sizeForItemAt: indexPath) { 376 | // let columnWidth = self.columnWidth(atSection: indexPath.section) 377 | // if itemSize.width == columnWidth { 378 | // return itemSize 379 | // } else { 380 | // return self.scaledSize(forOriginalSize: itemSize, limitWidth: columnWidth) 381 | // } 382 | // } else { 383 | // let columnWidth = self.columnWidth(atSection: indexPath.section) 384 | // return CGSize(width: columnWidth, height: columnWidth) 385 | // } 386 | // } 387 | 388 | 389 | func columnWidth(atSection section: Int, columnIndex: Int) -> CGFloat { 390 | if let collectionView = self.collectionView, 391 | let columnWidth = self.delegate?.collectionView?(collectionView, layout: self, columnWidthAtSection: section, columnIndex: columnIndex) { 392 | assert(columnWidth > 0, "columnWidth must be greater than 0 at section \(section)!!!") 393 | return columnWidth 394 | } 395 | 396 | let count = self.columnCount(atSection: section) 397 | let sectionInset = self.sectionInset(atSection: section) 398 | let spacing = self.minimumColumnSpacing(atSection: section) 399 | let width = self.collectionViewContentSize.width - sectionInset.left - sectionInset.right 400 | if count > 1 { 401 | return floor((width - CGFloat(count - 1) * spacing) / CGFloat(count)) 402 | } else { 403 | return width 404 | } 405 | } 406 | 407 | func itemSize(atIndexPath indexPath: IndexPath, columnIndex: Int) -> CGSize { 408 | if let collectionView = self.collectionView, 409 | let itemSize = self.delegate?.collectionView?(collectionView, layout: self, sizeForItemAt: indexPath) { 410 | let columnWidth = self.columnWidth(atSection: indexPath.section, columnIndex: columnIndex) 411 | if itemSize.width == columnWidth { 412 | return itemSize 413 | } else { 414 | return self.scaledSize(forOriginalSize: itemSize, limitWidth: columnWidth) 415 | } 416 | } else { 417 | let columnWidth = self.columnWidth(atSection: indexPath.section, columnIndex: columnIndex) 418 | return CGSize(width: columnWidth, height: columnWidth) 419 | } 420 | } 421 | } 422 | 423 | 424 | -------------------------------------------------------------------------------- /LXMWaterfallLayout/MyTestLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MyTestLayout.swift 3 | // LXMWaterfallLayout 4 | // 5 | // Created by luxiaoming on 2017/9/6. 6 | // Copyright © 2017年 duowan. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | 12 | 13 | /// 这个类演示如何创建一个自己的类,并让它遵从LXMLayoutHeaderFooterProtocol 14 | /// 如果只是自己用的,确定不会调用到 `layoutAttributesForItem(at indexPath: IndexPath)` 15 | /// 或者`layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath)` 16 | /// 那这两个方法就可以不用实现,只用实现`layoutAttributesForElements(in rect: CGRect)`即可 17 | class MyTestLayout: UICollectionViewFlowLayout, LXMLayoutHeaderFooterProtocol { 18 | 19 | 20 | } 21 | 22 | extension MyTestLayout { 23 | open override func prepare() { 24 | super.prepare() 25 | 26 | guard let collectionView = self.collectionView else { return } 27 | let numberOfSections = collectionView.numberOfSections 28 | if numberOfSections <= 0 { 29 | return 30 | } 31 | 32 | self.collectionViewHeaderAttributes = nil 33 | self.collectionViewFooterAttributes = nil 34 | self.allAttributesArray = [UICollectionViewLayoutAttributes]() 35 | 36 | for section in 0 ..< numberOfSections { 37 | for index in 0 ..< collectionView.numberOfItems(inSection: section) { 38 | let indexPath = IndexPath(item: index, section: section) 39 | let itemAttributes = super.layoutAttributesForItem(at: indexPath) 40 | self.allAttributesArray?.append(itemAttributes) 41 | } 42 | 43 | let headerAttributes = super.layoutAttributesForSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, at: IndexPath(item: 0, section: section)) 44 | self.allAttributesArray?.append(headerAttributes) 45 | 46 | let footerAttributes = super.layoutAttributesForSupplementaryView(ofKind: UICollectionView.elementKindSectionFooter, at: IndexPath(item: 0, section: section)) 47 | self.allAttributesArray?.append(footerAttributes) 48 | 49 | } 50 | self.allAttributesArray = self.allAttributesArray?.map({ (attributes) in 51 | let copyed = attributes.copy() as? UICollectionViewLayoutAttributes 52 | return self.updateAttributesForHeaderAndFooter(attributes: copyed)! 53 | }) 54 | 55 | self.allAttributesArray?.append(self.collectionViewHeaderAttributes) 56 | self.allAttributesArray?.append(self.collectionViewFooterAttributes) 57 | 58 | if let array = self.allAttributesArray, array.isEmpty { 59 | self.allAttributesArray = nil 60 | } 61 | 62 | 63 | 64 | } 65 | 66 | open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { 67 | return self.allAttributesArray?.filter({ (attributes) -> Bool in 68 | return attributes.frame.intersects(rect) 69 | }) 70 | } 71 | 72 | 73 | /// 如果确定不会使用到改方法,那这个方法可以不用实现 74 | open override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { 75 | let attributes = super.layoutAttributesForItem(at: indexPath) 76 | return self.updateAttributesForHeaderAndFooter(attributes: attributes) 77 | } 78 | 79 | /// 如果确定不会使用到改方法,那这个方法可以不用实现 80 | open override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { 81 | if elementKind == LXMCollectionElementKindHeader { 82 | return self.collectionViewHeaderAttributes 83 | } else if elementKind == LXMCollectionElementKindFooter { 84 | return self.collectionViewFooterAttributes 85 | } else { 86 | let attributes = super.layoutAttributesForSupplementaryView(ofKind: elementKind, at: indexPath) 87 | return self.updateAttributesForHeaderAndFooter(attributes: attributes) 88 | } 89 | } 90 | 91 | 92 | /// 注意:一定要用super.collectionViewContentSize返回的宽度,系统貌似根据contentInset做了优化计算 93 | open override var collectionViewContentSize: CGSize { 94 | return updateContentSizeForHeaderAndFooter(contentSize: super.collectionViewContentSize) 95 | } 96 | 97 | 98 | open override func shouldInvalidateLayout (forBoundsChange newBounds: CGRect) -> Bool { 99 | if let collectionView = self.collectionView { 100 | if newBounds.width != collectionView.bounds.width { 101 | return true 102 | } 103 | } 104 | return false 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /LXMWaterfallLayout/TestObject.h: -------------------------------------------------------------------------------- 1 | // 2 | // TestObject.h 3 | // LXMWaterfallLayout 4 | // 5 | // Created by luxiaoming on 2017/8/28. 6 | // Copyright © 2017年 duowan. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | 12 | @interface TestObject : NSObject 13 | 14 | @end 15 | 16 | 17 | @interface TestObject (Test) 18 | 19 | @end 20 | 21 | 22 | 23 | 24 | @interface OneObject : TestObject 25 | 26 | @end 27 | -------------------------------------------------------------------------------- /LXMWaterfallLayout/TestObject.m: -------------------------------------------------------------------------------- 1 | // 2 | // TestObject.m 3 | // LXMWaterfallLayout 4 | // 5 | // Created by luxiaoming on 2017/8/28. 6 | // Copyright © 2017年 duowan. All rights reserved. 7 | // 8 | 9 | #import "TestObject.h" 10 | #import 11 | #import 12 | #import "LXMWaterfallLayout-Swift.h" 13 | 14 | @interface TestObject () 15 | 16 | @end 17 | 18 | @implementation TestObject 19 | 20 | @end 21 | 22 | 23 | @implementation TestObject (Test) 24 | 25 | 26 | 27 | @end 28 | 29 | 30 | @implementation OneObject 31 | 32 | 33 | 34 | @end 35 | -------------------------------------------------------------------------------- /LXMWaterfallLayout/TestViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestViewController.swift 3 | // LXMWaterfallLayout 4 | // 5 | // Created by luxiaoming on 2017/8/26. 6 | // Copyright © 2017年 duowan. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class TestViewController: DemoBaseViewController { 12 | 13 | } 14 | 15 | 16 | // MARK: - Lifecycle 17 | extension TestViewController { 18 | 19 | override func viewDidLoad() { 20 | super.viewDidLoad() 21 | 22 | // let layout = collectionView.collectionViewLayout 23 | 24 | 25 | var dict: [Int : String] = [0 : "a", 26 | 1 : "b", 27 | 2 : "c"] 28 | 29 | dict[1] = nil 30 | 31 | var array: [String?] = ["a", "b", "c", nil] 32 | 33 | array.append(nil) 34 | 35 | } 36 | 37 | override func didReceiveMemoryWarning() { 38 | super.didReceiveMemoryWarning() 39 | // Dispose of any resources that can be recreated. 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LXMWaterfallLayout 2 | A collectionViewLayout layout cells like waterfall, which add the missing collectionViewHeader and collectionViewFooter. 3 | 4 | LXMWaterfallLayout is inspired by [CHTCollectionViewWaterfallLayout](https://github.com/chiahsien/CHTCollectionViewWaterfallLayout), and made several improvements to make it easier to use. It is subclass of UICollectionViewLayout and it's usage is just like UICollectionViewFlowLayout. 5 | 6 | ![screenshot](https://github.com/Phelthas/LXMWaterfallLayout/blob/master/ScreenShots/LXMWaterfallLayout.gif) 7 | 8 | [传送门](http://www.jianshu.com/p/82daa5db4a74)和[传送门](http://www.jianshu.com/p/21f97112cc8e)是我写的总结 9 | ## Requirements 10 | Swift3.0 + 11 | Xcode8.0 + 12 | 13 | 14 | 15 | ## Install 16 | 1, CocoaPods 17 | add `pod 'LXMWaterfallLayout'` to your podfile and run `pod install` 18 | 2, Manual 19 | drag `LXMWaterfallLayout.swift` into your project 20 | 21 | 22 | ## Updates 23 | 24 | 1.0.6 25 | * add `LXMHorizontalMenuLayout`; 26 | 27 | 28 | 1.0.4 29 | * update to Swift4.2,Swift4.1 and before please use 1.0.3 30 | 31 | 1.0.3 32 | * Fix a bug 33 | 34 | 1.0.0 35 | * Add `horiziontalAlignment` and `verticalAlignment` property, which make `LXMWaterfallLayout` supports alignment now; 36 | * Add support for `UICollectionViewScrollDirection.horizontal` 37 | 38 | 0.0.4 39 | * Fix bugs with contentInset 40 | 41 | 0.0.3 42 | * Add `LXMLayoutHeaderFooterProtocol` and `LXMHeaderFooterFlowLayout` 43 | Now both `LXMHeaderFooterFlowLayout` and `LXMWaterfallLayout` confirm to `LXMLayoutHeaderFooterProtocol` so the architecture is more clear, what's more, if you have your own collectionViewLayout and you want it to have a header or footer too, you can complete it in minutes by adopting `LXMLayoutHeaderFooterProtocol` 44 | 45 | 46 | ## How to use 47 | It is just like UICollectionViewFlowLayout, all you have to do is `LXMWaterfallLayout()` and assign it to a collectionView 48 | 49 | ## Issues 50 | ``` 51 | if (find any bug || have any problem) { 52 | feel free to open an issue or pull request 53 | } else { 54 | star it if it helps 55 | } 56 | ``` 57 | I will try my best to help as soon as I see it~ 58 | 59 | ## License 60 | LXMWaterfallLayout is available under the MIT license. See the LICENSE file for more info. 61 | 62 | -------------------------------------------------------------------------------- /ScreenShots/LXMWaterfallLayout.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phelthas/LXMWaterfallLayout/2a8bba106b0ec1479a7a953e8703447156de1c47/ScreenShots/LXMWaterfallLayout.gif --------------------------------------------------------------------------------