├── .swift-version ├── AppStoreCollectionViewLayout-Demo ├── AppStoreCollectionViewLayout-Demo-Bridging-Header.h ├── AppDelegate.swift ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Info.plist ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── CustomItemSizeDemoCollectionViewController.swift ├── Apps.plist └── AppStoreCollectionViewLayoutDemoViewController.swift ├── CollectionViewShelfLayout.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcshareddata │ └── xcschemes │ │ └── AppStoreCollectionViewLayout.xcscheme └── project.pbxproj ├── CollectionViewShelfLayout ├── CollectionViewShelfLayout.h ├── Info.plist └── CollectionViewShelfLayout.swift ├── LICENSE ├── CollectionViewShelfLayout.podspec ├── .gitignore └── README.md /.swift-version: -------------------------------------------------------------------------------- 1 | 5.0 2 | -------------------------------------------------------------------------------- /AppStoreCollectionViewLayout-Demo/AppStoreCollectionViewLayout-Demo-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | -------------------------------------------------------------------------------- /CollectionViewShelfLayout.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CollectionViewShelfLayout.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /AppStoreCollectionViewLayout-Demo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // AppStoreCollectionViewLayout-Demo 4 | // 5 | // Created by Pitiphong Phongpattranont on 7/26/2016. 6 | // Copyright © 2016 Pitiphong Phongpattranont. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | } 23 | 24 | -------------------------------------------------------------------------------- /CollectionViewShelfLayout/CollectionViewShelfLayout.h: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewShelfLayout.h 3 | // CollectionViewShelfLayout 4 | // 5 | // Created by Pitiphong Phongpattranont on 7/26/2016. 6 | // Copyright © 2016 Pitiphong Phongpattranont. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for CollectionViewShelfLayout. 12 | FOUNDATION_EXPORT double CollectionViewShelfLayoutVersionNumber; 13 | 14 | //! Project version string for CollectionViewShelfLayout. 15 | FOUNDATION_EXPORT const unsigned char CollectionViewShelfLayoutVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /AppStoreCollectionViewLayout-Demo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /CollectionViewShelfLayout/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 | FMWK 17 | CFBundleShortVersionString 18 | 0.6.6 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Pitiphong Phongpattranont 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 | -------------------------------------------------------------------------------- /AppStoreCollectionViewLayout-Demo/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 | 0.6.4 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /CollectionViewShelfLayout.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | 3 | s.name = "CollectionViewShelfLayout" 4 | s.version = "0.6.6" 5 | s.summary = "A UICollectionViewLayout subclass displays as rows of items similar to App Store Feature tab without a nested UITable/CollectionView hack." 6 | 7 | s.description = "A UICollectionViewLayout subclass displays its items as rows of items similar to the App Store Feature tab without a nested UITableView/UICollectionView hack. You can use a single data source for all of your contents. Each section displays its items in a row. CollectionViewShelfLayout supports collection view's header view and footer view similar to table view's tableHeaderView and tableFooterView also sections' header and footer views too." 8 | 9 | s.homepage = "https://github.com/pitiphong-p/CollectionViewShelfLayout" 10 | s.screenshots = "https://cocoacontrols-production.s3.amazonaws.com/uploads/control_image/image/9666/CollectionViewShelfLayout_small.png" 11 | 12 | s.license = "MIT" 13 | 14 | s.author = "Pitiphong Phongpattranont" 15 | s.social_media_url = "http://twitter.com/pitiphong_p" 16 | 17 | s.platform = :ios, "9.0" 18 | s.source = { :git => "https://github.com/pitiphong-p/CollectionViewShelfLayout.git", :tag => s.version } 19 | 20 | s.source_files = ["CollectionViewShelfLayout/*.swift"] 21 | 22 | s.xcconfig = { 'SWIFT_VERSION' => '5.0' } 23 | 24 | end 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | *.dSYM.zip 28 | *.dSYM 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | # Swift Package Manager 35 | # 36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 37 | # Packages/ 38 | .build/ 39 | 40 | # CocoaPods 41 | # 42 | # We recommend against adding the Pods directory to your .gitignore. However 43 | # you should judge for yourself, the pros and cons are mentioned at: 44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 45 | # 46 | # Pods/ 47 | 48 | # Carthage 49 | # 50 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 51 | # Carthage/Checkouts 52 | 53 | Carthage/Build 54 | 55 | # fastlane 56 | # 57 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 58 | # screenshots whenever they are needed. 59 | # For more information about the recommended setup visit: 60 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 61 | 62 | fastlane/report.xml 63 | fastlane/Preview.html 64 | fastlane/screenshots 65 | fastlane/test_output 66 | -------------------------------------------------------------------------------- /AppStoreCollectionViewLayout-Demo/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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CollectionViewShelfLayout 2 | A UICollectionViewLayout subclass displays its items as rows of items similar to the App Store Feature tab without a nested UITableView/UICollectionView hack. You can use a single data source for all of your contents. Each section displays its items in a row. `CollectionViewShelfLayout` supports collection view's *header view and footer view* similar to table view's *tableHeaderView and tableFooterView* also *sections' header and footer views* too. 3 | 4 | ![CollectionViewShelfLayout screenshot](https://cocoacontrols-production.s3.amazonaws.com/uploads/control_image/image/9666/CollectionViewShelfLayout_small.png) 5 | 6 | # Requirements 7 | - iOS 9+ 8 | - Swift 4.0+ 9 | 10 | This requirement is due to usage of some Auto Layout APIs available in iOS 8 and 9 or later. 11 | If you want to use `CollectionViewShelfLayout` in iOS 8, you can replace NSLayoutAnchor usage with other APIs. 12 | 13 | # Installation 14 | ## Manaully 15 | This project comes with built in *`CollectionViewShelfLayout framework`* target. You can drag `CollectionViewShelfLayout.xcproj` file into your project, add `CollectionViewShelfLayout framework` target as a target dependency and link/embed that framework. and Voila!!! 16 | ````swift 17 | import CollectionViewShelfLayout 18 | ```` 19 | ## CocoaPods 20 | Add the following to your `Podfile` 21 | ````ruby 22 | pod 'CollectionViewShelfLayout' 23 | use_frameworks! 24 | ```` 25 | ## Carthage 26 | Add the following to your `Cartfile` 27 | ````ruby 28 | github "pitiphong-p/CollectionViewShelfLayout" 29 | ```` 30 | ## Swift 2 31 | You can use CollectionViewShelfLayout in Swift 2.2 by checking out tag `0.5.5` 32 | ## Swift 4.0 33 | You can use CollectionViewShelfLayout in Swift 4.0 by checking out tag `0.6.4` 34 | ## Swift 4.2 35 | You can use CollectionViewShelfLayout in Swift 4.0 by checking out tag `0.6.5` 36 | 37 | # Usage 38 | Set collecion view's layout to an instance of `CollectionViewShelfLayout`. Set the layout's properties you want (eg. cellSize). You can set its layout both via code or `Storyboard`. 39 | ````swift 40 | let shelfLayout = CollectionViewShelfLayout() 41 | shelfLayout.itemSize = CGSize(width: 100, height: 180) 42 | collectionView.collectionViewLayout = shelfLayout 43 | ```` 44 | 45 | # Demo App 46 | `CollectionViewShelfLayout` project comes with a demo app target. You can see `CollectionViewShelfLayout` in action by just running `AppStoreCollectionViewLayout-Demo` demo app target. 47 | # Contact 48 | Pitiphong Phongpattranont 49 | - [@pitiphong_p on Twitter] (https://twitter.com/pitiphong_p) 50 | 51 | # License 52 | `CollectionViewShelfLayout` is released under an MIT License. 53 | Copyright © 2016-present Pitiphong Phongpattranont. 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /CollectionViewShelfLayout.xcodeproj/xcshareddata/xcschemes/AppStoreCollectionViewLayout.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /AppStoreCollectionViewLayout-Demo/CustomItemSizeDemoCollectionViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomItemSizeDemoCollectionViewController.swift 3 | // AppStoreCollectionViewLayout-Demo 4 | // 5 | // Created by Pitiphong Phongpattranont on 29/10/2017. 6 | // Copyright © 2017 Pitiphong Phongpattranont. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CollectionViewShelfLayout 11 | 12 | 13 | private let reuseIdentifier = "Cell" 14 | 15 | class ImageCollectionViewCell: UICollectionViewCell { 16 | @IBOutlet var imageView: UIImageView! 17 | } 18 | 19 | class CustomItemSizeDemoCollectionViewController: UICollectionViewController, CollectionViewDelegateShelfLayout { 20 | 21 | var imageSizes = [[CGSize]]() 22 | var imageCache = [[UIImage?]]() 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | 27 | let refreshControl = UIRefreshControl() 28 | refreshControl.addTarget(self, action: #selector(refresh), for: .valueChanged) 29 | if #available(iOS 10.0, *) { 30 | collectionView?.refreshControl = refreshControl 31 | } 32 | 33 | if let layout = collectionView?.collectionViewLayout as? CollectionViewShelfLayout { 34 | layout.sectionCellInset = UIEdgeInsets(top: 4.0, left: 4.0, bottom: 4.0, right: 4.0) 35 | } 36 | 37 | refresh() 38 | } 39 | 40 | override func didReceiveMemoryWarning() { 41 | super.didReceiveMemoryWarning() 42 | // Dispose of any resources that can be recreated. 43 | } 44 | 45 | override func numberOfSections(in collectionView: UICollectionView) -> Int { 46 | return 5 47 | } 48 | 49 | override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 50 | return 8 51 | } 52 | 53 | override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 54 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! ImageCollectionViewCell 55 | 56 | if imageCache[indexPath.section][indexPath.item] == nil { 57 | let size = imageSizes[indexPath.section][indexPath.item] 58 | var urlComponents = URLComponents(string: "https://dummyimage.com/3:2x\(size.height * collectionView.traitCollection.displayScale)")! 59 | urlComponents.queryItems = [ URLQueryItem(name: "text", value: "\(Int(size.width))x\(Int(size.height))")] 60 | 61 | let request = URLRequest.init(url: urlComponents.url!) 62 | let task = URLSession.shared.dataTask(with: request, completionHandler: { [weak self] (data, response, error) in 63 | if let data = data, let image = UIImage(data: data) { 64 | self?.imageCache[indexPath.section][indexPath.item] = image 65 | DispatchQueue.main.async { 66 | if let cell = collectionView.cellForItem(at: indexPath) as? ImageCollectionViewCell { 67 | cell.imageView.image = image 68 | } 69 | } 70 | } 71 | }) 72 | task.resume() 73 | } 74 | 75 | return cell 76 | } 77 | 78 | override func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { 79 | let cell = cell as! ImageCollectionViewCell 80 | cell.imageView.image = imageCache[indexPath.section][indexPath.item] 81 | } 82 | 83 | @objc func refresh() { 84 | // Generate image size randomly with aspect ratio of 3:2 and the height is between 120 - 240 points with stepping of 20.0 points 85 | imageSizes = (0..<5).map({ _ in 86 | (0..<8).map({ _ in 87 | let height = (CGFloat(arc4random_uniform(6)) * 20.0) + 120.0 88 | let width = height * 3 / 2 89 | return CGSize(width: width, height: height) 90 | }) 91 | }) 92 | 93 | imageCache = (0..<5).map({ _ in 94 | Array(repeating: nil, count: 8) 95 | }) 96 | 97 | collectionView?.reloadData() 98 | if #available(iOS 10.0, *) { 99 | collectionView?.refreshControl?.endRefreshing() 100 | } 101 | } 102 | 103 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 104 | return imageSizes[indexPath.section][indexPath.item] 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /AppStoreCollectionViewLayout-Demo/Apps.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Messaging 6 | 7 | 8 | name 9 | WhatsApp Messenger 10 | price 11 | 0 12 | iconURL 13 | http://a716.phobos.apple.com/us/r30/Purple60/v4/05/11/7c/05117cd1-f177-1979-7151-7925d2f15756/AppIcon57x57.png 14 | id 15 | 310633997 16 | 17 | 18 | name 19 | Messenger 20 | price 21 | 0 22 | iconURL 23 | http://a482.phobos.apple.com/us/r30/Purple60/v4/da/a7/d0/daa7d07e-ec6b-551c-f4e3-4ce848b4bdf8/Icon-Production.png 24 | id 25 | 454638411 26 | 27 | 28 | Internet 29 | 30 | 31 | name 32 | Overcast: Podcast Player 33 | price 34 | 0 35 | iconURL 36 | http://a624.phobos.apple.com/us/r30/Purple18/v4/e3/7f/98/e37f9809-ff30-404f-0dc1-8de17b2c9766/AppIcon60x60_U00402x.png 37 | id 38 | 888422857 39 | 40 | 41 | name 42 | Tweetbot 4 for Twitter 43 | price 44 | 4.99 45 | iconURL 46 | http://a1384.phobos.apple.com/us/r30/Purple30/v4/89/c3/fc/89c3fcc0-aba8-a0aa-2c2d-4b98e43e2b29/AppIcon60x60_U00402x.png 47 | id 48 | 1018355599 49 | 50 | 51 | name 52 | Facebook 53 | price 54 | 0 55 | iconURL 56 | http://a1627.phobos.apple.com/us/r30/Purple18/v4/a8/ed/f3/a8edf3a6-88aa-b55c-d92e-f87928ab6bd9/Icon-Production.png 57 | id 58 | 284882215 59 | 60 | 61 | name 62 | Pocket: Save Articles and Videos to View Later 63 | price 64 | 0 65 | iconURL 66 | http://a1363.phobos.apple.com/us/r30/Purple60/v4/86/af/d6/86afd680-5638-6855-2136-bd6cb42a6c43/AppIcon60x60_U00402x.png 67 | id 68 | 309601447 69 | 70 | 71 | name 72 | WWDC 73 | price 74 | 0 75 | iconURL 76 | http://a606.phobos.apple.com/us/r30/Purple18/v4/c6/44/1f/c6441f72-ebf8-fe33-3ae4-88e02ef8b15b/AppIcon60x60_U00402x.png 77 | id 78 | 640199958 79 | 80 | 81 | Productivity 82 | 83 | 84 | name 85 | Things 86 | price 87 | 9.99 88 | iconURL 89 | http://a1206.phobos.apple.com/us/r30/Purple49/v4/b0/45/0b/b0450b64-2653-3c9d-06ec-39c3439f69d1/AppIcon-57px.png 90 | id 91 | 284971781 92 | 93 | 94 | name 95 | Scanner Pro 7 - Document and receipt PDF scanner with OCR 96 | price 97 | 3.99 98 | iconURL 99 | http://a16.phobos.apple.com/us/r30/Purple18/v4/25/51/e5/2551e58a-07df-77f9-fbf7-20944ae84116/ScannerPro60x60_U00402x.png 100 | id 101 | 333710667 102 | 103 | 104 | name 105 | Scanbot - QR & Document Scanner 106 | price 107 | 0 108 | iconURL 109 | http://a208.phobos.apple.com/us/r30/Purple20/v4/ba/d7/3c/bad73c03-7190-7ca6-c4d3-1d467965e885/AppIcon60x60_U00402x.png 110 | id 111 | 834854351 112 | 113 | 114 | name 115 | Evernote - capture notes and sync across all devices. Stay organized. 116 | price 117 | 0 118 | iconURL 119 | http://a1307.phobos.apple.com/us/r30/Purple18/v4/93/74/3f/93743fa1-c401-a766-f8ca-05d1b6797718/AppIcon60x60_U00402x.png 120 | id 121 | 281796108 122 | 123 | 124 | name 125 | Dropbox 126 | price 127 | 0 128 | iconURL 129 | http://a752.phobos.apple.com/us/r30/Purple60/v4/88/16/c4/8816c42f-5b7f-cc8c-87bc-d04951f8e499/AppIcon60x60_U00402x.png 130 | id 131 | 327630330 132 | 133 | 134 | Utility 135 | 136 | 137 | name 138 | Sleep Cycle alarm clock 139 | price 140 | 0 141 | iconURL 142 | http://a374.phobos.apple.com/us/r30/Purple60/v4/d5/4f/c9/d54fc939-a768-05d2-6bcb-8773ba75550c/AppIcon60x60_U00402x.png 143 | id 144 | 320606217 145 | 146 | 147 | name 148 | Uber 149 | price 150 | 0 151 | iconURL 152 | http://a607.phobos.apple.com/us/r30/Purple18/v4/49/5e/c0/495ec051-f384-d956-b1cb-c2b20f4590f7/AppIcon60x60_U00402x.png 153 | id 154 | 368677368 155 | 156 | 157 | name 158 | 1Password - Password Manager and Secure Wallet 159 | price 160 | 0 161 | iconURL 162 | http://a655.phobos.apple.com/us/r30/Purple60/v4/2f/d0/4b/2fd04be0-f5a7-c9ef-d1b1-33062f70c24e/AppIcon60x60_U00402x.png 163 | id 164 | 568903335 165 | 166 | 167 | name 168 | Letterpress – Word Game 169 | price 170 | 0 171 | iconURL 172 | http://a1585.phobos.apple.com/us/r30/Purple30/v4/2d/58/28/2d582876-1f2b-6590-e1b2-668967cae24b/AppIcon57x57.png 173 | id 174 | 526619424 175 | 176 | 177 | 178 | 179 | -------------------------------------------------------------------------------- /AppStoreCollectionViewLayout-Demo/AppStoreCollectionViewLayoutDemoViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppStoreCollectionViewLayoutDemoViewController.swift 3 | // AppStoreCollectionViewLayout 4 | // 5 | // Created by Pitiphong Phongpattranont on 7/26/2016. 6 | // Copyright © 2016 Pitiphong Phongpattranont. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CollectionViewShelfLayout 11 | import StoreKit 12 | 13 | private let reuseIdentifier = "Cell" 14 | 15 | 16 | protocol AppStoreCollectionSectionHeaderViewDelegate: class { 17 | func sectionHeaderViewDidTappedButton(_ view: AppStoreCollectionSectionHeaderView) 18 | } 19 | 20 | class AppStoreCollectionSectionHeaderView: UICollectionReusableView { 21 | let label: UILabel = UILabel() 22 | let button: UIButton = UIButton() 23 | var indexPath: IndexPath? 24 | 25 | override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { 26 | super.apply(layoutAttributes) 27 | 28 | self.indexPath = layoutAttributes.indexPath 29 | } 30 | 31 | weak var delegate: AppStoreCollectionSectionHeaderViewDelegate? 32 | 33 | override init(frame: CGRect) { 34 | super.init(frame: frame) 35 | 36 | isUserInteractionEnabled = true 37 | 38 | backgroundColor = UIColor.white 39 | label.translatesAutoresizingMaskIntoConstraints = false 40 | button.translatesAutoresizingMaskIntoConstraints = false 41 | addSubview(label) 42 | addSubview(button) 43 | 44 | label.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor).isActive = true 45 | label.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true 46 | 47 | button.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor).isActive = true 48 | button.lastBaselineAnchor.constraint(equalTo: label.lastBaselineAnchor).isActive = true 49 | 50 | button.leadingAnchor.constraint(greaterThanOrEqualTo: label.trailingAnchor, constant: 8.0).isActive = true 51 | 52 | button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside) 53 | } 54 | 55 | @objc func buttonTapped() { 56 | delegate?.sectionHeaderViewDidTappedButton(self) 57 | } 58 | 59 | required init?(coder aDecoder: NSCoder) { 60 | super.init(coder: aDecoder) 61 | } 62 | } 63 | 64 | class AppStoreCollectionViewLayoutDemoViewController: UICollectionViewController { 65 | 66 | enum Section: String { 67 | case Messaging 68 | case Internet 69 | case Productivity 70 | case Utility 71 | } 72 | 73 | let appData: [Section: [AppDetail]] = { 74 | let bundle = Bundle(for: AppStoreCollectionViewLayoutDemoViewController.self) 75 | let appDataPListURL = bundle.url(forResource: "Apps", withExtension: "plist")! 76 | let appDataPList = NSDictionary(contentsOf: appDataPListURL)! as! [String: [[String: AnyObject]]] 77 | 78 | var appData = [Section: [AppDetail]]() 79 | for (sectionName, apps) in appDataPList { 80 | let appDetails: [AppDetail] 81 | appDetails = apps.compactMap(AppDetail.init(plistData:)) 82 | let section = Section(rawValue: sectionName)! 83 | appData[section] = appDetails 84 | } 85 | 86 | return appData 87 | }() 88 | 89 | subscript (appDetail indexPath: IndexPath) -> AppDetail { 90 | return appData[sections[(indexPath as NSIndexPath).section]]![(indexPath as NSIndexPath).row] 91 | } 92 | 93 | var sections: [Section] { 94 | return appData.keys.sorted(by: { (firstSection, secondSection) in 95 | return firstSection.rawValue < secondSection.rawValue 96 | }) 97 | } 98 | 99 | @IBOutlet var headerView: UIView! 100 | override func viewDidLoad() { 101 | super.viewDidLoad() 102 | 103 | if let layout = collectionView?.collectionViewLayout as? CollectionViewShelfLayout { 104 | layout.sectionCellInset = UIEdgeInsets(top: 4.0, left: 4.0, bottom: 4.0, right: 4.0) 105 | 106 | headerView.translatesAutoresizingMaskIntoConstraints = false 107 | 108 | collectionView?.register(AppStoreCollectionSectionHeaderView.self, forSupplementaryViewOfKind: ShelfElementKindSectionHeader, withReuseIdentifier: "Header") 109 | } 110 | } 111 | 112 | override func didReceiveMemoryWarning() { 113 | super.didReceiveMemoryWarning() 114 | // Dispose of any resources that can be recreated. 115 | } 116 | 117 | 118 | // MARK: UICollectionViewDataSource 119 | 120 | override func numberOfSections(in collectionView: UICollectionView) -> Int { 121 | return sections.count 122 | } 123 | 124 | override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 125 | return appData[sections[section]]?.count ?? 0 126 | } 127 | 128 | let priceFormatter: NumberFormatter = { 129 | let formatter = NumberFormatter() 130 | formatter.currencyCode = "USD" 131 | formatter.numberStyle = NumberFormatter.Style.currencyAccounting 132 | return formatter 133 | }() 134 | 135 | override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 136 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! AppStoreCollectionViewCell 137 | let appDetail = self[appDetail: indexPath] 138 | cell.appNameLabel.text = appDetail.name 139 | cell.appPriceLabel.text = priceFormatter.string(from: NSNumber(value: appDetail.price)) 140 | 141 | DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async { 142 | let iconData = try? Data(contentsOf: appDetail.iconURL) 143 | 144 | if let iconData = iconData { 145 | let icon = UIImage(data: iconData) 146 | DispatchQueue.main.async(execute: { 147 | if let currentIndexPath = collectionView.indexPath(for: cell), currentIndexPath == indexPath { 148 | cell.appIconImageView.image = icon 149 | } 150 | }) 151 | } 152 | } 153 | return cell 154 | } 155 | 156 | override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { 157 | let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Header", for: indexPath) 158 | if let view = view as? AppStoreCollectionSectionHeaderView { 159 | view.label.text = sections[(indexPath as NSIndexPath).section].rawValue 160 | view.button.setTitle("See All >", for: UIControl.State()) 161 | view.button.setTitleColor(UIColor.darkGray, for: UIControl.State()) 162 | view.delegate = self 163 | } 164 | return view 165 | } 166 | 167 | override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 168 | let appData = self[appDetail: indexPath] 169 | print(appData.name) 170 | 171 | let appStoreViewerController = SKStoreProductViewController() 172 | appStoreViewerController.delegate = self 173 | appStoreViewerController.loadProduct(withParameters: [SKStoreProductParameterITunesItemIdentifier : appData.id], completionBlock: { (result, error) in 174 | print(result, error as Any) 175 | }) 176 | present(appStoreViewerController, animated: true, completion: nil) 177 | } 178 | } 179 | 180 | extension AppStoreCollectionViewLayoutDemoViewController: SKStoreProductViewControllerDelegate { 181 | func productViewControllerDidFinish(_ viewController: SKStoreProductViewController) { 182 | dismiss(animated: true, completion: nil) 183 | } 184 | } 185 | 186 | extension AppStoreCollectionViewLayoutDemoViewController: AppStoreCollectionSectionHeaderViewDelegate { 187 | func sectionHeaderViewDidTappedButton(_ view: AppStoreCollectionSectionHeaderView) { 188 | guard let indexPath = view.indexPath else { 189 | return 190 | } 191 | let section = sections[(indexPath as NSIndexPath).section] 192 | let alertController = UIAlertController(title: section.rawValue, message: "You tapped the \(section.rawValue) section.", preferredStyle: .alert) 193 | 194 | alertController.addAction( 195 | UIAlertAction(title: "OK", style: UIAlertAction.Style.cancel, handler: nil) 196 | ) 197 | 198 | present(alertController, animated: true, completion: nil) 199 | } 200 | } 201 | 202 | struct AppDetail { 203 | let id: Int 204 | let name: String 205 | let iconURL: URL 206 | let price: Double 207 | 208 | init?(plistData: [String: AnyObject]) { 209 | guard let id = plistData["id"] as? Int, let name = plistData["name"] as? String, 210 | let iconURL = (plistData["iconURL"] as? String).flatMap(URL.init(string:)), let price = plistData["price"] as? Double else { 211 | return nil 212 | } 213 | 214 | self.id = id 215 | self.name = name 216 | self.iconURL = iconURL 217 | self.price = price 218 | } 219 | } 220 | 221 | class AppStoreCollectionViewCell: UICollectionViewCell { 222 | @IBOutlet weak var appIconImageView: UIImageView! 223 | @IBOutlet weak var appNameLabel: UILabel! 224 | @IBOutlet weak var appPriceLabel: UILabel! 225 | 226 | override func awakeFromNib() { 227 | super.awakeFromNib() 228 | 229 | appIconImageView.layer.cornerRadius = 16.0 230 | appIconImageView.layer.masksToBounds = true 231 | } 232 | } 233 | 234 | -------------------------------------------------------------------------------- /AppStoreCollectionViewLayout-Demo/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 | 48 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | -------------------------------------------------------------------------------- /CollectionViewShelfLayout/CollectionViewShelfLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewShelfLayout.swift 3 | // CollectionViewShelfLayout 4 | // 5 | // Created by Pitiphong Phongpattranont on 7/26/2016. 6 | // Copyright © 2016 Pitiphong Phongpattranont. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | 12 | private let ShelfElementKindCollectionHeader = "ShelfElementKindCollectionHeader" 13 | private let ShelfElementKindCollectionFooter = "ShelfElementKindCollectionFooter" 14 | 15 | /// An element kind of *Section Header* 16 | public let ShelfElementKindSectionHeader = UICollectionView.elementKindSectionHeader 17 | /// An element kind of *Section Footer* 18 | public let ShelfElementKindSectionFooter = UICollectionView.elementKindSectionFooter 19 | 20 | 21 | public protocol CollectionViewDelegateShelfLayout: UICollectionViewDelegate { 22 | 23 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize 24 | } 25 | 26 | extension CollectionViewDelegateShelfLayout { 27 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 28 | return (collectionViewLayout as? CollectionViewShelfLayout)?.cellSize ?? CGSize.zero 29 | } 30 | } 31 | 32 | 33 | 34 | /// A collection view layout mimics the layout of the iOS App Store. 35 | open class CollectionViewShelfLayout: UICollectionViewLayout { 36 | fileprivate var headerViewLayoutAttributes: CollectionViewShelfLayoutHeaderFooterViewLayoutAttributes? 37 | fileprivate var footerViewLayoutAttributes: CollectionViewShelfLayoutHeaderFooterViewLayoutAttributes? 38 | 39 | fileprivate var sectionsFrame: [CGRect] = [] 40 | fileprivate var sectionsCellFrame: [CGRect] = [] 41 | fileprivate var cellPanningScrollViews: [TrackingScrollView] = [] 42 | 43 | fileprivate var sectionHeaderViewsLayoutAttributes: [UICollectionViewLayoutAttributes] = [] 44 | fileprivate var sectionFooterViewsLayoutAttributes: [UICollectionViewLayoutAttributes] = [] 45 | fileprivate var cellsLayoutAttributes: [[UICollectionViewLayoutAttributes]] = [] 46 | 47 | /// A height of each section header. Set this value to 0.0 if you don't want section header views. Default is *0.0* 48 | @IBInspectable open var sectionHeaderHeight: CGFloat = 0.0 49 | /// A height of each section footer. Set this value to 0.0 if you don't want section footer views. Default is *0.0* 50 | @IBInspectable open var sectionFooterHeight: CGFloat = 0.0 51 | /// An inset around the cell area in each section inset from section header and footer view and the collection view's bounds. Default is *zero* on every sides. 52 | @IBInspectable open var sectionCellInset: UIEdgeInsets = UIEdgeInsets() 53 | /// A size of each cells. 54 | @IBInspectable open var cellSize: CGSize = CGSize.zero 55 | /// Horizontal spacing between cells. Default value is *8.0* 56 | @IBInspectable open var spacing: CGFloat = 8.0 57 | 58 | /// A header view of the collection view. Similar to table view's *tableHeaderView* 59 | @IBOutlet open var headerView: UIView? 60 | /// A footer view of the collection view. Similar to table view's *tableFooterView* 61 | @IBOutlet open var footerView: UIView? 62 | 63 | /// A boolean indicates that the layout is preparing for cell panning. This will be set to *true* when we invalidate layout by panning cells. 64 | fileprivate var preparingForCellPanning = false 65 | 66 | public override init() { 67 | super.init() 68 | register(ShelfHeaderFooterView.self, forDecorationViewOfKind: ShelfElementKindCollectionHeader) 69 | register(ShelfHeaderFooterView.self, forDecorationViewOfKind: ShelfElementKindCollectionFooter) 70 | } 71 | 72 | required public init?(coder aDecoder: NSCoder) { 73 | super.init(coder: aDecoder) 74 | register(ShelfHeaderFooterView.self, forDecorationViewOfKind: ShelfElementKindCollectionHeader) 75 | register(ShelfHeaderFooterView.self, forDecorationViewOfKind: ShelfElementKindCollectionFooter) 76 | } 77 | 78 | open override func prepare() { 79 | defer { 80 | super.prepare() 81 | } 82 | 83 | guard let collectionView = collectionView else { 84 | return 85 | } 86 | 87 | if preparingForCellPanning { 88 | self.preparingForCellPanning = false 89 | 90 | return 91 | } 92 | 93 | headerViewLayoutAttributes = nil 94 | footerViewLayoutAttributes = nil 95 | sectionsFrame = [] 96 | sectionsCellFrame = [] 97 | sectionHeaderViewsLayoutAttributes = [] 98 | sectionFooterViewsLayoutAttributes = [] 99 | cellsLayoutAttributes = [] 100 | 101 | let oldPanningScrollViews = cellPanningScrollViews 102 | cellPanningScrollViews = [] 103 | defer { 104 | oldPanningScrollViews.forEach({ $0.trackingView = nil }) 105 | } 106 | 107 | var currentY = CGFloat(0.0) 108 | let collectionBounds = collectionView.bounds 109 | let collectionViewWidth = collectionBounds.width 110 | if let headerView = headerView { 111 | headerViewLayoutAttributes = CollectionViewShelfLayoutHeaderFooterViewLayoutAttributes(forDecorationViewOfKind: ShelfElementKindCollectionHeader, with: IndexPath(item: 0, section: 0)) 112 | headerViewLayoutAttributes?.view = headerView 113 | let headerViewSize = headerView.systemLayoutSizeFitting(CGSize(width: collectionViewWidth, height: 0.0), withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel) 114 | headerViewLayoutAttributes?.size = headerViewSize 115 | headerViewLayoutAttributes?.frame = CGRect(origin: CGPoint(x: collectionBounds.minX, y: currentY), size: headerViewSize) 116 | currentY += headerViewSize.height 117 | } 118 | 119 | let numberOfSections = collectionView.numberOfSections 120 | for section in 0.. 0.0 { 123 | let sectionHeaderAttributes = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: ShelfElementKindSectionHeader, with: IndexPath(item: 0, section: section)) 124 | sectionHeaderAttributes.frame = CGRect( 125 | origin: CGPoint(x: collectionBounds.minX, y: currentY), 126 | size: CGSize(width: collectionBounds.width, height: sectionHeaderHeight) 127 | ) 128 | sectionHeaderViewsLayoutAttributes.append(sectionHeaderAttributes) 129 | currentY += sectionHeaderHeight 130 | } 131 | 132 | var currentCellX = collectionBounds.minX + sectionCellInset.left 133 | if section < oldPanningScrollViews.count { 134 | // Apply the old scrolling offset before preparing layout 135 | currentCellX -= oldPanningScrollViews[section].contentOffset.x 136 | } 137 | 138 | let cellMinX = currentCellX - sectionCellInset.left 139 | currentY += sectionCellInset.top 140 | let topSectionCellMinY = currentY 141 | var cellInSectionAttributes: [UICollectionViewLayoutAttributes] = [] 142 | 143 | let itemsRange: CountableRange = (0.. CGSize in 146 | let cellSize: CGSize 147 | 148 | if let delegate = collectionView.delegate as? CollectionViewDelegateShelfLayout { 149 | cellSize = delegate.collectionView(collectionView, layout: self, sizeForItemAt: IndexPath(item: item, section: section)) 150 | } else { 151 | cellSize = self.cellSize 152 | } 153 | 154 | return cellSize 155 | }) 156 | 157 | let sectionCellHeight = cellSizes.reduce(0.0, { max($0, $1.height) }) 158 | 159 | for item in itemsRange { 160 | let cellSize = cellSizes[item] 161 | let cellAttributes = UICollectionViewLayoutAttributes(forCellWith: IndexPath(item: item, section: section)) 162 | cellAttributes.frame = CGRect( 163 | origin: CGPoint(x: currentCellX, y: currentY + (sectionCellHeight - cellSize.height) / 2), 164 | size: cellSize 165 | ) 166 | currentCellX += cellSize.width + spacing 167 | cellInSectionAttributes.append(cellAttributes) 168 | } 169 | let sectionCellFrame = CGRect( 170 | origin: CGPoint(x: 0.0, y: topSectionCellMinY), 171 | size: CGSize(width: currentCellX - spacing + sectionCellInset.right - cellMinX, height: sectionCellHeight) 172 | ) 173 | sectionsCellFrame.append(sectionCellFrame) 174 | 175 | cellsLayoutAttributes.append(cellInSectionAttributes) 176 | currentY += sectionCellHeight + sectionCellInset.bottom 177 | 178 | if sectionFooterHeight > 0.0 { 179 | let sectionHeaderAttributes = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: ShelfElementKindSectionFooter, with: IndexPath(item: 0, section: section)) 180 | sectionHeaderAttributes.frame = CGRect( 181 | origin: CGPoint(x: collectionBounds.minX, y: currentY), 182 | size: CGSize(width: collectionBounds.width, height: sectionFooterHeight) 183 | ) 184 | sectionFooterViewsLayoutAttributes.append(sectionHeaderAttributes) 185 | currentY += sectionFooterHeight 186 | } 187 | 188 | let sectionFrame = CGRect( 189 | origin: CGPoint(x: 0.0, y: sectionMinY), 190 | size: CGSize(width: collectionViewWidth, height: currentY - sectionMinY) 191 | ) 192 | sectionsFrame.append(sectionFrame) 193 | 194 | let panningScrollView = TrackingScrollView(frame: CGRect(origin: CGPoint.zero, size: sectionFrame.size)) 195 | panningScrollView.delegate = self 196 | panningScrollView.trackingView = collectionView 197 | panningScrollView.trackingFrame = sectionCellFrame 198 | if section < oldPanningScrollViews.count { 199 | // Apply scrolling content offset with the old offset before preparing layout 200 | panningScrollView.contentOffset = oldPanningScrollViews[section].contentOffset 201 | } 202 | 203 | cellPanningScrollViews.append(panningScrollView) 204 | } 205 | 206 | if let footerView = footerView { 207 | footerViewLayoutAttributes = CollectionViewShelfLayoutHeaderFooterViewLayoutAttributes(forDecorationViewOfKind: ShelfElementKindCollectionFooter, with: IndexPath(item: 0, section: 0)) 208 | footerViewLayoutAttributes?.view = footerView 209 | let footerViewSize = footerView.systemLayoutSizeFitting(CGSize(width: collectionViewWidth, height: 0.0), withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel) 210 | footerViewLayoutAttributes?.size = footerViewSize 211 | footerViewLayoutAttributes?.frame = CGRect(origin: CGPoint(x: collectionBounds.minX, y: currentY), size: footerViewSize) 212 | currentY += footerViewSize.height 213 | } 214 | } 215 | 216 | open override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { 217 | guard let collectionView = collectionView else { 218 | return true 219 | } 220 | return collectionView.frame.size != newBounds.size 221 | } 222 | 223 | open override var collectionViewContentSize: CGSize { 224 | guard let collectionView = collectionView else { 225 | return .zero 226 | } 227 | let width = collectionView.bounds.width 228 | let numberOfSections = CGFloat(collectionView.numberOfSections) 229 | 230 | let headerHeight = headerViewLayoutAttributes?.size.height ?? 0.0 231 | let footerHeight = footerViewLayoutAttributes?.size.height ?? 0.0 232 | let sectionHeaderHeight = self.sectionHeaderHeight * numberOfSections 233 | let sectionFooterHeight = self.sectionFooterHeight * numberOfSections 234 | 235 | let sectionCellInsetHeight = (sectionCellInset.bottom + sectionCellInset.top) * numberOfSections 236 | 237 | let heightOfAllSectionCells = sectionsCellFrame.reduce(0.0, { $0 + $1.height }) 238 | 239 | return CGSize( 240 | width: width, 241 | height: headerHeight + footerHeight + sectionHeaderHeight + sectionFooterHeight + sectionCellInsetHeight + heightOfAllSectionCells 242 | ) 243 | } 244 | 245 | open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { 246 | let headerAndFooterAttributes: [UICollectionViewLayoutAttributes] = [ headerViewLayoutAttributes, footerViewLayoutAttributes ] 247 | .compactMap({ $0 }).filter({ (attributes) -> Bool in 248 | return rect.intersects(attributes.frame) 249 | }) 250 | 251 | let visibleSections = sectionsFrame.enumerated() 252 | .filter({ (index: Int, element: CGRect) -> Bool in 253 | return rect.intersects(element) 254 | }) 255 | .map({ $0.offset }) 256 | 257 | let visibleAttributes = visibleSections 258 | .flatMap({ (section) -> [UICollectionViewLayoutAttributes] in 259 | var attributes: [UICollectionViewLayoutAttributes] = [] 260 | if section < self.sectionHeaderViewsLayoutAttributes.count { 261 | let header = self.sectionHeaderViewsLayoutAttributes[section] 262 | if rect.intersects(header.frame) { 263 | attributes.append(header) 264 | } 265 | } 266 | 267 | let visibleCellAttributes = self.cellsLayoutAttributes[section].filter({ (attributes) -> Bool in 268 | return rect.intersects(attributes.frame) 269 | }) 270 | 271 | attributes += visibleCellAttributes 272 | 273 | if section < self.sectionFooterViewsLayoutAttributes.count { 274 | let footer = self.sectionFooterViewsLayoutAttributes[section] 275 | if rect.intersects(footer.frame) { 276 | attributes.append(footer) 277 | } 278 | } 279 | 280 | return attributes 281 | }) 282 | 283 | return visibleAttributes + headerAndFooterAttributes 284 | } 285 | 286 | open override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { 287 | return cellsLayoutAttributes[indexPath.section][indexPath.item] 288 | } 289 | 290 | open override func layoutAttributesForDecorationView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { 291 | switch elementKind { 292 | case ShelfElementKindCollectionHeader: 293 | return headerViewLayoutAttributes 294 | case ShelfElementKindCollectionFooter: 295 | return footerViewLayoutAttributes 296 | default: 297 | return nil 298 | } 299 | } 300 | 301 | open override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { 302 | guard let sectionIndex = indexPath.firstIndex(of: 0) else { 303 | return nil 304 | } 305 | switch elementKind { 306 | case ShelfElementKindSectionHeader: 307 | return sectionHeaderViewsLayoutAttributes[sectionIndex] 308 | case ShelfElementKindSectionFooter: 309 | return sectionFooterViewsLayoutAttributes[sectionIndex] 310 | default: 311 | return nil 312 | } 313 | } 314 | 315 | open override func invalidateLayout(with context: UICollectionViewLayoutInvalidationContext) { 316 | if let context = context as? CollectionViewShelfLayoutInvalidationContext, 317 | let panningInformation = context.panningScrollView, 318 | let indexOfPanningScrollView = cellPanningScrollViews.firstIndex(of: panningInformation) { 319 | 320 | let panningCellsAttributes = cellsLayoutAttributes[indexOfPanningScrollView] 321 | let minX = panningCellsAttributes 322 | .reduce(CGFloat.greatestFiniteMagnitude, { (currentX, attributes) in 323 | return min(currentX, attributes.frame.minX) 324 | }) - sectionCellInset.left 325 | 326 | let offset = -panningInformation.contentOffset.x - minX 327 | 328 | // UICollectionViewLayout will not guarantee to call prepareLayout on every invalidation. 329 | // So we do the panning cell translation in the invalidate layout so that we can guarantee that every panning will be accounted. 330 | panningCellsAttributes.forEach({ (attributes) in 331 | attributes.frame = attributes.frame.offsetBy(dx: offset, dy: 0.0) 332 | }) 333 | 334 | self.preparingForCellPanning = true 335 | } 336 | 337 | super.invalidateLayout(with: context) 338 | } 339 | 340 | open override class var invalidationContextClass: AnyClass { 341 | return CollectionViewShelfLayoutInvalidationContext.self 342 | } 343 | } 344 | 345 | 346 | // MARK: - UIScrollViewDelegate methods 347 | 348 | extension CollectionViewShelfLayout: UIScrollViewDelegate { 349 | public func scrollViewDidScroll(_ scrollView: UIScrollView) { 350 | // Because we tell Collection View that it has its width of the content size equals to its width of its frame (and bounds). 351 | // This means that we can use its pan gesture recognizer to scroll our cells 352 | // Our hack is to use a scroll view per section, steal that scroll view's pan gesture recognizer and add it to collection view. 353 | // Uses the scroll view's content offset to tell us how uses scroll our cells 354 | guard let trackingScrollView = scrollView as? TrackingScrollView else { return } 355 | 356 | let context = CollectionViewShelfLayoutInvalidationContext(panningScrollView: trackingScrollView) 357 | invalidateLayout(with: context) 358 | } 359 | } 360 | 361 | 362 | // MARK: - App Store Collection Layout Data Types 363 | 364 | private class CollectionViewShelfLayoutHeaderFooterViewLayoutAttributes: UICollectionViewLayoutAttributes { 365 | var view: UIView! 366 | } 367 | 368 | private class CollectionViewShelfLayoutInvalidationContext: UICollectionViewLayoutInvalidationContext { 369 | fileprivate let panningScrollView: TrackingScrollView? 370 | 371 | override fileprivate var invalidateEverything: Bool { 372 | if panningScrollView == nil { 373 | return super.invalidateEverything 374 | } else { 375 | return false 376 | } 377 | } 378 | 379 | override fileprivate var invalidateDataSourceCounts: Bool { 380 | if panningScrollView == nil { 381 | return super.invalidateDataSourceCounts 382 | } else { 383 | return false 384 | } 385 | } 386 | 387 | override init() { 388 | self.panningScrollView = nil 389 | } 390 | 391 | init(panningScrollView: TrackingScrollView) { 392 | self.panningScrollView = panningScrollView 393 | } 394 | } 395 | 396 | 397 | // MARK: - Shelf Layout UICollectionReusableView 398 | 399 | private class ShelfHeaderFooterView: UICollectionReusableView { 400 | var view: UIView? { 401 | willSet { 402 | view?.removeFromSuperview() 403 | } 404 | didSet { 405 | if let view = view { 406 | addSubview(view) 407 | view.topAnchor.constraint(equalTo: topAnchor).isActive = true 408 | view.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true 409 | view.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true 410 | view.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true 411 | } 412 | } 413 | } 414 | 415 | fileprivate override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { 416 | if let layoutAttributes = layoutAttributes as? CollectionViewShelfLayoutHeaderFooterViewLayoutAttributes { 417 | view = layoutAttributes.view 418 | } 419 | super.apply(layoutAttributes) 420 | } 421 | } 422 | 423 | private class TrackingScrollView: UIScrollView { 424 | weak var trackingView: UIView? { 425 | willSet { 426 | removeFromSuperview() 427 | trackingView?.removeGestureRecognizer(panGestureRecognizer) 428 | } 429 | didSet { 430 | trackingView?.addGestureRecognizer(panGestureRecognizer) 431 | translatesAutoresizingMaskIntoConstraints = false 432 | trackingView?.insertSubview(self, at: 0) 433 | frame = CGRect(origin: .zero, size: trackingView?.bounds.size ?? .zero) 434 | } 435 | } 436 | var trackingFrame: CGRect = CGRect.zero { 437 | didSet { 438 | contentSize = trackingFrame.size 439 | } 440 | } 441 | 442 | override init(frame: CGRect) { 443 | super.init(frame: frame) 444 | layer.contents = UIImage().cgImage 445 | panGestureRecognizer.maximumNumberOfTouches = 1 446 | isHidden = true 447 | alpha = 0.0 448 | } 449 | 450 | required init?(coder aDecoder: NSCoder) { 451 | super.init(coder: aDecoder) 452 | layer.contents = UIImage().cgImage 453 | panGestureRecognizer.maximumNumberOfTouches = 1 454 | isHidden = true 455 | alpha = 0.0 456 | } 457 | 458 | @objc fileprivate override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { 459 | guard let panGestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer, panGestureRecognizer === self.panGestureRecognizer else { 460 | return false 461 | } 462 | 463 | let positionInTrackingView = panGestureRecognizer.location(in: trackingView) 464 | return trackingFrame.contains(positionInTrackingView) 465 | } 466 | 467 | @objc fileprivate func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool { 468 | guard let panGestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer, panGestureRecognizer === self.panGestureRecognizer else { 469 | return false 470 | } 471 | guard let otherPanGestureRecognizer = otherGestureRecognizer as? UIPanGestureRecognizer, otherPanGestureRecognizer.delegate is TrackingScrollView && otherPanGestureRecognizer.view === trackingView else { 472 | return false 473 | } 474 | 475 | return true 476 | } 477 | 478 | @objc fileprivate func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceiveTouch touch: UITouch) -> Bool { 479 | guard let panGestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer, self.panGestureRecognizer === panGestureRecognizer else { 480 | return false 481 | } 482 | 483 | let positionInTrackingView = touch.location(in: trackingView) 484 | return trackingFrame.contains(positionInTrackingView) 485 | } 486 | } 487 | 488 | 489 | -------------------------------------------------------------------------------- /CollectionViewShelfLayout.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 8A8C89381D4A797100E2F53B /* CollectionViewShelfLayout.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8A8DB5131D476F7F00F21FC7 /* CollectionViewShelfLayout.framework */; }; 11 | 8A8C89391D4A797100E2F53B /* CollectionViewShelfLayout.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 8A8DB5131D476F7F00F21FC7 /* CollectionViewShelfLayout.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 12 | 8A8DB5171D476F7F00F21FC7 /* CollectionViewShelfLayout.h in Headers */ = {isa = PBXBuildFile; fileRef = 8A8DB5161D476F7F00F21FC7 /* CollectionViewShelfLayout.h */; settings = {ATTRIBUTES = (Public, ); }; }; 13 | 8A8DB51F1D476FF400F21FC7 /* CollectionViewShelfLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A8DB51E1D476FF400F21FC7 /* CollectionViewShelfLayout.swift */; }; 14 | 8A91137C1FA5D7CA005029FB /* CustomItemSizeDemoCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A91137B1FA5D7CA005029FB /* CustomItemSizeDemoCollectionViewController.swift */; }; 15 | 8AC87D7D1D48EF7C00B0DAFE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AC87D731D48EF7C00B0DAFE /* AppDelegate.swift */; }; 16 | 8AC87D7E1D48EF7C00B0DAFE /* Apps.plist in Resources */ = {isa = PBXBuildFile; fileRef = 8AC87D741D48EF7C00B0DAFE /* Apps.plist */; }; 17 | 8AC87D7F1D48EF7C00B0DAFE /* AppStoreCollectionViewLayoutDemoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AC87D751D48EF7C00B0DAFE /* AppStoreCollectionViewLayoutDemoViewController.swift */; }; 18 | 8AC87D801D48EF7C00B0DAFE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8AC87D761D48EF7C00B0DAFE /* Assets.xcassets */; }; 19 | 8AC87D811D48EF7C00B0DAFE /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8AC87D781D48EF7C00B0DAFE /* LaunchScreen.storyboard */; }; 20 | 8AC87D821D48EF7C00B0DAFE /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8AC87D7A1D48EF7C00B0DAFE /* Main.storyboard */; }; 21 | /* End PBXBuildFile section */ 22 | 23 | /* Begin PBXContainerItemProxy section */ 24 | 8AC87D841D48EF8800B0DAFE /* PBXContainerItemProxy */ = { 25 | isa = PBXContainerItemProxy; 26 | containerPortal = 8A8DB50A1D476F7F00F21FC7 /* Project object */; 27 | proxyType = 1; 28 | remoteGlobalIDString = 8A8DB5121D476F7F00F21FC7; 29 | remoteInfo = AppStoreCollectionViewLayout; 30 | }; 31 | /* End PBXContainerItemProxy section */ 32 | 33 | /* Begin PBXCopyFilesBuildPhase section */ 34 | 8A8C893A1D4A797100E2F53B /* Embed Frameworks */ = { 35 | isa = PBXCopyFilesBuildPhase; 36 | buildActionMask = 2147483647; 37 | dstPath = ""; 38 | dstSubfolderSpec = 10; 39 | files = ( 40 | 8A8C89391D4A797100E2F53B /* CollectionViewShelfLayout.framework in Embed Frameworks */, 41 | ); 42 | name = "Embed Frameworks"; 43 | runOnlyForDeploymentPostprocessing = 0; 44 | }; 45 | /* End PBXCopyFilesBuildPhase section */ 46 | 47 | /* Begin PBXFileReference section */ 48 | 8A8DB5131D476F7F00F21FC7 /* CollectionViewShelfLayout.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CollectionViewShelfLayout.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 49 | 8A8DB5161D476F7F00F21FC7 /* CollectionViewShelfLayout.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CollectionViewShelfLayout.h; sourceTree = ""; }; 50 | 8A8DB5181D476F7F00F21FC7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 51 | 8A8DB51E1D476FF400F21FC7 /* CollectionViewShelfLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionViewShelfLayout.swift; sourceTree = ""; }; 52 | 8A91137B1FA5D7CA005029FB /* CustomItemSizeDemoCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomItemSizeDemoCollectionViewController.swift; sourceTree = ""; }; 53 | 8AC87D4E1D48EEFE00B0DAFE /* AppStoreCollectionViewLayout-Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "AppStoreCollectionViewLayout-Demo.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 54 | 8AC87D721D48EF7B00B0DAFE /* AppStoreCollectionViewLayout-Demo-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "AppStoreCollectionViewLayout-Demo-Bridging-Header.h"; sourceTree = ""; }; 55 | 8AC87D731D48EF7C00B0DAFE /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 56 | 8AC87D741D48EF7C00B0DAFE /* Apps.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Apps.plist; sourceTree = ""; }; 57 | 8AC87D751D48EF7C00B0DAFE /* AppStoreCollectionViewLayoutDemoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppStoreCollectionViewLayoutDemoViewController.swift; sourceTree = ""; }; 58 | 8AC87D761D48EF7C00B0DAFE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 59 | 8AC87D791D48EF7C00B0DAFE /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = LaunchScreen.storyboard; sourceTree = ""; }; 60 | 8AC87D7B1D48EF7C00B0DAFE /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Main.storyboard; sourceTree = ""; }; 61 | 8AC87D7C1D48EF7C00B0DAFE /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 62 | /* End PBXFileReference section */ 63 | 64 | /* Begin PBXFrameworksBuildPhase section */ 65 | 8A8DB50F1D476F7F00F21FC7 /* Frameworks */ = { 66 | isa = PBXFrameworksBuildPhase; 67 | buildActionMask = 2147483647; 68 | files = ( 69 | ); 70 | runOnlyForDeploymentPostprocessing = 0; 71 | }; 72 | 8AC87D4B1D48EEFE00B0DAFE /* Frameworks */ = { 73 | isa = PBXFrameworksBuildPhase; 74 | buildActionMask = 2147483647; 75 | files = ( 76 | 8A8C89381D4A797100E2F53B /* CollectionViewShelfLayout.framework in Frameworks */, 77 | ); 78 | runOnlyForDeploymentPostprocessing = 0; 79 | }; 80 | /* End PBXFrameworksBuildPhase section */ 81 | 82 | /* Begin PBXGroup section */ 83 | 8A8DB5091D476F7F00F21FC7 = { 84 | isa = PBXGroup; 85 | children = ( 86 | 8A8DB5151D476F7F00F21FC7 /* CollectionViewShelfLayout */, 87 | 8AC87D4F1D48EEFE00B0DAFE /* AppStoreCollectionViewLayout-Demo */, 88 | 8A8DB5141D476F7F00F21FC7 /* Products */, 89 | ); 90 | sourceTree = ""; 91 | }; 92 | 8A8DB5141D476F7F00F21FC7 /* Products */ = { 93 | isa = PBXGroup; 94 | children = ( 95 | 8A8DB5131D476F7F00F21FC7 /* CollectionViewShelfLayout.framework */, 96 | 8AC87D4E1D48EEFE00B0DAFE /* AppStoreCollectionViewLayout-Demo.app */, 97 | ); 98 | name = Products; 99 | sourceTree = ""; 100 | }; 101 | 8A8DB5151D476F7F00F21FC7 /* CollectionViewShelfLayout */ = { 102 | isa = PBXGroup; 103 | children = ( 104 | 8A8DB51E1D476FF400F21FC7 /* CollectionViewShelfLayout.swift */, 105 | 8A8DB5161D476F7F00F21FC7 /* CollectionViewShelfLayout.h */, 106 | 8A8DB5181D476F7F00F21FC7 /* Info.plist */, 107 | ); 108 | path = CollectionViewShelfLayout; 109 | sourceTree = ""; 110 | }; 111 | 8AC87D4F1D48EEFE00B0DAFE /* AppStoreCollectionViewLayout-Demo */ = { 112 | isa = PBXGroup; 113 | children = ( 114 | 8AC87D721D48EF7B00B0DAFE /* AppStoreCollectionViewLayout-Demo-Bridging-Header.h */, 115 | 8AC87D741D48EF7C00B0DAFE /* Apps.plist */, 116 | 8AC87D7C1D48EF7C00B0DAFE /* Info.plist */, 117 | 8AC87D731D48EF7C00B0DAFE /* AppDelegate.swift */, 118 | 8AC87D751D48EF7C00B0DAFE /* AppStoreCollectionViewLayoutDemoViewController.swift */, 119 | 8A91137B1FA5D7CA005029FB /* CustomItemSizeDemoCollectionViewController.swift */, 120 | 8AC87D761D48EF7C00B0DAFE /* Assets.xcassets */, 121 | 8AC87D771D48EF7C00B0DAFE /* Base.lproj */, 122 | ); 123 | path = "AppStoreCollectionViewLayout-Demo"; 124 | sourceTree = ""; 125 | }; 126 | 8AC87D771D48EF7C00B0DAFE /* Base.lproj */ = { 127 | isa = PBXGroup; 128 | children = ( 129 | 8AC87D781D48EF7C00B0DAFE /* LaunchScreen.storyboard */, 130 | 8AC87D7A1D48EF7C00B0DAFE /* Main.storyboard */, 131 | ); 132 | path = Base.lproj; 133 | sourceTree = ""; 134 | }; 135 | /* End PBXGroup section */ 136 | 137 | /* Begin PBXHeadersBuildPhase section */ 138 | 8A8DB5101D476F7F00F21FC7 /* Headers */ = { 139 | isa = PBXHeadersBuildPhase; 140 | buildActionMask = 2147483647; 141 | files = ( 142 | 8A8DB5171D476F7F00F21FC7 /* CollectionViewShelfLayout.h in Headers */, 143 | ); 144 | runOnlyForDeploymentPostprocessing = 0; 145 | }; 146 | /* End PBXHeadersBuildPhase section */ 147 | 148 | /* Begin PBXNativeTarget section */ 149 | 8A8DB5121D476F7F00F21FC7 /* CollectionViewShelfLayout */ = { 150 | isa = PBXNativeTarget; 151 | buildConfigurationList = 8A8DB51B1D476F7F00F21FC7 /* Build configuration list for PBXNativeTarget "CollectionViewShelfLayout" */; 152 | buildPhases = ( 153 | 8A8DB50E1D476F7F00F21FC7 /* Sources */, 154 | 8A8DB50F1D476F7F00F21FC7 /* Frameworks */, 155 | 8A8DB5101D476F7F00F21FC7 /* Headers */, 156 | 8A8DB5111D476F7F00F21FC7 /* Resources */, 157 | ); 158 | buildRules = ( 159 | ); 160 | dependencies = ( 161 | ); 162 | name = CollectionViewShelfLayout; 163 | productName = AppStoreCollectionViewLayout; 164 | productReference = 8A8DB5131D476F7F00F21FC7 /* CollectionViewShelfLayout.framework */; 165 | productType = "com.apple.product-type.framework"; 166 | }; 167 | 8AC87D4D1D48EEFE00B0DAFE /* AppStoreCollectionViewLayout-Demo */ = { 168 | isa = PBXNativeTarget; 169 | buildConfigurationList = 8AC87D5D1D48EEFE00B0DAFE /* Build configuration list for PBXNativeTarget "AppStoreCollectionViewLayout-Demo" */; 170 | buildPhases = ( 171 | 8AC87D4A1D48EEFE00B0DAFE /* Sources */, 172 | 8AC87D4B1D48EEFE00B0DAFE /* Frameworks */, 173 | 8AC87D4C1D48EEFE00B0DAFE /* Resources */, 174 | 8A8C893A1D4A797100E2F53B /* Embed Frameworks */, 175 | ); 176 | buildRules = ( 177 | ); 178 | dependencies = ( 179 | 8AC87D851D48EF8800B0DAFE /* PBXTargetDependency */, 180 | ); 181 | name = "AppStoreCollectionViewLayout-Demo"; 182 | productName = "AppStoreCollectionViewLayout-Demo"; 183 | productReference = 8AC87D4E1D48EEFE00B0DAFE /* AppStoreCollectionViewLayout-Demo.app */; 184 | productType = "com.apple.product-type.application"; 185 | }; 186 | /* End PBXNativeTarget section */ 187 | 188 | /* Begin PBXProject section */ 189 | 8A8DB50A1D476F7F00F21FC7 /* Project object */ = { 190 | isa = PBXProject; 191 | attributes = { 192 | LastSwiftUpdateCheck = 0730; 193 | LastUpgradeCheck = 0930; 194 | ORGANIZATIONNAME = "Pitiphong Phongpattranont"; 195 | TargetAttributes = { 196 | 8A8DB5121D476F7F00F21FC7 = { 197 | CreatedOnToolsVersion = 7.3.1; 198 | LastSwiftMigration = 1020; 199 | }; 200 | 8AC87D4D1D48EEFE00B0DAFE = { 201 | CreatedOnToolsVersion = 7.3.1; 202 | LastSwiftMigration = 1020; 203 | }; 204 | }; 205 | }; 206 | buildConfigurationList = 8A8DB50D1D476F7F00F21FC7 /* Build configuration list for PBXProject "CollectionViewShelfLayout" */; 207 | compatibilityVersion = "Xcode 3.2"; 208 | developmentRegion = en; 209 | hasScannedForEncodings = 0; 210 | knownRegions = ( 211 | en, 212 | Base, 213 | ); 214 | mainGroup = 8A8DB5091D476F7F00F21FC7; 215 | productRefGroup = 8A8DB5141D476F7F00F21FC7 /* Products */; 216 | projectDirPath = ""; 217 | projectRoot = ""; 218 | targets = ( 219 | 8A8DB5121D476F7F00F21FC7 /* CollectionViewShelfLayout */, 220 | 8AC87D4D1D48EEFE00B0DAFE /* AppStoreCollectionViewLayout-Demo */, 221 | ); 222 | }; 223 | /* End PBXProject section */ 224 | 225 | /* Begin PBXResourcesBuildPhase section */ 226 | 8A8DB5111D476F7F00F21FC7 /* Resources */ = { 227 | isa = PBXResourcesBuildPhase; 228 | buildActionMask = 2147483647; 229 | files = ( 230 | ); 231 | runOnlyForDeploymentPostprocessing = 0; 232 | }; 233 | 8AC87D4C1D48EEFE00B0DAFE /* Resources */ = { 234 | isa = PBXResourcesBuildPhase; 235 | buildActionMask = 2147483647; 236 | files = ( 237 | 8AC87D811D48EF7C00B0DAFE /* LaunchScreen.storyboard in Resources */, 238 | 8AC87D801D48EF7C00B0DAFE /* Assets.xcassets in Resources */, 239 | 8AC87D7E1D48EF7C00B0DAFE /* Apps.plist in Resources */, 240 | 8AC87D821D48EF7C00B0DAFE /* Main.storyboard in Resources */, 241 | ); 242 | runOnlyForDeploymentPostprocessing = 0; 243 | }; 244 | /* End PBXResourcesBuildPhase section */ 245 | 246 | /* Begin PBXSourcesBuildPhase section */ 247 | 8A8DB50E1D476F7F00F21FC7 /* Sources */ = { 248 | isa = PBXSourcesBuildPhase; 249 | buildActionMask = 2147483647; 250 | files = ( 251 | 8A8DB51F1D476FF400F21FC7 /* CollectionViewShelfLayout.swift in Sources */, 252 | ); 253 | runOnlyForDeploymentPostprocessing = 0; 254 | }; 255 | 8AC87D4A1D48EEFE00B0DAFE /* Sources */ = { 256 | isa = PBXSourcesBuildPhase; 257 | buildActionMask = 2147483647; 258 | files = ( 259 | 8A91137C1FA5D7CA005029FB /* CustomItemSizeDemoCollectionViewController.swift in Sources */, 260 | 8AC87D7F1D48EF7C00B0DAFE /* AppStoreCollectionViewLayoutDemoViewController.swift in Sources */, 261 | 8AC87D7D1D48EF7C00B0DAFE /* AppDelegate.swift in Sources */, 262 | ); 263 | runOnlyForDeploymentPostprocessing = 0; 264 | }; 265 | /* End PBXSourcesBuildPhase section */ 266 | 267 | /* Begin PBXTargetDependency section */ 268 | 8AC87D851D48EF8800B0DAFE /* PBXTargetDependency */ = { 269 | isa = PBXTargetDependency; 270 | target = 8A8DB5121D476F7F00F21FC7 /* CollectionViewShelfLayout */; 271 | targetProxy = 8AC87D841D48EF8800B0DAFE /* PBXContainerItemProxy */; 272 | }; 273 | /* End PBXTargetDependency section */ 274 | 275 | /* Begin PBXVariantGroup section */ 276 | 8AC87D781D48EF7C00B0DAFE /* LaunchScreen.storyboard */ = { 277 | isa = PBXVariantGroup; 278 | children = ( 279 | 8AC87D791D48EF7C00B0DAFE /* Base */, 280 | ); 281 | name = LaunchScreen.storyboard; 282 | sourceTree = ""; 283 | }; 284 | 8AC87D7A1D48EF7C00B0DAFE /* Main.storyboard */ = { 285 | isa = PBXVariantGroup; 286 | children = ( 287 | 8AC87D7B1D48EF7C00B0DAFE /* Base */, 288 | ); 289 | name = Main.storyboard; 290 | sourceTree = ""; 291 | }; 292 | /* End PBXVariantGroup section */ 293 | 294 | /* Begin XCBuildConfiguration section */ 295 | 8A8DB5191D476F7F00F21FC7 /* Debug */ = { 296 | isa = XCBuildConfiguration; 297 | buildSettings = { 298 | ALWAYS_SEARCH_USER_PATHS = NO; 299 | CLANG_ANALYZER_NONNULL = YES; 300 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 301 | CLANG_CXX_LIBRARY = "libc++"; 302 | CLANG_ENABLE_MODULES = YES; 303 | CLANG_ENABLE_OBJC_ARC = YES; 304 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 305 | CLANG_WARN_BOOL_CONVERSION = YES; 306 | CLANG_WARN_COMMA = YES; 307 | CLANG_WARN_CONSTANT_CONVERSION = YES; 308 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 309 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 310 | CLANG_WARN_EMPTY_BODY = YES; 311 | CLANG_WARN_ENUM_CONVERSION = YES; 312 | CLANG_WARN_INFINITE_RECURSION = YES; 313 | CLANG_WARN_INT_CONVERSION = YES; 314 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 315 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 316 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 317 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 318 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 319 | CLANG_WARN_STRICT_PROTOTYPES = YES; 320 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 321 | CLANG_WARN_UNREACHABLE_CODE = YES; 322 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 323 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 324 | COPY_PHASE_STRIP = NO; 325 | CURRENT_PROJECT_VERSION = 1; 326 | DEBUG_INFORMATION_FORMAT = dwarf; 327 | ENABLE_STRICT_OBJC_MSGSEND = YES; 328 | ENABLE_TESTABILITY = YES; 329 | GCC_C_LANGUAGE_STANDARD = gnu99; 330 | GCC_DYNAMIC_NO_PIC = NO; 331 | GCC_NO_COMMON_BLOCKS = YES; 332 | GCC_OPTIMIZATION_LEVEL = 0; 333 | GCC_PREPROCESSOR_DEFINITIONS = ( 334 | "DEBUG=1", 335 | "$(inherited)", 336 | ); 337 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 338 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 339 | GCC_WARN_UNDECLARED_SELECTOR = YES; 340 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 341 | GCC_WARN_UNUSED_FUNCTION = YES; 342 | GCC_WARN_UNUSED_VARIABLE = YES; 343 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 344 | MTL_ENABLE_DEBUG_INFO = YES; 345 | ONLY_ACTIVE_ARCH = YES; 346 | SDKROOT = iphoneos; 347 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 348 | SWIFT_VERSION = 4.2; 349 | TARGETED_DEVICE_FAMILY = "1,2"; 350 | VERSIONING_SYSTEM = "apple-generic"; 351 | VERSION_INFO_PREFIX = ""; 352 | }; 353 | name = Debug; 354 | }; 355 | 8A8DB51A1D476F7F00F21FC7 /* Release */ = { 356 | isa = XCBuildConfiguration; 357 | buildSettings = { 358 | ALWAYS_SEARCH_USER_PATHS = NO; 359 | CLANG_ANALYZER_NONNULL = YES; 360 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 361 | CLANG_CXX_LIBRARY = "libc++"; 362 | CLANG_ENABLE_MODULES = YES; 363 | CLANG_ENABLE_OBJC_ARC = YES; 364 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 365 | CLANG_WARN_BOOL_CONVERSION = YES; 366 | CLANG_WARN_COMMA = YES; 367 | CLANG_WARN_CONSTANT_CONVERSION = YES; 368 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 369 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 370 | CLANG_WARN_EMPTY_BODY = YES; 371 | CLANG_WARN_ENUM_CONVERSION = YES; 372 | CLANG_WARN_INFINITE_RECURSION = YES; 373 | CLANG_WARN_INT_CONVERSION = YES; 374 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 375 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 376 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 377 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 378 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 379 | CLANG_WARN_STRICT_PROTOTYPES = YES; 380 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 381 | CLANG_WARN_UNREACHABLE_CODE = YES; 382 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 383 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 384 | COPY_PHASE_STRIP = NO; 385 | CURRENT_PROJECT_VERSION = 1; 386 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 387 | ENABLE_NS_ASSERTIONS = NO; 388 | ENABLE_STRICT_OBJC_MSGSEND = YES; 389 | GCC_C_LANGUAGE_STANDARD = gnu99; 390 | GCC_NO_COMMON_BLOCKS = YES; 391 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 392 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 393 | GCC_WARN_UNDECLARED_SELECTOR = YES; 394 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 395 | GCC_WARN_UNUSED_FUNCTION = YES; 396 | GCC_WARN_UNUSED_VARIABLE = YES; 397 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 398 | MTL_ENABLE_DEBUG_INFO = NO; 399 | SDKROOT = iphoneos; 400 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 401 | SWIFT_VERSION = 4.2; 402 | TARGETED_DEVICE_FAMILY = "1,2"; 403 | VALIDATE_PRODUCT = YES; 404 | VERSIONING_SYSTEM = "apple-generic"; 405 | VERSION_INFO_PREFIX = ""; 406 | }; 407 | name = Release; 408 | }; 409 | 8A8DB51C1D476F7F00F21FC7 /* Debug */ = { 410 | isa = XCBuildConfiguration; 411 | buildSettings = { 412 | CODE_SIGN_IDENTITY = "iPhone Developer"; 413 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 414 | DEFINES_MODULE = YES; 415 | DYLIB_COMPATIBILITY_VERSION = 0.6.0; 416 | DYLIB_CURRENT_VERSION = 0.6.2; 417 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 418 | INFOPLIST_FILE = CollectionViewShelfLayout/Info.plist; 419 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 420 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 421 | PRODUCT_BUNDLE_IDENTIFIER = me.pitiphong.CollectionViewShelfLayout; 422 | PRODUCT_NAME = "$(TARGET_NAME)"; 423 | SKIP_INSTALL = YES; 424 | }; 425 | name = Debug; 426 | }; 427 | 8A8DB51D1D476F7F00F21FC7 /* Release */ = { 428 | isa = XCBuildConfiguration; 429 | buildSettings = { 430 | CODE_SIGN_IDENTITY = "iPhone Developer"; 431 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 432 | DEFINES_MODULE = YES; 433 | DYLIB_COMPATIBILITY_VERSION = 0.6.0; 434 | DYLIB_CURRENT_VERSION = 0.6.2; 435 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 436 | INFOPLIST_FILE = CollectionViewShelfLayout/Info.plist; 437 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 438 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 439 | PRODUCT_BUNDLE_IDENTIFIER = me.pitiphong.CollectionViewShelfLayout; 440 | PRODUCT_NAME = "$(TARGET_NAME)"; 441 | SKIP_INSTALL = YES; 442 | }; 443 | name = Release; 444 | }; 445 | 8AC87D5E1D48EEFE00B0DAFE /* Debug */ = { 446 | isa = XCBuildConfiguration; 447 | buildSettings = { 448 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 449 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 450 | CODE_SIGN_IDENTITY = "iPhone Developer"; 451 | INFOPLIST_FILE = "AppStoreCollectionViewLayout-Demo/Info.plist"; 452 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 453 | PRODUCT_BUNDLE_IDENTIFIER = "me.pitiphong.AppStoreCollectionViewLayout-Demo"; 454 | PRODUCT_NAME = "$(TARGET_NAME)"; 455 | SWIFT_OBJC_BRIDGING_HEADER = "AppStoreCollectionViewLayout-Demo/AppStoreCollectionViewLayout-Demo-Bridging-Header.h"; 456 | }; 457 | name = Debug; 458 | }; 459 | 8AC87D5F1D48EEFE00B0DAFE /* Release */ = { 460 | isa = XCBuildConfiguration; 461 | buildSettings = { 462 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 463 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 464 | CODE_SIGN_IDENTITY = "iPhone Developer"; 465 | INFOPLIST_FILE = "AppStoreCollectionViewLayout-Demo/Info.plist"; 466 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 467 | PRODUCT_BUNDLE_IDENTIFIER = "me.pitiphong.AppStoreCollectionViewLayout-Demo"; 468 | PRODUCT_NAME = "$(TARGET_NAME)"; 469 | SWIFT_OBJC_BRIDGING_HEADER = "AppStoreCollectionViewLayout-Demo/AppStoreCollectionViewLayout-Demo-Bridging-Header.h"; 470 | }; 471 | name = Release; 472 | }; 473 | /* End XCBuildConfiguration section */ 474 | 475 | /* Begin XCConfigurationList section */ 476 | 8A8DB50D1D476F7F00F21FC7 /* Build configuration list for PBXProject "CollectionViewShelfLayout" */ = { 477 | isa = XCConfigurationList; 478 | buildConfigurations = ( 479 | 8A8DB5191D476F7F00F21FC7 /* Debug */, 480 | 8A8DB51A1D476F7F00F21FC7 /* Release */, 481 | ); 482 | defaultConfigurationIsVisible = 0; 483 | defaultConfigurationName = Release; 484 | }; 485 | 8A8DB51B1D476F7F00F21FC7 /* Build configuration list for PBXNativeTarget "CollectionViewShelfLayout" */ = { 486 | isa = XCConfigurationList; 487 | buildConfigurations = ( 488 | 8A8DB51C1D476F7F00F21FC7 /* Debug */, 489 | 8A8DB51D1D476F7F00F21FC7 /* Release */, 490 | ); 491 | defaultConfigurationIsVisible = 0; 492 | defaultConfigurationName = Release; 493 | }; 494 | 8AC87D5D1D48EEFE00B0DAFE /* Build configuration list for PBXNativeTarget "AppStoreCollectionViewLayout-Demo" */ = { 495 | isa = XCConfigurationList; 496 | buildConfigurations = ( 497 | 8AC87D5E1D48EEFE00B0DAFE /* Debug */, 498 | 8AC87D5F1D48EEFE00B0DAFE /* Release */, 499 | ); 500 | defaultConfigurationIsVisible = 0; 501 | defaultConfigurationName = Release; 502 | }; 503 | /* End XCConfigurationList section */ 504 | }; 505 | rootObject = 8A8DB50A1D476F7F00F21FC7 /* Project object */; 506 | } 507 | --------------------------------------------------------------------------------