├── .github └── stale.yml ├── .gitignore ├── .swiftlint.yml ├── Classes ├── GLCollectionTableViewCell.swift ├── GLIndexedCollectionViewCell.swift ├── GLIndexedCollectionViewCell.xib └── GLTableCollectionViewController.swift ├── GLTableCollectionView.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── WorkspaceSettings.xcsettings ├── xcshareddata │ └── xcschemes │ │ └── GLTableCollectionView.xcscheme └── xcuserdata │ └── giuliolombardo.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── GLTableCollectionView ├── AppDelegate.swift ├── Assets.xcassets │ └── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-60@2x-1.png │ │ ├── Icon-60@2x.png │ │ ├── Icon-60@3x-1.png │ │ ├── Icon-60@3x.png │ │ ├── Icon-76.png │ │ ├── Icon-76@2x-1.png │ │ ├── Icon-76@2x.png │ │ ├── Icon-77.png │ │ ├── Icon-AppStore.png │ │ ├── Icon-Small-1.png │ │ ├── Icon-Small.png │ │ ├── Icon-Small@2x-1.png │ │ ├── Icon-Small@2x-2.png │ │ ├── Icon-Small@2x-3.png │ │ ├── Icon-Small@2x.png │ │ ├── Icon-Small@3x-1.png │ │ ├── Icon-Small@3x.png │ │ ├── Icon-Spotlight-40.png │ │ ├── Icon-Spotlight-40@2x-1.png │ │ ├── Icon-Spotlight-40@2x-2.png │ │ ├── Icon-Spotlight-40@2x-3.png │ │ ├── Icon-Spotlight-40@2x.png │ │ ├── Icon-Spotlight-40@3x-1.png │ │ ├── Icon-Spotlight-40@3x.png │ │ ├── Icon-Spotlight-41.png │ │ ├── Icon-iPadPro@2x-1.png │ │ └── Icon-iPadPro@2x.png ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard └── Info.plist ├── GLTableCollectionViewTests ├── GLTableCollectionViewTests.swift └── Info.plist ├── GitHub Page └── Images │ ├── Source │ ├── Diagram.graffle │ ├── Icon.pxm │ └── Logo.pxm │ ├── demonstration.gif │ ├── diagram.png │ └── logo.png ├── ISSUE_TEMPLATE.md ├── LICENSE.txt └── README.md /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - crash 8 | # Label to use when marking an issue as stale 9 | staleLabel: standby 10 | # Comment to post when marking an issue as stale. Set to `false` to disable 11 | markComment: > 12 | This issue has been automatically marked as stale because it has not had 13 | recent activity. It will be closed if no further activity occurs. Thank you 14 | for your contributions. 15 | # Comment to post when closing a stale issue. Set to `false` to disable 16 | closeComment: false 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled source # 2 | ################### 3 | *.com 4 | *.class 5 | *.dll 6 | *.exe 7 | *.o 8 | *.so 9 | 10 | # Packages # 11 | ############ 12 | # it's better to unpack these files and commit the raw source 13 | # git has its own built in compression methods 14 | *.7z 15 | *.dmg 16 | *.gz 17 | *.iso 18 | *.jar 19 | *.rar 20 | *.tar 21 | *.zip 22 | 23 | # Logs and databases # 24 | ###################### 25 | *.log 26 | *.sql 27 | *.sqlite 28 | 29 | # OS generated files # 30 | ###################### 31 | .DS_Store 32 | .DS_Store? 33 | ._* 34 | .Spotlight-V100 35 | .Trashes 36 | ehthumbs.db 37 | Thumbs.db 38 | 39 | # Build generated # 40 | ################### 41 | build/ 42 | DerivedData/ 43 | 44 | # Various settings # 45 | #################### 46 | *.pbxuser 47 | !default.pbxuser 48 | *.mode1v3 49 | !default.mode1v3 50 | *.mode2v3 51 | !default.mode2v3 52 | *.perspectivev3 53 | !default.perspectivev3 54 | xcuserdata/ 55 | 56 | # Other # 57 | ######### 58 | *.moved-aside 59 | *.xccheckout 60 | *.xcscmblueprint 61 | 62 | # Cocoapods # 63 | ############# 64 | Pods -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | opt_in_rules: 2 | - anyobject_protocol 3 | - closure_spacing 4 | - contains_over_first_not_nil 5 | - convenience_type 6 | - empty_count 7 | - empty_string 8 | - empty_xctest_method 9 | - explicit_init 10 | - fallthrough 11 | - fatal_error_message 12 | - file_name 13 | - first_where 14 | - force_unwrapping 15 | - implicit_return 16 | - let_var_whitespace 17 | - literal_expression_end_indentation 18 | - operator_usage_whitespace 19 | - overridden_super_call 20 | - override_in_extension 21 | - private_action 22 | - private_outlet 23 | - prohibited_super_call 24 | - single_test_class 25 | - sorted_imports 26 | - toggle_bool 27 | - unused_import 28 | - unused_private_declaration 29 | 30 | included: 31 | 32 | force_cast: warning 33 | 34 | identifier_name: 35 | min_length: 2 36 | 37 | line_length: 160 38 | 39 | disabled_rules: 40 | - nesting 41 | - custom_rules 42 | -------------------------------------------------------------------------------- /Classes/GLCollectionTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GLCollectionTableViewCell.swift 3 | // GLTableCollectionView 4 | // 5 | // Created by Giulio Lombardo on 24/11/16. 6 | // 7 | // MIT License 8 | // 9 | // Copyright (c) 2018 Giulio Lombardo 10 | // 11 | // Permission is hereby granted, free of charge, to any person obtaining a copy 12 | // of this software and associated documentation files (the "Software"), to 13 | // deal in the Software without restriction, including without limitation the 14 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 15 | // sell copies of the Software, and to permit persons to whom the Software is 16 | // furnished to do so, subject to the following conditions: 17 | // 18 | // The above copyright notice and this permission notice shall be included in 19 | // all copies or substantial portions of the Software. 20 | // 21 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 27 | // IN THE SOFTWARE. 28 | // 29 | 30 | import UIKit 31 | 32 | class GLIndexedCollectionViewFlowLayout: UICollectionViewFlowLayout { 33 | fileprivate var paginatedScroll: Bool? 34 | 35 | override func awakeFromNib() { 36 | super.awakeFromNib() 37 | } 38 | 39 | override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint { 40 | guard proposedContentOffset.x > 0 else { 41 | return CGPoint(x: 0, y: 0) 42 | } 43 | 44 | // If the UICollectionView has paginatedScroll set to false there is no 45 | // need to apply any pagination logic, we will return the current 46 | // proposedContentOffset coordinates. 47 | guard paginatedScroll == true else { 48 | return CGPoint(x: proposedContentOffset.x, y: 0) 49 | } 50 | 51 | // It's not a bad idea to shield us for some strange cases where the 52 | // UICollectionView won't be there for any reason, since it comes in an 53 | // Optional flavor. If it won't be available we return the current 54 | // proposedContentOffset coordinates and exit. 55 | guard let collectionView: UICollectionView = collectionView else { 56 | return CGPoint(x: proposedContentOffset.x, y: 0) 57 | } 58 | 59 | let scannerFrame: CGRect = CGRect(x: proposedContentOffset.x, 60 | y: 0, 61 | width: collectionView.bounds.width, 62 | height: collectionView.bounds.height) 63 | 64 | // If there is no UICollectionViewLayoutAttributes for the given 65 | // scannerFrame there is no reason to calculate a paginated layout for 66 | // it, so we return the current proposedContentOffset coordinates. 67 | guard let layoutAttributes: [UICollectionViewLayoutAttributes] = super.layoutAttributesForElements(in: scannerFrame) else { 68 | return CGPoint(x: proposedContentOffset.x, y: 0) 69 | } 70 | 71 | // To make paginated scrolling work fine this CGFloat below MUST be 72 | // equal to the value set in the insetForSectionAt method of 73 | // UICollectionView's UICollectionViewDelegate Flow Layout. 74 | let collectionViewInsets: CGFloat = 10.0 75 | 76 | // Since UICollectionViewFlowLayout's proposedContentOffset coordinates 77 | // won't take count of any UICollectionView UIEdgeInsets values we need 78 | // to fix it by adding collectionViewInsets to the .x coordinate. 79 | // 80 | // Note: This will only cover horizontal scrolling and pagination, if 81 | // you need vertical pagination replace the .x coordinate with .y and 82 | // update collectionViewInsets value with the appropriate one. 83 | let proposedXCoordWithInsets: CGFloat = proposedContentOffset.x + collectionViewInsets 84 | 85 | // We now create a variable and we assign a very high CGFloat to it (a 86 | // to cover very large UICollectionViewContentSize cases). 87 | // This var will hold the needed horizontal adjustment to make the 88 | // UICollectionView paginate scroll. 89 | var offsetCorrection: CGFloat = .greatestFiniteMagnitude 90 | 91 | // UICollectionViewLayoutAttributes may contain all sort of layout 92 | // attributes so we need to check if it belongs to a 93 | // UICollectionViewCell, otherwise logic won't work. 94 | layoutAttributes.filter { layoutAttribute -> Bool in 95 | 96 | layoutAttribute.representedElementCategory == .cell 97 | }.forEach { cellLayoutAttribute in 98 | 99 | // Now we loop through all the different layout attributes of the 100 | // UICollectionViewCells contained between the .x value of the 101 | // proposedContentOffset and collectionView's width, looking for the 102 | // cell which needs the least offsetCorrection value: it will mean 103 | // that it's the first cell on the left of the screen which will 104 | // give pagination. 105 | // To accurately calculate the offsetCorrection we will check only 106 | // the cells contained in one half of UICollectionView's width, 107 | // following the scrolling direction. The check will be done by the 108 | // if statement below. This will fix the "last cell" issue. 109 | let discardableScrollingElementsFrame: CGFloat = collectionView.contentOffset.x + (collectionView.frame.size.width / 2) 110 | 111 | if (cellLayoutAttribute.center.x <= discardableScrollingElementsFrame && velocity.x > 0) || 112 | (cellLayoutAttribute.center.x >= discardableScrollingElementsFrame && velocity.x < 0) { 113 | return 114 | } 115 | 116 | if abs(cellLayoutAttribute.frame.origin.x - proposedXCoordWithInsets) < abs(offsetCorrection) { 117 | offsetCorrection = cellLayoutAttribute.frame.origin.x - proposedXCoordWithInsets 118 | } 119 | } 120 | 121 | return CGPoint(x: proposedContentOffset.x + offsetCorrection, y: 0) 122 | } 123 | } 124 | 125 | class GLIndexedCollectionView: UICollectionView { 126 | /// The inner-`indexPath` of the GLIndexedCollectionView. 127 | /// 128 | /// Use it to discriminate between all the possible GLIndexedCollectionViews 129 | /// inside `UICollectionView`'s `dataSource` and `delegate` methods. 130 | /// 131 | /// This should be set and updated only through GLCollectionTableViewCell's 132 | /// `setCollectionViewDataSourceDelegate` func to avoid strange behaviors. 133 | var indexPath: IndexPath! 134 | } 135 | 136 | class GLCollectionTableViewCell: UITableViewCell { 137 | /// The `UICollectionView`-inside-a-`UITableViewCell` itself. 138 | /// 139 | /// Keep the variable `public` so it would be easier to access later in the 140 | /// code, for example in UITableView's `dataSource` and 141 | /// `delegate` methods. 142 | /// 143 | /// GLIndexedCollectionView requires a `strong` ARC reference, do not assign 144 | /// a `weak` reference to it otherwise it could be released unexpectedly, 145 | /// causing a crash. 146 | var collectionView: GLIndexedCollectionView! 147 | var collectionFlowLayout: GLIndexedCollectionViewFlowLayout! 148 | 149 | /// A Boolean value that controls whether the `UICollectionViewFlowLayout` 150 | /// of the GLIndexedCollectionView will paginate scroll or not. 151 | /// 152 | /// Set [true]() to make the UICollectionView paginate scroll based on it's 153 | /// `itemSize`, set to [false]() for regular scrolling. The 154 | /// `UICollectionViewFlowLayout` will deduct the right scrolling offset 155 | /// values automatically so you should not set the `itemSize` value 156 | /// directly. 157 | /// 158 | /// Default value is `nil`, since `Bool` is `Optional`. 159 | var collectionViewPaginatedScroll: Bool? 160 | 161 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 162 | super.init(style: style, reuseIdentifier: reuseIdentifier) 163 | 164 | collectionFlowLayout = GLIndexedCollectionViewFlowLayout() 165 | collectionFlowLayout.scrollDirection = .horizontal 166 | 167 | collectionView = GLIndexedCollectionView(frame: .zero, collectionViewLayout: collectionFlowLayout) 168 | collectionView.register(UINib(nibName: "GLIndexedCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: GLIndexedCollectionViewCell.identifier) 169 | collectionView.backgroundColor = .white 170 | collectionView.showsHorizontalScrollIndicator = false 171 | collectionView.showsVerticalScrollIndicator = false 172 | collectionView.bounces = true 173 | collectionView.isDirectionalLockEnabled = true 174 | collectionView.isMultipleTouchEnabled = false 175 | collectionView.isOpaque = true 176 | 177 | contentView.addSubview(collectionView) 178 | } 179 | 180 | required init?(coder _: NSCoder) { 181 | fatalError("init(coder:) has not been implemented") 182 | } 183 | 184 | override func awakeFromNib() { 185 | super.awakeFromNib() 186 | // Initialization code 187 | } 188 | 189 | final override func layoutSubviews() { 190 | super.layoutSubviews() 191 | 192 | collectionFlowLayout.paginatedScroll = collectionViewPaginatedScroll 193 | 194 | if collectionViewPaginatedScroll == true { 195 | collectionView.isPagingEnabled = false 196 | } 197 | 198 | guard collectionView.frame != contentView.bounds else { 199 | return 200 | } 201 | 202 | collectionView.frame = contentView.bounds 203 | } 204 | 205 | override func setSelected(_ selected: Bool, animated: Bool) { 206 | super.setSelected(selected, animated: animated) 207 | 208 | // Configure the view for the selected state 209 | } 210 | 211 | /// Re-assigns `dataSource` and `delegate` classes back to the 212 | /// GLIndexedCollectionView inside GLCollectionTableViewCell. 213 | /// 214 | /// Call this `func` in your [tableView(_:willDisplay:forRowAt:)](apple-reference-documentation://hs3G9NleF7) 215 | /// method of GLTableCollectionViewController so the UITableView will 216 | /// re-assign it automatically following the regular UITableViewCells reuse 217 | /// logic. 218 | /// 219 | /// This method will also check if the re-assignment are needed or not. 220 | /// 221 | /// - Parameter dataSource: The `dataSource` class for the 222 | /// GLIndexedCollectionView in the GLCollectionTableViewCell, responsible 223 | /// for the UICollectionView's `dataSource` methods. 224 | /// 225 | /// - Parameter delegate: The `delegate class` for the 226 | /// GLIndexedCollectionView in the GLCollectionTableViewCell, responsible 227 | /// for the UICollectionView's delegation methods. 228 | /// 229 | /// - Parameter indexPath: The inner-`indexPath` of the 230 | /// GLIndexedCollectionView, it's recommended to pass the same `indexPath` 231 | /// of the UITableViewCell to the GLIndexedCollectionView so they will share 232 | /// the same `indexPath.section` making easier to understand from which 233 | /// UITableViewCell the UICollectionView will come from. 234 | final func setCollectionView(dataSource: UICollectionViewDataSource, delegate: UICollectionViewDelegate, indexPath: IndexPath) { 235 | collectionView.indexPath = indexPath 236 | 237 | if collectionView.dataSource == nil { 238 | collectionView.dataSource = dataSource 239 | } 240 | 241 | if collectionView.delegate == nil { 242 | collectionView.delegate = delegate 243 | } 244 | 245 | collectionView.reloadData() 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /Classes/GLIndexedCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GLIndexedCollectionViewCell.swift 3 | // GLTableCollectionView 4 | // 5 | // Created by Giulio Lombardo on 24/11/16. 6 | // 7 | // MIT License 8 | // 9 | // Copyright (c) 2018 Giulio Lombardo 10 | // 11 | // Permission is hereby granted, free of charge, to any person obtaining a copy 12 | // of this software and associated documentation files (the "Software"), to 13 | // deal in the Software without restriction, including without limitation the 14 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 15 | // sell copies of the Software, and to permit persons to whom the Software is 16 | // furnished to do so, subject to the following conditions: 17 | // 18 | // The above copyright notice and this permission notice shall be included in 19 | // all copies or substantial portions of the Software. 20 | // 21 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 27 | // IN THE SOFTWARE. 28 | // 29 | 30 | import UIKit 31 | 32 | class GLIndexedCollectionViewCell: UICollectionViewCell { 33 | static let identifier: String = "collectionViewCellID" 34 | 35 | override func awakeFromNib() { 36 | super.awakeFromNib() 37 | // Initialization code 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Classes/GLIndexedCollectionViewCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Classes/GLTableCollectionViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GLTableCollectionViewController.swift 3 | // GLTableCollectionView 4 | // 5 | // Created by Giulio Lombardo on 24/11/16. 6 | // 7 | // MIT License 8 | // 9 | // Copyright (c) 2018 Giulio Lombardo 10 | // 11 | // Permission is hereby granted, free of charge, to any person obtaining a copy 12 | // of this software and associated documentation files (the "Software"), to 13 | // deal in the Software without restriction, including without limitation the 14 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 15 | // sell copies of the Software, and to permit persons to whom the Software is 16 | // furnished to do so, subject to the following conditions: 17 | // 18 | // The above copyright notice and this permission notice shall be included in 19 | // all copies or substantial portions of the Software. 20 | // 21 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 27 | // IN THE SOFTWARE. 28 | // 29 | 30 | import UIKit 31 | 32 | final class GLTableCollectionViewController: UITableViewController, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout { 33 | // This static string constant will be the cellIdentifier for the 34 | // UITableViewCells holding the UICollectionView, it's important to append 35 | // "_section#" to it so we can understand which cell is the one we are 36 | // looking for in the debugger. Look in UITableView's data source 37 | // cellForRowAt method for more explanations about the UITableViewCell reuse 38 | // handling. 39 | static let tableCellID: String = "tableViewCellID_section_#" 40 | 41 | let numberOfSections: Int = 20 42 | let numberOfCollectionsForRow: Int = 1 43 | let numberOfCollectionItems: Int = 20 44 | 45 | var colorsDict: [Int: [UIColor]] = [:] 46 | 47 | /// Set true to enable UICollectionViews scroll pagination 48 | var paginationEnabled: Bool = true 49 | 50 | override func viewDidLoad() { 51 | super.viewDidLoad() 52 | 53 | // Uncomment the following line to preserve selection between 54 | // presentations 55 | // self.clearsSelectionOnViewWillAppear = false 56 | 57 | // Uncomment the following line to display an Edit button in the 58 | // navigation bar for this view controller. 59 | // self.navigationItem.rightBarButtonItem = self.editButtonItem() 60 | 61 | (0 ... numberOfSections - 1).forEach { section in 62 | colorsDict[section] = randomRowColors() 63 | } 64 | } 65 | 66 | private final func randomRowColors() -> [UIColor] { 67 | let colors: [UIColor] = (0 ... numberOfCollectionItems - 1).map({ _ -> UIColor in 68 | var randomRed: CGFloat = CGFloat(arc4random_uniform(256)) 69 | let randomGreen: CGFloat = CGFloat(arc4random_uniform(256)) 70 | let randomBlue: CGFloat = CGFloat(arc4random_uniform(256)) 71 | 72 | if randomRed == 255.0 && randomGreen == 255.0 && randomBlue == 255.0 { 73 | randomRed = CGFloat(arc4random_uniform(128)) 74 | } 75 | 76 | let color: UIColor 77 | 78 | if #available(iOS 10.0, *) { 79 | if traitCollection.displayGamut == .P3 { 80 | color = UIColor(displayP3Red: randomRed / 255.0, green: randomGreen / 255.0, blue: randomBlue / 255.0, alpha: 1.0) 81 | } else { 82 | color = UIColor(red: randomRed / 255.0, green: randomGreen / 255.0, blue: randomBlue / 255.0, alpha: 1.0) 83 | } 84 | } else { 85 | color = UIColor(red: randomRed / 255.0, green: randomGreen / 255.0, blue: randomBlue / 255.0, alpha: 1.0) 86 | } 87 | 88 | return color 89 | }) 90 | 91 | return colors 92 | } 93 | 94 | override func didReceiveMemoryWarning() { 95 | super.didReceiveMemoryWarning() 96 | // Dispose of any resources that can be recreated. 97 | } 98 | 99 | // MARK: 100 | 101 | override func numberOfSections(in _: UITableView) -> Int { 102 | return numberOfSections 103 | } 104 | 105 | override func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { 106 | return numberOfCollectionsForRow 107 | } 108 | 109 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 110 | // Instead of having a single cellIdentifier for each type of 111 | // UITableViewCells, like in a regular implementation, we have multiple 112 | // cellIDs, each related to a indexPath section. By Doing so the 113 | // UITableViewCells will still be recycled but only with 114 | // dequeueReusableCell of that section. 115 | // 116 | // For example the cellIdentifier for section 4 cells will be: 117 | // 118 | // "tableViewCellID_section_#3" 119 | // 120 | // dequeueReusableCell will only reuse previous UITableViewCells with 121 | // the same cellIdentifier instead of using any UITableViewCell as a 122 | // regular UITableView would do, this is necessary because every cell 123 | // will have a different UICollectionView with UICollectionViewCells in 124 | // it and UITableView reuse won't work as expected giving back wrong 125 | // cells. 126 | 127 | var cell: GLCollectionTableViewCell? = tableView.dequeueReusableCell(withIdentifier: GLTableCollectionViewController.tableCellID + indexPath.section.description) as? GLCollectionTableViewCell 128 | 129 | if cell == nil { 130 | cell = GLCollectionTableViewCell(style: .default, reuseIdentifier: GLTableCollectionViewController.tableCellID + indexPath.section.description) 131 | 132 | // Configure the cell... 133 | cell?.selectionStyle = .none 134 | cell?.collectionViewPaginatedScroll = paginationEnabled 135 | } 136 | 137 | return cell! 138 | } 139 | 140 | override func tableView(_: UITableView, titleForHeaderInSection section: Int) -> String? { 141 | return "Section: " + section.description 142 | } 143 | 144 | // MARK: 145 | 146 | override func tableView(_: UITableView, heightForRowAt _: IndexPath) -> CGFloat { 147 | return 88 148 | } 149 | 150 | override func tableView(_: UITableView, heightForHeaderInSection _: Int) -> CGFloat { 151 | return 28 152 | } 153 | 154 | override func tableView(_: UITableView, heightForFooterInSection _: Int) -> CGFloat { 155 | return 0.0001 156 | } 157 | 158 | override func tableView(_: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { 159 | guard let cell: GLCollectionTableViewCell = cell as? GLCollectionTableViewCell else { 160 | return 161 | } 162 | 163 | cell.setCollectionView(dataSource: self, delegate: self, indexPath: indexPath) 164 | } 165 | 166 | // MARK: 167 | 168 | func numberOfSections(in _: UICollectionView) -> Int { 169 | return 1 170 | } 171 | 172 | func collectionView(_: UICollectionView, numberOfItemsInSection _: Int) -> Int { 173 | return numberOfCollectionItems 174 | } 175 | 176 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 177 | guard let cell: GLIndexedCollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: GLIndexedCollectionViewCell.identifier, for: indexPath) as? GLIndexedCollectionViewCell else { 178 | fatalError("UICollectionViewCell must be of GLIndexedCollectionViewCell type") 179 | } 180 | 181 | guard let indexedCollectionView: GLIndexedCollectionView = collectionView as? GLIndexedCollectionView else { 182 | fatalError("UICollectionView must be of GLIndexedCollectionView type") 183 | } 184 | 185 | // Configure the cell... 186 | cell.backgroundColor = colorsDict[indexedCollectionView.indexPath.section]?[indexPath.row] 187 | 188 | return cell 189 | } 190 | 191 | // MARK: 192 | 193 | let collectionTopInset: CGFloat = 0 194 | let collectionBottomInset: CGFloat = 0 195 | let collectionLeftInset: CGFloat = 10 196 | let collectionRightInset: CGFloat = 10 197 | 198 | func collectionView(_: UICollectionView, layout _: UICollectionViewLayout, insetForSectionAt _: Int) -> UIEdgeInsets { 199 | return UIEdgeInsets(top: collectionTopInset, left: collectionLeftInset, bottom: collectionBottomInset, right: collectionRightInset) 200 | } 201 | 202 | func collectionView(_: UICollectionView, layout _: UICollectionViewLayout, sizeForItemAt _: IndexPath) -> CGSize { 203 | let tableViewCellHeight: CGFloat = tableView.rowHeight 204 | let collectionItemWidth: CGFloat = tableViewCellHeight - (collectionLeftInset + collectionRightInset) 205 | let collectionViewHeight: CGFloat = collectionItemWidth 206 | 207 | return CGSize(width: collectionItemWidth, height: collectionViewHeight) 208 | } 209 | 210 | func collectionView(_: UICollectionView, layout _: UICollectionViewLayout, minimumLineSpacingForSectionAt _: Int) -> CGFloat { 211 | return 10 212 | } 213 | 214 | func collectionView(_: UICollectionView, layout _: UICollectionViewLayout, minimumInteritemSpacingForSectionAt _: Int) -> CGFloat { 215 | return 0 216 | } 217 | 218 | // MARK: 219 | 220 | func collectionView(_: UICollectionView, didSelectItemAt _: IndexPath) { 221 | } 222 | 223 | /* 224 | // MARK: 225 | 226 | // In a storyboard-based application, you will often want to do a little 227 | // preparation before navigation 228 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 229 | // Get the new view controller using segue.destinationViewController. 230 | // Pass the selected object to the new view controller. 231 | } 232 | */ 233 | } 234 | -------------------------------------------------------------------------------- /GLTableCollectionView.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 48; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 742CE82E1E099064000ACFB6 /* GLTableCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 742CE82D1E099064000ACFB6 /* GLTableCollectionViewController.swift */; }; 11 | 742CE8301E099070000ACFB6 /* GLCollectionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 742CE82F1E099070000ACFB6 /* GLCollectionTableViewCell.swift */; }; 12 | 742CE8321E099077000ACFB6 /* GLIndexedCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 742CE8311E099077000ACFB6 /* GLIndexedCollectionViewCell.swift */; }; 13 | 742CE8341E09907D000ACFB6 /* GLIndexedCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 742CE8331E09907D000ACFB6 /* GLIndexedCollectionViewCell.xib */; }; 14 | 747CF8FD1DE6EB010051A5FF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 747CF8FC1DE6EB010051A5FF /* AppDelegate.swift */; }; 15 | 747CF9021DE6EB010051A5FF /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 747CF9001DE6EB010051A5FF /* Main.storyboard */; }; 16 | 747CF9041DE6EB010051A5FF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 747CF9031DE6EB010051A5FF /* Assets.xcassets */; }; 17 | 747CF9071DE6EB010051A5FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 747CF9051DE6EB010051A5FF /* LaunchScreen.storyboard */; }; 18 | 747CF9121DE6EB010051A5FF /* GLTableCollectionViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 747CF9111DE6EB010051A5FF /* GLTableCollectionViewTests.swift */; }; 19 | /* End PBXBuildFile section */ 20 | 21 | /* Begin PBXContainerItemProxy section */ 22 | 747CF90E1DE6EB010051A5FF /* PBXContainerItemProxy */ = { 23 | isa = PBXContainerItemProxy; 24 | containerPortal = 747CF8F11DE6EB010051A5FF /* Project object */; 25 | proxyType = 1; 26 | remoteGlobalIDString = 747CF8F81DE6EB010051A5FF; 27 | remoteInfo = GLTableCollectionView; 28 | }; 29 | /* End PBXContainerItemProxy section */ 30 | 31 | /* Begin PBXFileReference section */ 32 | 742CE82D1E099064000ACFB6 /* GLTableCollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GLTableCollectionViewController.swift; path = Classes/GLTableCollectionViewController.swift; sourceTree = SOURCE_ROOT; }; 33 | 742CE82F1E099070000ACFB6 /* GLCollectionTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GLCollectionTableViewCell.swift; path = Classes/GLCollectionTableViewCell.swift; sourceTree = SOURCE_ROOT; }; 34 | 742CE8311E099077000ACFB6 /* GLIndexedCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GLIndexedCollectionViewCell.swift; path = Classes/GLIndexedCollectionViewCell.swift; sourceTree = SOURCE_ROOT; }; 35 | 742CE8331E09907D000ACFB6 /* GLIndexedCollectionViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = GLIndexedCollectionViewCell.xib; path = Classes/GLIndexedCollectionViewCell.xib; sourceTree = SOURCE_ROOT; }; 36 | 747CF8F91DE6EB010051A5FF /* GLTableCollectionView.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GLTableCollectionView.app; sourceTree = BUILT_PRODUCTS_DIR; }; 37 | 747CF8FC1DE6EB010051A5FF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 38 | 747CF9011DE6EB010051A5FF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 39 | 747CF9031DE6EB010051A5FF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 40 | 747CF9061DE6EB010051A5FF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 41 | 747CF9081DE6EB010051A5FF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 42 | 747CF90D1DE6EB010051A5FF /* GLTableCollectionViewTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GLTableCollectionViewTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 43 | 747CF9111DE6EB010051A5FF /* GLTableCollectionViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GLTableCollectionViewTests.swift; sourceTree = ""; }; 44 | 747CF9131DE6EB010051A5FF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 45 | /* End PBXFileReference section */ 46 | 47 | /* Begin PBXFrameworksBuildPhase section */ 48 | 747CF8F61DE6EB010051A5FF /* Frameworks */ = { 49 | isa = PBXFrameworksBuildPhase; 50 | buildActionMask = 2147483647; 51 | files = ( 52 | ); 53 | runOnlyForDeploymentPostprocessing = 0; 54 | }; 55 | 747CF90A1DE6EB010051A5FF /* Frameworks */ = { 56 | isa = PBXFrameworksBuildPhase; 57 | buildActionMask = 2147483647; 58 | files = ( 59 | ); 60 | runOnlyForDeploymentPostprocessing = 0; 61 | }; 62 | /* End PBXFrameworksBuildPhase section */ 63 | 64 | /* Begin PBXGroup section */ 65 | 747CF8F01DE6EB010051A5FF = { 66 | isa = PBXGroup; 67 | children = ( 68 | 747CF8FB1DE6EB010051A5FF /* GLTableCollectionView */, 69 | 747CF9101DE6EB010051A5FF /* GLTableCollectionViewTests */, 70 | 747CF8FA1DE6EB010051A5FF /* Products */, 71 | ); 72 | sourceTree = ""; 73 | }; 74 | 747CF8FA1DE6EB010051A5FF /* Products */ = { 75 | isa = PBXGroup; 76 | children = ( 77 | 747CF8F91DE6EB010051A5FF /* GLTableCollectionView.app */, 78 | 747CF90D1DE6EB010051A5FF /* GLTableCollectionViewTests.xctest */, 79 | ); 80 | name = Products; 81 | sourceTree = ""; 82 | }; 83 | 747CF8FB1DE6EB010051A5FF /* GLTableCollectionView */ = { 84 | isa = PBXGroup; 85 | children = ( 86 | 747CF8FC1DE6EB010051A5FF /* AppDelegate.swift */, 87 | 742CE82D1E099064000ACFB6 /* GLTableCollectionViewController.swift */, 88 | 742CE82F1E099070000ACFB6 /* GLCollectionTableViewCell.swift */, 89 | 742CE8311E099077000ACFB6 /* GLIndexedCollectionViewCell.swift */, 90 | 742CE8331E09907D000ACFB6 /* GLIndexedCollectionViewCell.xib */, 91 | 747CF9001DE6EB010051A5FF /* Main.storyboard */, 92 | 747CF9051DE6EB010051A5FF /* LaunchScreen.storyboard */, 93 | 747CF9031DE6EB010051A5FF /* Assets.xcassets */, 94 | 747CF9081DE6EB010051A5FF /* Info.plist */, 95 | ); 96 | path = GLTableCollectionView; 97 | sourceTree = ""; 98 | }; 99 | 747CF9101DE6EB010051A5FF /* GLTableCollectionViewTests */ = { 100 | isa = PBXGroup; 101 | children = ( 102 | 747CF9111DE6EB010051A5FF /* GLTableCollectionViewTests.swift */, 103 | 747CF9131DE6EB010051A5FF /* Info.plist */, 104 | ); 105 | path = GLTableCollectionViewTests; 106 | sourceTree = ""; 107 | }; 108 | /* End PBXGroup section */ 109 | 110 | /* Begin PBXNativeTarget section */ 111 | 747CF8F81DE6EB010051A5FF /* GLTableCollectionView */ = { 112 | isa = PBXNativeTarget; 113 | buildConfigurationList = 747CF9161DE6EB010051A5FF /* Build configuration list for PBXNativeTarget "GLTableCollectionView" */; 114 | buildPhases = ( 115 | EF73511D1E8E9B5800F73EDB /* SwiftLint */, 116 | 747CF8F51DE6EB010051A5FF /* Sources */, 117 | 747CF8F61DE6EB010051A5FF /* Frameworks */, 118 | 747CF8F71DE6EB010051A5FF /* Resources */, 119 | ); 120 | buildRules = ( 121 | ); 122 | dependencies = ( 123 | ); 124 | name = GLTableCollectionView; 125 | productName = GLTableCollectionView; 126 | productReference = 747CF8F91DE6EB010051A5FF /* GLTableCollectionView.app */; 127 | productType = "com.apple.product-type.application"; 128 | }; 129 | 747CF90C1DE6EB010051A5FF /* GLTableCollectionViewTests */ = { 130 | isa = PBXNativeTarget; 131 | buildConfigurationList = 747CF9191DE6EB010051A5FF /* Build configuration list for PBXNativeTarget "GLTableCollectionViewTests" */; 132 | buildPhases = ( 133 | 747CF9091DE6EB010051A5FF /* Sources */, 134 | 747CF90A1DE6EB010051A5FF /* Frameworks */, 135 | 747CF90B1DE6EB010051A5FF /* Resources */, 136 | ); 137 | buildRules = ( 138 | ); 139 | dependencies = ( 140 | 747CF90F1DE6EB010051A5FF /* PBXTargetDependency */, 141 | ); 142 | name = GLTableCollectionViewTests; 143 | productName = GLTableCollectionViewTests; 144 | productReference = 747CF90D1DE6EB010051A5FF /* GLTableCollectionViewTests.xctest */; 145 | productType = "com.apple.product-type.bundle.unit-test"; 146 | }; 147 | /* End PBXNativeTarget section */ 148 | 149 | /* Begin PBXProject section */ 150 | 747CF8F11DE6EB010051A5FF /* Project object */ = { 151 | isa = PBXProject; 152 | attributes = { 153 | CLASSPREFIX = GL; 154 | LastSwiftUpdateCheck = 0800; 155 | LastUpgradeCheck = 0930; 156 | ORGANIZATIONNAME = "Giulio Lombardo"; 157 | TargetAttributes = { 158 | 747CF8F81DE6EB010051A5FF = { 159 | CreatedOnToolsVersion = 8.0; 160 | LastSwiftMigration = 1010; 161 | ProvisioningStyle = Manual; 162 | }; 163 | 747CF90C1DE6EB010051A5FF = { 164 | CreatedOnToolsVersion = 8.0; 165 | LastSwiftMigration = 1010; 166 | ProvisioningStyle = Manual; 167 | TestTargetID = 747CF8F81DE6EB010051A5FF; 168 | }; 169 | }; 170 | }; 171 | buildConfigurationList = 747CF8F41DE6EB010051A5FF /* Build configuration list for PBXProject "GLTableCollectionView" */; 172 | compatibilityVersion = "Xcode 8.0"; 173 | developmentRegion = English; 174 | hasScannedForEncodings = 0; 175 | knownRegions = ( 176 | en, 177 | Base, 178 | ); 179 | mainGroup = 747CF8F01DE6EB010051A5FF; 180 | productRefGroup = 747CF8FA1DE6EB010051A5FF /* Products */; 181 | projectDirPath = ""; 182 | projectRoot = ""; 183 | targets = ( 184 | 747CF8F81DE6EB010051A5FF /* GLTableCollectionView */, 185 | 747CF90C1DE6EB010051A5FF /* GLTableCollectionViewTests */, 186 | ); 187 | }; 188 | /* End PBXProject section */ 189 | 190 | /* Begin PBXResourcesBuildPhase section */ 191 | 747CF8F71DE6EB010051A5FF /* Resources */ = { 192 | isa = PBXResourcesBuildPhase; 193 | buildActionMask = 2147483647; 194 | files = ( 195 | 747CF9071DE6EB010051A5FF /* LaunchScreen.storyboard in Resources */, 196 | 747CF9041DE6EB010051A5FF /* Assets.xcassets in Resources */, 197 | 742CE8341E09907D000ACFB6 /* GLIndexedCollectionViewCell.xib in Resources */, 198 | 747CF9021DE6EB010051A5FF /* Main.storyboard in Resources */, 199 | ); 200 | runOnlyForDeploymentPostprocessing = 0; 201 | }; 202 | 747CF90B1DE6EB010051A5FF /* Resources */ = { 203 | isa = PBXResourcesBuildPhase; 204 | buildActionMask = 2147483647; 205 | files = ( 206 | ); 207 | runOnlyForDeploymentPostprocessing = 0; 208 | }; 209 | /* End PBXResourcesBuildPhase section */ 210 | 211 | /* Begin PBXShellScriptBuildPhase section */ 212 | EF73511D1E8E9B5800F73EDB /* SwiftLint */ = { 213 | isa = PBXShellScriptBuildPhase; 214 | buildActionMask = 2147483647; 215 | files = ( 216 | ); 217 | inputPaths = ( 218 | ); 219 | name = SwiftLint; 220 | outputPaths = ( 221 | ); 222 | runOnlyForDeploymentPostprocessing = 0; 223 | shellPath = /bin/sh; 224 | shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; 225 | }; 226 | /* End PBXShellScriptBuildPhase section */ 227 | 228 | /* Begin PBXSourcesBuildPhase section */ 229 | 747CF8F51DE6EB010051A5FF /* Sources */ = { 230 | isa = PBXSourcesBuildPhase; 231 | buildActionMask = 2147483647; 232 | files = ( 233 | 742CE8301E099070000ACFB6 /* GLCollectionTableViewCell.swift in Sources */, 234 | 742CE82E1E099064000ACFB6 /* GLTableCollectionViewController.swift in Sources */, 235 | 747CF8FD1DE6EB010051A5FF /* AppDelegate.swift in Sources */, 236 | 742CE8321E099077000ACFB6 /* GLIndexedCollectionViewCell.swift in Sources */, 237 | ); 238 | runOnlyForDeploymentPostprocessing = 0; 239 | }; 240 | 747CF9091DE6EB010051A5FF /* Sources */ = { 241 | isa = PBXSourcesBuildPhase; 242 | buildActionMask = 2147483647; 243 | files = ( 244 | 747CF9121DE6EB010051A5FF /* GLTableCollectionViewTests.swift in Sources */, 245 | ); 246 | runOnlyForDeploymentPostprocessing = 0; 247 | }; 248 | /* End PBXSourcesBuildPhase section */ 249 | 250 | /* Begin PBXTargetDependency section */ 251 | 747CF90F1DE6EB010051A5FF /* PBXTargetDependency */ = { 252 | isa = PBXTargetDependency; 253 | target = 747CF8F81DE6EB010051A5FF /* GLTableCollectionView */; 254 | targetProxy = 747CF90E1DE6EB010051A5FF /* PBXContainerItemProxy */; 255 | }; 256 | /* End PBXTargetDependency section */ 257 | 258 | /* Begin PBXVariantGroup section */ 259 | 747CF9001DE6EB010051A5FF /* Main.storyboard */ = { 260 | isa = PBXVariantGroup; 261 | children = ( 262 | 747CF9011DE6EB010051A5FF /* Base */, 263 | ); 264 | name = Main.storyboard; 265 | sourceTree = ""; 266 | }; 267 | 747CF9051DE6EB010051A5FF /* LaunchScreen.storyboard */ = { 268 | isa = PBXVariantGroup; 269 | children = ( 270 | 747CF9061DE6EB010051A5FF /* Base */, 271 | ); 272 | name = LaunchScreen.storyboard; 273 | sourceTree = ""; 274 | }; 275 | /* End PBXVariantGroup section */ 276 | 277 | /* Begin XCBuildConfiguration section */ 278 | 747CF9141DE6EB010051A5FF /* Debug */ = { 279 | isa = XCBuildConfiguration; 280 | buildSettings = { 281 | ALWAYS_SEARCH_USER_PATHS = NO; 282 | CLANG_ANALYZER_NONNULL = YES; 283 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 284 | CLANG_CXX_LIBRARY = "libc++"; 285 | CLANG_ENABLE_MODULES = YES; 286 | CLANG_ENABLE_OBJC_ARC = YES; 287 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 288 | CLANG_WARN_BOOL_CONVERSION = YES; 289 | CLANG_WARN_COMMA = YES; 290 | CLANG_WARN_CONSTANT_CONVERSION = YES; 291 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 292 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 293 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 294 | CLANG_WARN_EMPTY_BODY = YES; 295 | CLANG_WARN_ENUM_CONVERSION = YES; 296 | CLANG_WARN_INFINITE_RECURSION = YES; 297 | CLANG_WARN_INT_CONVERSION = YES; 298 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 299 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 300 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 301 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 302 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 303 | CLANG_WARN_STRICT_PROTOTYPES = YES; 304 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 305 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 306 | CLANG_WARN_UNREACHABLE_CODE = YES; 307 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 308 | CODE_SIGN_IDENTITY = ""; 309 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 310 | COPY_PHASE_STRIP = NO; 311 | DEBUG_INFORMATION_FORMAT = dwarf; 312 | ENABLE_STRICT_OBJC_MSGSEND = YES; 313 | ENABLE_TESTABILITY = YES; 314 | GCC_C_LANGUAGE_STANDARD = gnu99; 315 | GCC_DYNAMIC_NO_PIC = NO; 316 | GCC_NO_COMMON_BLOCKS = YES; 317 | GCC_OPTIMIZATION_LEVEL = 0; 318 | GCC_PREPROCESSOR_DEFINITIONS = ( 319 | "DEBUG=1", 320 | "$(inherited)", 321 | ); 322 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 323 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 324 | GCC_WARN_UNDECLARED_SELECTOR = YES; 325 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 326 | GCC_WARN_UNUSED_FUNCTION = YES; 327 | GCC_WARN_UNUSED_VARIABLE = YES; 328 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 329 | MTL_ENABLE_DEBUG_INFO = YES; 330 | ONLY_ACTIVE_ARCH = YES; 331 | SDKROOT = iphoneos; 332 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 333 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 334 | SWIFT_VERSION = 4.0; 335 | TARGETED_DEVICE_FAMILY = "1,2"; 336 | }; 337 | name = Debug; 338 | }; 339 | 747CF9151DE6EB010051A5FF /* Release */ = { 340 | isa = XCBuildConfiguration; 341 | buildSettings = { 342 | ALWAYS_SEARCH_USER_PATHS = NO; 343 | CLANG_ANALYZER_NONNULL = YES; 344 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 345 | CLANG_CXX_LIBRARY = "libc++"; 346 | CLANG_ENABLE_MODULES = YES; 347 | CLANG_ENABLE_OBJC_ARC = YES; 348 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 349 | CLANG_WARN_BOOL_CONVERSION = YES; 350 | CLANG_WARN_COMMA = YES; 351 | CLANG_WARN_CONSTANT_CONVERSION = YES; 352 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 353 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 354 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 355 | CLANG_WARN_EMPTY_BODY = YES; 356 | CLANG_WARN_ENUM_CONVERSION = YES; 357 | CLANG_WARN_INFINITE_RECURSION = YES; 358 | CLANG_WARN_INT_CONVERSION = YES; 359 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 360 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 361 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 362 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 363 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 364 | CLANG_WARN_STRICT_PROTOTYPES = YES; 365 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 366 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 367 | CLANG_WARN_UNREACHABLE_CODE = YES; 368 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 369 | CODE_SIGN_IDENTITY = ""; 370 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 371 | COPY_PHASE_STRIP = NO; 372 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 373 | ENABLE_NS_ASSERTIONS = NO; 374 | ENABLE_STRICT_OBJC_MSGSEND = YES; 375 | GCC_C_LANGUAGE_STANDARD = gnu99; 376 | GCC_NO_COMMON_BLOCKS = YES; 377 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 378 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 379 | GCC_WARN_UNDECLARED_SELECTOR = YES; 380 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 381 | GCC_WARN_UNUSED_FUNCTION = YES; 382 | GCC_WARN_UNUSED_VARIABLE = YES; 383 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 384 | MTL_ENABLE_DEBUG_INFO = NO; 385 | SDKROOT = iphoneos; 386 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 387 | SWIFT_VERSION = 4.0; 388 | TARGETED_DEVICE_FAMILY = "1,2"; 389 | VALIDATE_PRODUCT = YES; 390 | }; 391 | name = Release; 392 | }; 393 | 747CF9171DE6EB010051A5FF /* Debug */ = { 394 | isa = XCBuildConfiguration; 395 | buildSettings = { 396 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 397 | CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; 398 | CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES; 399 | CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES; 400 | CLANG_ENABLE_OBJC_WEAK = YES; 401 | CLANG_WARN_ASSIGN_ENUM = YES; 402 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 403 | CLANG_WARN_IMPLICIT_SIGN_CONVERSION = YES; 404 | CLANG_WARN_OBJC_EXPLICIT_OWNERSHIP_TYPE = YES; 405 | CLANG_WARN_OBJC_IMPLICIT_ATOMIC_PROPERTIES = YES; 406 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 407 | CLANG_WARN_OBJC_MISSING_PROPERTY_SYNTHESIS = YES; 408 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES; 409 | CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES; 410 | CODE_SIGN_IDENTITY = ""; 411 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 412 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 413 | DEVELOPMENT_TEAM = ""; 414 | GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES; 415 | GCC_WARN_ABOUT_MISSING_NEWLINE = YES; 416 | GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; 417 | GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; 418 | GCC_WARN_SIGN_COMPARE = YES; 419 | GCC_WARN_UNKNOWN_PRAGMAS = YES; 420 | GCC_WARN_UNUSED_LABEL = YES; 421 | GCC_WARN_UNUSED_PARAMETER = YES; 422 | INFOPLIST_FILE = GLTableCollectionView/Info.plist; 423 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 424 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 425 | PRODUCT_BUNDLE_IDENTIFIER = giuliolombardo.GLTableCollectionView; 426 | PRODUCT_NAME = "$(TARGET_NAME)"; 427 | PROVISIONING_PROFILE = ""; 428 | PROVISIONING_PROFILE_SPECIFIER = ""; 429 | SWIFT_VERSION = 4.2; 430 | }; 431 | name = Debug; 432 | }; 433 | 747CF9181DE6EB010051A5FF /* Release */ = { 434 | isa = XCBuildConfiguration; 435 | buildSettings = { 436 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 437 | CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; 438 | CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES; 439 | CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES; 440 | CLANG_ENABLE_OBJC_WEAK = YES; 441 | CLANG_WARN_ASSIGN_ENUM = YES; 442 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 443 | CLANG_WARN_IMPLICIT_SIGN_CONVERSION = YES; 444 | CLANG_WARN_OBJC_EXPLICIT_OWNERSHIP_TYPE = YES; 445 | CLANG_WARN_OBJC_IMPLICIT_ATOMIC_PROPERTIES = YES; 446 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 447 | CLANG_WARN_OBJC_MISSING_PROPERTY_SYNTHESIS = YES; 448 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES; 449 | CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES; 450 | CODE_SIGN_IDENTITY = ""; 451 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 452 | DEVELOPMENT_TEAM = ""; 453 | GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES; 454 | GCC_WARN_ABOUT_MISSING_NEWLINE = YES; 455 | GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; 456 | GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; 457 | GCC_WARN_SIGN_COMPARE = YES; 458 | GCC_WARN_UNKNOWN_PRAGMAS = YES; 459 | GCC_WARN_UNUSED_LABEL = YES; 460 | GCC_WARN_UNUSED_PARAMETER = YES; 461 | INFOPLIST_FILE = GLTableCollectionView/Info.plist; 462 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 463 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 464 | PRODUCT_BUNDLE_IDENTIFIER = giuliolombardo.GLTableCollectionView; 465 | PRODUCT_NAME = "$(TARGET_NAME)"; 466 | SWIFT_VERSION = 4.2; 467 | }; 468 | name = Release; 469 | }; 470 | 747CF91A1DE6EB010051A5FF /* Debug */ = { 471 | isa = XCBuildConfiguration; 472 | buildSettings = { 473 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 474 | BUNDLE_LOADER = "$(TEST_HOST)"; 475 | CODE_SIGN_IDENTITY = ""; 476 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 477 | DEVELOPMENT_TEAM = ""; 478 | INFOPLIST_FILE = GLTableCollectionViewTests/Info.plist; 479 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 480 | PRODUCT_BUNDLE_IDENTIFIER = giuliolombardo.GLTableCollectionViewTests; 481 | PRODUCT_NAME = "$(TARGET_NAME)"; 482 | SWIFT_VERSION = 4.2; 483 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GLTableCollectionView.app/GLTableCollectionView"; 484 | }; 485 | name = Debug; 486 | }; 487 | 747CF91B1DE6EB010051A5FF /* Release */ = { 488 | isa = XCBuildConfiguration; 489 | buildSettings = { 490 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 491 | BUNDLE_LOADER = "$(TEST_HOST)"; 492 | CODE_SIGN_IDENTITY = ""; 493 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 494 | DEVELOPMENT_TEAM = ""; 495 | INFOPLIST_FILE = GLTableCollectionViewTests/Info.plist; 496 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 497 | PRODUCT_BUNDLE_IDENTIFIER = giuliolombardo.GLTableCollectionViewTests; 498 | PRODUCT_NAME = "$(TARGET_NAME)"; 499 | SWIFT_VERSION = 4.2; 500 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GLTableCollectionView.app/GLTableCollectionView"; 501 | }; 502 | name = Release; 503 | }; 504 | /* End XCBuildConfiguration section */ 505 | 506 | /* Begin XCConfigurationList section */ 507 | 747CF8F41DE6EB010051A5FF /* Build configuration list for PBXProject "GLTableCollectionView" */ = { 508 | isa = XCConfigurationList; 509 | buildConfigurations = ( 510 | 747CF9141DE6EB010051A5FF /* Debug */, 511 | 747CF9151DE6EB010051A5FF /* Release */, 512 | ); 513 | defaultConfigurationIsVisible = 0; 514 | defaultConfigurationName = Release; 515 | }; 516 | 747CF9161DE6EB010051A5FF /* Build configuration list for PBXNativeTarget "GLTableCollectionView" */ = { 517 | isa = XCConfigurationList; 518 | buildConfigurations = ( 519 | 747CF9171DE6EB010051A5FF /* Debug */, 520 | 747CF9181DE6EB010051A5FF /* Release */, 521 | ); 522 | defaultConfigurationIsVisible = 0; 523 | defaultConfigurationName = Release; 524 | }; 525 | 747CF9191DE6EB010051A5FF /* Build configuration list for PBXNativeTarget "GLTableCollectionViewTests" */ = { 526 | isa = XCConfigurationList; 527 | buildConfigurations = ( 528 | 747CF91A1DE6EB010051A5FF /* Debug */, 529 | 747CF91B1DE6EB010051A5FF /* Release */, 530 | ); 531 | defaultConfigurationIsVisible = 0; 532 | defaultConfigurationName = Release; 533 | }; 534 | /* End XCConfigurationList section */ 535 | }; 536 | rootObject = 747CF8F11DE6EB010051A5FF /* Project object */; 537 | } 538 | -------------------------------------------------------------------------------- /GLTableCollectionView.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /GLTableCollectionView.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /GLTableCollectionView.xcodeproj/xcshareddata/xcschemes/GLTableCollectionView.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 65 | 66 | 67 | 68 | 78 | 80 | 86 | 87 | 88 | 89 | 90 | 91 | 97 | 99 | 105 | 106 | 107 | 108 | 110 | 111 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /GLTableCollectionView.xcodeproj/xcuserdata/giuliolombardo.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | GLTableCollectionView.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 747CF8F81DE6EB010051A5FF 16 | 17 | primary 18 | 19 | 20 | 747CF90C1DE6EB010051A5FF 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /GLTableCollectionView/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // GLTableCollectionView 4 | // 5 | // Created by Giulio Lombardo on 24/11/16. 6 | // 7 | // MIT License 8 | // 9 | // Copyright (c) 2018 Giulio Lombardo 10 | // 11 | // Permission is hereby granted, free of charge, to any person obtaining a copy 12 | // of this software and associated documentation files (the "Software"), to 13 | // deal in the Software without restriction, including without limitation the 14 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 15 | // sell copies of the Software, and to permit persons to whom the Software is 16 | // furnished to do so, subject to the following conditions: 17 | // 18 | // The above copyright notice and this permission notice shall be included in 19 | // all copies or substantial portions of the Software. 20 | // 21 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 27 | // IN THE SOFTWARE. 28 | // 29 | 30 | import UIKit 31 | 32 | @UIApplicationMain 33 | final class AppDelegate: UIResponder, UIApplicationDelegate { 34 | var window: UIWindow? 35 | 36 | func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 37 | // Override point for customization after application launch. 38 | return true 39 | } 40 | 41 | func applicationWillResignActive(_: UIApplication) { 42 | // Sent when the application is about to move from active to inactive 43 | // state. This can occur for certain types of temporary interruptions 44 | // (such as an incoming phone call or SMS message) or when the user 45 | // quits the application and it begins the transition to the background 46 | // state. Use this method to pause ongoing tasks, disable timers, and 47 | // invalidate graphics rendering callbacks. Games should use this method 48 | // to pause the game. 49 | } 50 | 51 | func applicationDidEnterBackground(_: UIApplication) { 52 | // Use this method to release shared resources, save user data, 53 | // invalidate timers, and store enough application state information to 54 | // restore your application to its current state in case it is 55 | // terminated later. If your application supports background execution, 56 | // this method is called instead of applicationWillTerminate: when the 57 | // user quits. 58 | } 59 | 60 | func applicationWillEnterForeground(_: UIApplication) { 61 | // Called as part of the transition from the background to the active 62 | // state; here you can undo many of the changes made on entering the 63 | // background. 64 | } 65 | 66 | func applicationDidBecomeActive(_: UIApplication) { 67 | // Restart any tasks that were paused (or not yet started) while the 68 | // application was inactive. If the application was previously in the 69 | // background, optionally refresh the user interface. 70 | } 71 | 72 | func applicationWillTerminate(_: UIApplication) { 73 | // Called when the application is about to terminate. Save data if 74 | // appropriate. See also applicationDidEnterBackground:. 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "display-gamut" : "sRGB", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "display-gamut" : "display-P3", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "20x20", 17 | "idiom" : "iphone", 18 | "display-gamut" : "sRGB", 19 | "scale" : "3x" 20 | }, 21 | { 22 | "size" : "20x20", 23 | "idiom" : "iphone", 24 | "display-gamut" : "display-P3", 25 | "scale" : "3x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-Small@2x.png", 31 | "display-gamut" : "sRGB", 32 | "scale" : "2x" 33 | }, 34 | { 35 | "size" : "29x29", 36 | "idiom" : "iphone", 37 | "filename" : "Icon-Small@2x-2.png", 38 | "display-gamut" : "display-P3", 39 | "scale" : "2x" 40 | }, 41 | { 42 | "size" : "29x29", 43 | "idiom" : "iphone", 44 | "filename" : "Icon-Small@3x.png", 45 | "display-gamut" : "sRGB", 46 | "scale" : "3x" 47 | }, 48 | { 49 | "size" : "29x29", 50 | "idiom" : "iphone", 51 | "filename" : "Icon-Small@3x-1.png", 52 | "display-gamut" : "display-P3", 53 | "scale" : "3x" 54 | }, 55 | { 56 | "size" : "40x40", 57 | "idiom" : "iphone", 58 | "filename" : "Icon-Spotlight-40@2x-1.png", 59 | "display-gamut" : "sRGB", 60 | "scale" : "2x" 61 | }, 62 | { 63 | "size" : "40x40", 64 | "idiom" : "iphone", 65 | "filename" : "Icon-Spotlight-40@2x-2.png", 66 | "display-gamut" : "display-P3", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "40x40", 71 | "idiom" : "iphone", 72 | "filename" : "Icon-Spotlight-40@3x.png", 73 | "display-gamut" : "sRGB", 74 | "scale" : "3x" 75 | }, 76 | { 77 | "size" : "40x40", 78 | "idiom" : "iphone", 79 | "filename" : "Icon-Spotlight-40@3x-1.png", 80 | "display-gamut" : "display-P3", 81 | "scale" : "3x" 82 | }, 83 | { 84 | "size" : "60x60", 85 | "idiom" : "iphone", 86 | "filename" : "Icon-60@2x.png", 87 | "display-gamut" : "sRGB", 88 | "scale" : "2x" 89 | }, 90 | { 91 | "size" : "60x60", 92 | "idiom" : "iphone", 93 | "filename" : "Icon-60@2x-1.png", 94 | "display-gamut" : "display-P3", 95 | "scale" : "2x" 96 | }, 97 | { 98 | "size" : "60x60", 99 | "idiom" : "iphone", 100 | "filename" : "Icon-60@3x.png", 101 | "display-gamut" : "sRGB", 102 | "scale" : "3x" 103 | }, 104 | { 105 | "size" : "60x60", 106 | "idiom" : "iphone", 107 | "filename" : "Icon-60@3x-1.png", 108 | "display-gamut" : "display-P3", 109 | "scale" : "3x" 110 | }, 111 | { 112 | "size" : "20x20", 113 | "idiom" : "ipad", 114 | "display-gamut" : "sRGB", 115 | "scale" : "1x" 116 | }, 117 | { 118 | "size" : "20x20", 119 | "idiom" : "ipad", 120 | "display-gamut" : "display-P3", 121 | "scale" : "1x" 122 | }, 123 | { 124 | "size" : "20x20", 125 | "idiom" : "ipad", 126 | "display-gamut" : "sRGB", 127 | "scale" : "2x" 128 | }, 129 | { 130 | "size" : "20x20", 131 | "idiom" : "ipad", 132 | "display-gamut" : "display-P3", 133 | "scale" : "2x" 134 | }, 135 | { 136 | "size" : "29x29", 137 | "idiom" : "ipad", 138 | "filename" : "Icon-Small.png", 139 | "display-gamut" : "sRGB", 140 | "scale" : "1x" 141 | }, 142 | { 143 | "size" : "29x29", 144 | "idiom" : "ipad", 145 | "filename" : "Icon-Small-1.png", 146 | "display-gamut" : "display-P3", 147 | "scale" : "1x" 148 | }, 149 | { 150 | "size" : "29x29", 151 | "idiom" : "ipad", 152 | "filename" : "Icon-Small@2x-1.png", 153 | "display-gamut" : "sRGB", 154 | "scale" : "2x" 155 | }, 156 | { 157 | "size" : "29x29", 158 | "idiom" : "ipad", 159 | "filename" : "Icon-Small@2x-3.png", 160 | "display-gamut" : "display-P3", 161 | "scale" : "2x" 162 | }, 163 | { 164 | "size" : "40x40", 165 | "idiom" : "ipad", 166 | "filename" : "Icon-Spotlight-40.png", 167 | "display-gamut" : "sRGB", 168 | "scale" : "1x" 169 | }, 170 | { 171 | "size" : "40x40", 172 | "idiom" : "ipad", 173 | "filename" : "Icon-Spotlight-41.png", 174 | "display-gamut" : "display-P3", 175 | "scale" : "1x" 176 | }, 177 | { 178 | "size" : "40x40", 179 | "idiom" : "ipad", 180 | "filename" : "Icon-Spotlight-40@2x.png", 181 | "display-gamut" : "sRGB", 182 | "scale" : "2x" 183 | }, 184 | { 185 | "size" : "40x40", 186 | "idiom" : "ipad", 187 | "filename" : "Icon-Spotlight-40@2x-3.png", 188 | "display-gamut" : "display-P3", 189 | "scale" : "2x" 190 | }, 191 | { 192 | "size" : "76x76", 193 | "idiom" : "ipad", 194 | "filename" : "Icon-76.png", 195 | "display-gamut" : "sRGB", 196 | "scale" : "1x" 197 | }, 198 | { 199 | "size" : "76x76", 200 | "idiom" : "ipad", 201 | "filename" : "Icon-77.png", 202 | "display-gamut" : "display-P3", 203 | "scale" : "1x" 204 | }, 205 | { 206 | "size" : "76x76", 207 | "idiom" : "ipad", 208 | "filename" : "Icon-76@2x.png", 209 | "display-gamut" : "sRGB", 210 | "scale" : "2x" 211 | }, 212 | { 213 | "size" : "76x76", 214 | "idiom" : "ipad", 215 | "filename" : "Icon-76@2x-1.png", 216 | "display-gamut" : "display-P3", 217 | "scale" : "2x" 218 | }, 219 | { 220 | "size" : "83.5x83.5", 221 | "idiom" : "ipad", 222 | "filename" : "Icon-iPadPro@2x.png", 223 | "display-gamut" : "sRGB", 224 | "scale" : "2x" 225 | }, 226 | { 227 | "size" : "83.5x83.5", 228 | "idiom" : "ipad", 229 | "filename" : "Icon-iPadPro@2x-1.png", 230 | "display-gamut" : "display-P3", 231 | "scale" : "2x" 232 | }, 233 | { 234 | "size" : "1024x1024", 235 | "idiom" : "ios-marketing", 236 | "filename" : "Icon-AppStore.png", 237 | "scale" : "1x" 238 | } 239 | ], 240 | "info" : { 241 | "version" : 1, 242 | "author" : "xcode" 243 | } 244 | } -------------------------------------------------------------------------------- /GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-60@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giulio92/GLTableCollectionView/e187baab7d7dc116bcafda476592667b519e8a57/GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-60@2x-1.png -------------------------------------------------------------------------------- /GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giulio92/GLTableCollectionView/e187baab7d7dc116bcafda476592667b519e8a57/GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png -------------------------------------------------------------------------------- /GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-60@3x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giulio92/GLTableCollectionView/e187baab7d7dc116bcafda476592667b519e8a57/GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-60@3x-1.png -------------------------------------------------------------------------------- /GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giulio92/GLTableCollectionView/e187baab7d7dc116bcafda476592667b519e8a57/GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png -------------------------------------------------------------------------------- /GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giulio92/GLTableCollectionView/e187baab7d7dc116bcafda476592667b519e8a57/GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-76.png -------------------------------------------------------------------------------- /GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-76@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giulio92/GLTableCollectionView/e187baab7d7dc116bcafda476592667b519e8a57/GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-76@2x-1.png -------------------------------------------------------------------------------- /GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giulio92/GLTableCollectionView/e187baab7d7dc116bcafda476592667b519e8a57/GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png -------------------------------------------------------------------------------- /GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-77.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giulio92/GLTableCollectionView/e187baab7d7dc116bcafda476592667b519e8a57/GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-77.png -------------------------------------------------------------------------------- /GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-AppStore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giulio92/GLTableCollectionView/e187baab7d7dc116bcafda476592667b519e8a57/GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-AppStore.png -------------------------------------------------------------------------------- /GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-Small-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giulio92/GLTableCollectionView/e187baab7d7dc116bcafda476592667b519e8a57/GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-Small-1.png -------------------------------------------------------------------------------- /GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-Small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giulio92/GLTableCollectionView/e187baab7d7dc116bcafda476592667b519e8a57/GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-Small.png -------------------------------------------------------------------------------- /GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giulio92/GLTableCollectionView/e187baab7d7dc116bcafda476592667b519e8a57/GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x-1.png -------------------------------------------------------------------------------- /GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giulio92/GLTableCollectionView/e187baab7d7dc116bcafda476592667b519e8a57/GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x-2.png -------------------------------------------------------------------------------- /GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giulio92/GLTableCollectionView/e187baab7d7dc116bcafda476592667b519e8a57/GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x-3.png -------------------------------------------------------------------------------- /GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giulio92/GLTableCollectionView/e187baab7d7dc116bcafda476592667b519e8a57/GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png -------------------------------------------------------------------------------- /GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giulio92/GLTableCollectionView/e187baab7d7dc116bcafda476592667b519e8a57/GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x-1.png -------------------------------------------------------------------------------- /GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giulio92/GLTableCollectionView/e187baab7d7dc116bcafda476592667b519e8a57/GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png -------------------------------------------------------------------------------- /GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giulio92/GLTableCollectionView/e187baab7d7dc116bcafda476592667b519e8a57/GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-40.png -------------------------------------------------------------------------------- /GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-40@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giulio92/GLTableCollectionView/e187baab7d7dc116bcafda476592667b519e8a57/GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-40@2x-1.png -------------------------------------------------------------------------------- /GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-40@2x-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giulio92/GLTableCollectionView/e187baab7d7dc116bcafda476592667b519e8a57/GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-40@2x-2.png -------------------------------------------------------------------------------- /GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-40@2x-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giulio92/GLTableCollectionView/e187baab7d7dc116bcafda476592667b519e8a57/GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-40@2x-3.png -------------------------------------------------------------------------------- /GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giulio92/GLTableCollectionView/e187baab7d7dc116bcafda476592667b519e8a57/GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-40@2x.png -------------------------------------------------------------------------------- /GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-40@3x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giulio92/GLTableCollectionView/e187baab7d7dc116bcafda476592667b519e8a57/GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-40@3x-1.png -------------------------------------------------------------------------------- /GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giulio92/GLTableCollectionView/e187baab7d7dc116bcafda476592667b519e8a57/GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-40@3x.png -------------------------------------------------------------------------------- /GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-41.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giulio92/GLTableCollectionView/e187baab7d7dc116bcafda476592667b519e8a57/GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-41.png -------------------------------------------------------------------------------- /GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-iPadPro@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giulio92/GLTableCollectionView/e187baab7d7dc116bcafda476592667b519e8a57/GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-iPadPro@2x-1.png -------------------------------------------------------------------------------- /GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-iPadPro@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giulio92/GLTableCollectionView/e187baab7d7dc116bcafda476592667b519e8a57/GLTableCollectionView/Assets.xcassets/AppIcon.appiconset/Icon-iPadPro@2x.png -------------------------------------------------------------------------------- /GLTableCollectionView/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 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /GLTableCollectionView/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /GLTableCollectionView/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.81 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 | UIInterfaceOrientationPortraitUpsideDown 35 | UIInterfaceOrientationLandscapeLeft 36 | UIInterfaceOrientationLandscapeRight 37 | 38 | UISupportedInterfaceOrientations~ipad 39 | 40 | UIInterfaceOrientationPortrait 41 | UIInterfaceOrientationPortraitUpsideDown 42 | UIInterfaceOrientationLandscapeLeft 43 | UIInterfaceOrientationLandscapeRight 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /GLTableCollectionViewTests/GLTableCollectionViewTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GLTableCollectionViewTests.swift 3 | // GLTableCollectionViewTests 4 | // 5 | // Created by Giulio Lombardo on 24/11/16. 6 | // 7 | // MIT License 8 | // 9 | // Copyright (c) 2018 Giulio Lombardo 10 | // 11 | // Permission is hereby granted, free of charge, to any person obtaining a copy 12 | // of this software and associated documentation files (the "Software"), to 13 | // deal in the Software without restriction, including without limitation the 14 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 15 | // sell copies of the Software, and to permit persons to whom the Software is 16 | // furnished to do so, subject to the following conditions: 17 | // 18 | // The above copyright notice and this permission notice shall be included in 19 | // all copies or substantial portions of the Software. 20 | // 21 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 27 | // IN THE SOFTWARE. 28 | // 29 | 30 | @testable import GLTableCollectionView 31 | import XCTest 32 | 33 | final class GLTableCollectionViewTests: XCTestCase { 34 | private var tableCollectionView: GLTableCollectionViewController! 35 | private var visibleCells: [UITableViewCell]! 36 | 37 | override func setUp() { 38 | super.setUp() 39 | // Put setup code here. This method is called before the invocation of 40 | // each test method in the class. 41 | 42 | let storyboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil) 43 | tableCollectionView = storyboard.instantiateViewController(withIdentifier: "tableCollectionView") as! GLTableCollectionViewController 44 | tableCollectionView.beginAppearanceTransition(true, animated: false) 45 | 46 | visibleCells = tableCollectionView.tableView.visibleCells 47 | } 48 | 49 | // MARK: 50 | 51 | func testInstanceVariables() { 52 | XCTAssertGreaterThan(tableCollectionView.numberOfSections, 0, 53 | "numberOfSections was: \(tableCollectionView.numberOfSections)\nUITableView must have at least one section") 54 | 55 | XCTAssertGreaterThan(tableCollectionView.numberOfCollectionsForRow, 0, 56 | "numberOfCollectionsForRow was: \(tableCollectionView.numberOfCollectionsForRow)\nThere must be at least a GLIndexedCollectionView per UITableViewCell") 57 | 58 | XCTAssertGreaterThan(tableCollectionView.numberOfCollectionItems, 0, 59 | "numberOfCollectionItems was: \(tableCollectionView.numberOfCollectionItems)\nThere must be at least one GLIndexedCollectionViewCell") 60 | 61 | XCTAssertNotEqual(GLTableCollectionViewController.tableCellID, "", 62 | "tableCellID was: \(GLTableCollectionViewController.tableCellID)\nUITableViewCell's cellIdentifier must not be empty") 63 | 64 | XCTAssertTrue(GLTableCollectionViewController.tableCellID.hasSuffix("_section_#"), 65 | "tableCellID was: \(GLTableCollectionViewController.tableCellID)\nUITableViewCell's cellIdentifier must end with section number suffix") 66 | 67 | XCTAssertTrue((GLTableCollectionViewController.tableCellID.components(separatedBy: "#").count - 1) == 1, 68 | "tableCellID was: \(GLTableCollectionViewController.tableCellID)\nUITableViewCell's cellIdentifier must contain only one # in it") 69 | } 70 | 71 | func testRandomColorsGeneration() { 72 | let colorsDictionary: [Int: [UIColor]] = tableCollectionView.colorsDict 73 | 74 | XCTAssertNotNil(colorsDictionary, "Colors dictionary is nil") 75 | XCTAssertNotEqual(colorsDictionary.count, 0, "Colors dictionary is empty") 76 | XCTAssertEqual(colorsDictionary.count, tableCollectionView.numberOfSections, 77 | "The number of keys in the colors dictionary must match the number of UITableView sections") 78 | 79 | (0 ... tableCollectionView.numberOfSections - 1).forEach { colorSection in 80 | XCTAssertEqual(colorsDictionary[colorSection]!.count, tableCollectionView.numberOfCollectionItems, 81 | "The number of colors for section must match the number of UICollectionCells") 82 | } 83 | } 84 | 85 | func testTableViewDataSource() { 86 | XCTAssertNotNil(tableCollectionView.tableView.dataSource, "UITableView dataSource is nil") 87 | } 88 | 89 | func testTableViewDelegate() { 90 | XCTAssertNotNil(tableCollectionView.tableView.delegate, "UITableView delegate is nil") 91 | } 92 | 93 | func testUITableViewCellClasses() { 94 | XCTAssertGreaterThan(visibleCells.count, 0, "UITableView visible cells must be greater than 0") 95 | 96 | visibleCells.forEach { cell in 97 | if cell is GLCollectionTableViewCell { 98 | return 99 | } else { 100 | XCTAssertTrue(cell is GLCollectionTableViewCell, "UITableViewCells must be GLCollectionTableViewCell") 101 | } 102 | } 103 | } 104 | 105 | func testTableViewCellIdentifiers() { 106 | XCTAssertGreaterThan(visibleCells.count, 0, "UITableView visible cells must be greater than 0") 107 | 108 | visibleCells.forEach { cell in 109 | guard let tableViewCell: GLCollectionTableViewCell = cell as? GLCollectionTableViewCell else { 110 | fatalError("UITableViewCells must be GLCollectionTableViewCell") 111 | } 112 | 113 | guard let reuseID: String = tableViewCell.reuseIdentifier else { 114 | fatalError("UITableViewCells must have a reuse identifier") 115 | } 116 | 117 | XCTAssertTrue(Int(reuseID.components(separatedBy: "#").last!)! >= 0, 118 | "GLCollectionTableViewCell cellIdentifier was: \(String(describing: reuseID))\nGLCollectionTableViewCell's cellIdentifier must end with a positive integer") 119 | } 120 | } 121 | 122 | // MARK: 123 | 124 | func testCollectionViewsDelegateAndDataSource() { 125 | XCTAssertGreaterThan(visibleCells.count, 0, "UITableView visible cells must be greater than 0") 126 | 127 | visibleCells.forEach { cell in 128 | guard let collectionTableCell: GLCollectionTableViewCell = cell as? GLCollectionTableViewCell else { 129 | fatalError("UITableViewCells must be GLCollectionTableViewCell") 130 | } 131 | 132 | XCTAssertNotNil(collectionTableCell.collectionView.dataSource, "GLCollectionTableViewCell dataSource is nil") 133 | XCTAssertNotNil(collectionTableCell.collectionView.delegate, "GLCollectionTableViewCell delegate is nil") 134 | } 135 | } 136 | 137 | func testCollectionNativePaginationFlag() { 138 | XCTAssertGreaterThan(visibleCells.count, 0, "UITableView visible cells must be greater than 0") 139 | 140 | visibleCells.forEach { cell in 141 | guard let collectionTableCell: GLCollectionTableViewCell = cell as? GLCollectionTableViewCell else { 142 | fatalError("UITableViewCells must be GLCollectionTableViewCell") 143 | } 144 | 145 | if collectionTableCell.collectionViewPaginatedScroll == true { 146 | XCTAssertFalse(collectionTableCell.collectionView.isPagingEnabled, 147 | "Custom scrolling pagination and native UICollectionView pagination can't be enabled at the same time") 148 | } 149 | } 150 | } 151 | 152 | func testCollectionViewPaginationConsistency() { 153 | XCTAssertGreaterThan(visibleCells.count, 0, "UITableView visible cells must be greater than 0") 154 | 155 | visibleCells.forEach { cell in 156 | guard let collectionTableCell: GLCollectionTableViewCell = cell as? GLCollectionTableViewCell else { 157 | fatalError("UITableViewCells must be GLCollectionTableViewCell") 158 | } 159 | 160 | if collectionTableCell.collectionViewPaginatedScroll == true { 161 | XCTAssertTrue(collectionTableCell.collectionView.isScrollEnabled, 162 | "If custom paginated scroll is enabled the UICollectionView should be scrollable") 163 | 164 | XCTAssertTrue(collectionTableCell.collectionView.isUserInteractionEnabled, 165 | "If custom paginated scroll is enabled the UICollectionView should be user interactive") 166 | } 167 | } 168 | } 169 | 170 | func testCollectionViewCellHeights() { 171 | XCTAssertGreaterThan(visibleCells.count, 0, "UITableView visible cells must be greater than 0") 172 | 173 | let tableViewCellHeight: CGFloat = tableCollectionView.tableView.rowHeight 174 | 175 | XCTAssertLessThanOrEqual(tableViewCellHeight + (tableCollectionView.collectionTopInset + tableCollectionView.collectionBottomInset), tableViewCellHeight, 176 | "UITableView rowHeight was: \(tableViewCellHeight)\nUICollectionViewCells height should be lower than rowHeight + (collectionTopInset + collectionBottomInset)") 177 | } 178 | 179 | // MARK: 180 | 181 | func testOpaqueFlag() { 182 | XCTAssertTrue(tableCollectionView.tableView.isOpaque, "The UITableView should be opaque for increased performances") 183 | 184 | XCTAssertGreaterThan(visibleCells.count, 0, "UITableView visible cells must be greater than 0") 185 | 186 | visibleCells.forEach { cell in 187 | guard let collectionTableCell: GLCollectionTableViewCell = cell as? GLCollectionTableViewCell else { 188 | fatalError("UITableViewCells must be GLCollectionTableViewCell") 189 | } 190 | 191 | XCTAssertTrue(collectionTableCell.collectionView.isOpaque, "The UICollectionView should be opaque for increased performances") 192 | } 193 | } 194 | 195 | override func tearDown() { 196 | // Put teardown code here. This method is called after the invocation of 197 | // each test method in the class. 198 | super.tearDown() 199 | 200 | tableCollectionView.endAppearanceTransition() 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /GLTableCollectionViewTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /GitHub Page/Images/Source/Diagram.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giulio92/GLTableCollectionView/e187baab7d7dc116bcafda476592667b519e8a57/GitHub Page/Images/Source/Diagram.graffle -------------------------------------------------------------------------------- /GitHub Page/Images/Source/Icon.pxm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giulio92/GLTableCollectionView/e187baab7d7dc116bcafda476592667b519e8a57/GitHub Page/Images/Source/Icon.pxm -------------------------------------------------------------------------------- /GitHub Page/Images/Source/Logo.pxm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giulio92/GLTableCollectionView/e187baab7d7dc116bcafda476592667b519e8a57/GitHub Page/Images/Source/Logo.pxm -------------------------------------------------------------------------------- /GitHub Page/Images/demonstration.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giulio92/GLTableCollectionView/e187baab7d7dc116bcafda476592667b519e8a57/GitHub Page/Images/demonstration.gif -------------------------------------------------------------------------------- /GitHub Page/Images/diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giulio92/GLTableCollectionView/e187baab7d7dc116bcafda476592667b519e8a57/GitHub Page/Images/diagram.png -------------------------------------------------------------------------------- /GitHub Page/Images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giulio92/GLTableCollectionView/e187baab7d7dc116bcafda476592667b519e8a57/GitHub Page/Images/logo.png -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Description: 4 | 6 | 7 | ### Expected Behavior: 8 | 9 | 10 | ### Actual Behavior: 11 | 12 | 13 | ### Steps to Reproduce: 14 | 15 | 16 | 1. 17 | 2. 18 | 3. 19 | 4. 20 | 21 | ### Your Environment: 22 | 23 | * GLTableCollectionView version: 24 | * Device: 25 | * iOS version: 26 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Giulio Lombardo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GLTableCollectionView 2 | 3 |

