├── .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 | 
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 |
--------------------------------------------------------------------------------