4 | 5 |

6 | 7 | |**Branch**|**Status**| 8 | |:--------:|:--------:| 9 | |`master`|[![BuddyBuild](https://dashboard.buddybuild.com/api/statusImage?appID=592889ed482e8d00016f99eb&branch=master&build=latest)](https://dashboard.buddybuild.com/apps/592889ed482e8d00016f99eb/build/latest?branch=master)| 10 | |`develop`|[![BuddyBuild](https://dashboard.buddybuild.com/api/statusImage?appID=592889ed482e8d00016f99eb&branch=develop&build=latest)](https://dashboard.buddybuild.com/apps/592889ed482e8d00016f99eb/build/latest?branch=develop)| 11 | 12 | ![Language](https://img.shields.io/badge/language-Swift%204.2-orange.svg) 13 | ![Supported platforms](https://img.shields.io/badge/platform-iOS-lightgrey.svg) 14 | [![codebeat badge](https://codebeat.co/badges/5a29ccd4-fda0-45d1-ae57-e7158e01449a)](https://codebeat.co/projects/github-com-giulio92-gltablecollectionview) 15 | [![license](https://img.shields.io/github/license/giulio92/GLTableCollectionView.svg)](https://github.com/giulio92/GLTableCollectionView/blob/master/LICENSE.txt) 16 | 17 | ## What it is 18 | GLTableCollectionView is a ready to use `UITableViewController` with a `UICollectionView` for each `UITableViewCell`, something like Netflix, Airbnb or the Apple's App Store are doing in their iOS apps. GLTableCollectionView is completely customizable in both its `UITableView` and `UICollectionView` parts since it has been made on the same Data Source and Delegate methods with no complicated additions. 19 | 20 | | | GLTableCollectionView | 21 | |----------|-------------------------------| 22 | 🔄|The **same** `UITableView` reusable cells logic provided from Apple's implementation 23 | ♻️|`UICollectionView` cell recycle 24 | 🆒|Both `UITableView` & `UICollectionView` can have their own sections and/or headers 25 | 🎨|Customization of `UICollectionViewCell`s using the same `UICollectionViewDelegate Flow Layout` you already know 26 | ✨|Previous `UICollectionView` **.contentOffset** value restoration after scroll 27 | ↔️|`UICollectionView` cell-size-based scroll pagination, see below for instructions 28 | 📐|Storyboard and Auto Layout compatibility 29 | 💎|Clean architecture 30 | 🔧|Unit Tests 31 | 32 | ## Enable/disable scroll pagination 33 | Set `paginationEnabled` variable `true` in GLTableCollectionViewController class, `false` to disable. Default value is `true`. 34 | ``` 35 | /// Set true to enable UICollectionViews scroll pagination 36 | var paginationEnabled: Bool = true 37 | ``` 38 | 39 | ## Demo 40 |

41 | 42 |

43 | 44 | ## How it works 45 |

46 | 47 |

48 | 49 | ## Requirements 50 | - Xcode 10.0+ 51 | - Swift 4.2+ 52 | - iOS 9.0+ 53 | - [SwiftLint](https://github.com/realm/SwiftLint) (Optional, but _highly_ suggested) 54 | 55 | ## Donations 56 | - [PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=giulio%2elombardo%40gmail%2ecom&lc=IT¤cy_code=EUR&bn=PP%2dDonationsBF%3abtn_donate_LG%2egif%3aNonHosted) 57 | 58 | - BTC: `3Mc25tFtxxwD9mXqtxFn5Qvkbndg3NhvXi` 59 | 60 | - LTC: `MUoZzdDqD2BkWsVpcSv1pQVHhCcUuiADCL` 61 | --------------------------------------------------------------------------------