├── .gitignore
├── LICENSE
├── README.md
├── Screenshots
└── TGLStackedViewExample.gif
├── TGLStackedViewController.podspec
├── TGLStackedViewController
├── Info.plist
├── TGLExposedLayout.h
├── TGLExposedLayout.m
├── TGLStackedLayout.h
├── TGLStackedLayout.m
├── TGLStackedViewController.h
└── TGLStackedViewController.m
├── TGLStackedViewExample.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ └── contents.xcworkspacedata
└── xcshareddata
│ └── xcschemes
│ └── TGLStackedViewController.xcscheme
└── TGLStackedViewExample
├── Base.lproj
└── Main.storyboard
├── Images.xcassets
├── AppIcon.appiconset
│ └── Contents.json
├── Background.imageset
│ ├── Contents.json
│ ├── background.png
│ └── background@2x.png
└── Contents.json
├── Launch Screen.xib
├── TGLAppDelegate.h
├── TGLAppDelegate.m
├── TGLBackgroundProxyView.h
├── TGLBackgroundProxyView.m
├── TGLCollectionViewCell.h
├── TGLCollectionViewCell.m
├── TGLSettingOptionsViewController.h
├── TGLSettingOptionsViewController.m
├── TGLSettingsViewController.h
├── TGLSettingsViewController.m
├── TGLStackedViewExample-Info.plist
├── TGLStackedViewExample-Prefix.pch
├── TGLViewController.h
├── TGLViewController.m
├── en.lproj
└── InfoPlist.strings
└── main.m
/.gitignore:
--------------------------------------------------------------------------------
1 | #########################
2 | # .gitignore file for Xcode5
3 | #
4 | # NB: if you are storing "built" products, this WILL NOT WORK,
5 | # and you should use a different .gitignore (or none at all)
6 | # This file is for SOURCE projects, where there are many extra
7 | # files that we want to exclude
8 | #
9 | # https://gist.github.com/dulaccc/31df7f166a462bf7eacd
10 | #########################
11 |
12 | #####
13 | # OS X temporary files that should never be committed
14 |
15 | .DS_Store
16 | *.swp
17 | profile
18 |
19 |
20 | ####
21 | # Xcode temporary files that should never be committed
22 | #
23 | # NB: NIB/XIB files still exist even on Storyboard projects, so we want this...
24 |
25 | *~.nib
26 |
27 |
28 | ####
29 | # Xcode build files -
30 | #
31 | # NB: slash on the end, so we only remove the FOLDER, not any files that were badly named "DerivedData"
32 |
33 | DerivedData/
34 |
35 | # NB: slash on the end, so we only remove the FOLDER, not any files that were badly named "build"
36 |
37 | build/
38 |
39 |
40 | #####
41 | # Xcode private settings (window sizes, bookmarks, breakpoints, custom executables, smart groups)
42 | #
43 | # This is complicated:
44 | #
45 | # SOMETIMES you need to put this file in version control.
46 | # Apple designed it poorly - if you use "custom executables", they are
47 | # saved in this file.
48 | # 99% of projects do NOT use those, so they do NOT want to version control this file.
49 | # ..but if you're in the 1%, comment out the line "*.pbxuser"
50 |
51 | *.pbxuser
52 | *.mode1v3
53 | *.mode2v3
54 | *.perspectivev3
55 | # NB: also, whitelist the default ones, some projects need to use these
56 | !default.pbxuser
57 | !default.mode1v3
58 | !default.mode2v3
59 | !default.perspectivev3
60 |
61 |
62 | ####
63 | # Xcode 4 - semi-personal settings, often included in workspaces
64 | #
65 | # You can safely ignore the xcuserdata files - but do NOT ignore the files next to them
66 | #
67 |
68 | xcuserdata
69 |
70 |
71 | ####
72 | # XCode 4 workspaces - more detailed
73 | #
74 | # Workspaces are important! They are a core feature of Xcode - don't exclude them :)
75 | #
76 | # Workspace layout is quite spammy. For reference:
77 | #
78 | # (root)/
79 | # (project-name).xcodeproj/
80 | # project.pbxproj
81 | # project.xcworkspace/
82 | # contents.xcworkspacedata
83 | # xcuserdata/
84 | # (your name)/xcuserdatad/
85 | # xcuserdata/
86 | # (your name)/xcuserdatad/
87 | #
88 | #
89 | #
90 | # Xcode 4 workspaces - SHARED
91 | #
92 | # This is UNDOCUMENTED (google: "developer.apple.com xcshareddata" - 0 results
93 | # But if you're going to kill personal workspaces, at least keep the shared ones...
94 | #
95 | #
96 |
97 | !xcshareddata
98 |
99 |
100 | ####
101 | # XCode 4 build-schemes
102 | #
103 | # PRIVATE ones are stored inside xcuserdata
104 |
105 | !xcschemes
106 |
107 |
108 | ####
109 | # Xcode 4 - Deprecated classes
110 | #
111 | # Allegedly, if you manually "deprecate" your classes, they get moved here.
112 | #
113 | # We're using source-control, so this is a "feature" that we do not want!
114 |
115 | *.moved-aside
116 |
117 |
118 | ####
119 | # Xcode 5 - Source Control files
120 | #
121 | # Xcode 5 introduced a new file type .xccheckout. This files contains VCS metadata
122 | # and should therefore not be checked into the VCS.
123 |
124 | *.xccheckout
125 |
126 |
127 | ####
128 | # Cocoapods
129 |
130 | Pods/
131 | !Podfile.lock
132 |
133 |
134 | ####
135 | # UNKNOWN: recommended by others, but I can't discover what these files are
136 | #
137 | # ...none. Everything is now explained.:
138 | /TGLStackedViewExample.xcodeproj/project.xcworkspace/xcshareddata
139 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014-2019 Tim Gleue (http://gleue-interactive.com)
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
13 | all 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
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](http://cocoadocs.org/docsets/TGLStackedViewController)
2 | [](https://github.com/gleue/TGLStackedViewController/tags)
3 | [](https://cocoapods.org)
4 | [](https://github.com/Carthage/Carthage)
5 | [](https://opensource.org/licenses/MIT)
6 | [](https://cocoapods.org/pods/TGLStackedViewController)
7 |
8 | TGLStackedViewController
9 | ========================
10 |
11 | A stack layout with gesture-based reordering using UICollectionView -- inspired by Passbook and Reminders apps.
12 |
13 | What's new in 2.2
14 | ------------------
15 |
16 | * Reimplementation of item reordering using UIKit drag and drop on iOS 11.0
17 |
18 | What's new in 2.0
19 | ------------------
20 |
21 | * Uses iOS 9 collection view reordering API instead of own implementation, therefore minimum deployment target is iOS 9.0
22 | * Unexposed items are pinned to bottom by default, now
23 | * The exposed item can be collapsed interactively in addition to tapping it
24 | * When pinning (the default) pan item down to switch back to stacked layout
25 | * When just pushing cards aside use pinch gesture instead
26 | * Improved sample project w/ lots of settings to tweak interactively
27 |
28 |
29 |
30 |
31 |
32 | Getting Started
33 | ===============
34 |
35 | Take a look at sample project `TGLStackedViewExample.xcodeproj`. You may use sample class `TGLViewController` as a starting point for your own implementation.
36 |
37 | Usage
38 | =====
39 |
40 | Via [CocoaPods](http://cocoapods.org):
41 |
42 | * Add `pod 'TGLStackedViewController', '~> 2.2'` to your project's `Podfile`
43 |
44 | Via [Carthage](https://github.com/Carthage/Carthage):
45 |
46 | * Add `github "gleue/TGLStackedViewController", ~> 2.2` to your project's `Cartfile`
47 |
48 | Or the "classic" way:
49 |
50 | * Add files in folder `TGLStackedViewController` to your project
51 |
52 | Then in your project:
53 |
54 | * Create a subclass derived from `TGLStackedViewController`
55 | * Implement the `UICollectionViewDataSource` protocol in your subclass
56 | * *Currently only 1 section is supported* therefore your implementation of `-numberOfSectionsInCollectionView` *has to* return `1`. `TGLStackedViewController` provides a suitable implementation for you -- no need to overwrite.
57 | * Implement methods `-numberOfSectionsInCollectionView:` and `-collectionView:cellForItemAtIndexPath` as usual.
58 | * **New in 2.0**: `TGLStackedViewController`'s implementation of method `-collectionView:canMoveItemAtIndexPath:` checks for stacked layout and a minimum number of 2 items before allowing reordering. Make sure to call `super` in your implementation and honor it's result.
59 | * **New in 2.0**: Implement method `-collectionView:moveItemAtIndexPath:toIndexPath:` to update your data model after items have been reordered
60 | * Implement the `UICollectionViewDelegate` protocol in your subclass
61 | * `TGLStackedViewController` already implements methods `-collectionView:shouldHighlightItemAtIndexPath:`, `-collectionView:didDeselectItemAtIndexPath`, and `-collectionView:didSelectItemAtIndexPath:` internally, so make sure to call `super` in your implementation.
62 | * Method `-collectionView:targetContentOffsetForProposedContentOffset:` is crucuial for properly transitioning betwenn exposed and stacked layout, so make sure to call `super` in your implementation.
63 | * Place `UICollectionViewController` in your storyboard and set its class to your derived class
64 | * Make sure to set up the collection view's `delegate` and `dataSource` connections properly
65 | * **New in 2.0**: `TGLStackedViewController` does no longer create a layout object internally.
66 | * Set the collection view controller's layout class to `TGLStackedLayout` or your own subclass of `TGLStackedLayout` in the inspector in Interface Builder.
67 | * When creating a `TGLStackedViewController` in code you have to set the collection view's layout before presenting the view controller.
68 |
69 | Requirements
70 | ============
71 |
72 | * ARC
73 | * iOS >= 9.0
74 | * Xcode 9.0
75 |
76 | Credits
77 | =======
78 |
79 | - Reordering in 1.x based on [LXReorderableCollectionViewFlowLayout](https://github.com/lxcid/LXReorderableCollectionViewFlowLayout)
80 | - Original `Podspec` by [Pierre Dulac](https://github.com/dulaccc)
81 | - Carthage support by [Hannes Oud](https://github.com/hannesoid)
82 |
83 | License
84 | =======
85 |
86 | TGLStackedViewController is available under the MIT License (MIT)
87 |
88 | Copyright (c) 2014-2019 Tim Gleue (http://gleue-interactive.com)
89 |
90 | Permission is hereby granted, free of charge, to any person obtaining a copy
91 | of this software and associated documentation files (the "Software"), to deal
92 | in the Software without restriction, including without limitation the rights
93 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
94 | copies of the Software, and to permit persons to whom the Software is
95 | furnished to do so, subject to the following conditions:
96 |
97 | The above copyright notice and this permission notice shall be included in
98 | all copies or substantial portions of the Software.
99 |
100 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
101 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
102 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
103 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
104 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
105 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
106 | THE SOFTWARE.
107 |
--------------------------------------------------------------------------------
/Screenshots/TGLStackedViewExample.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gleue/TGLStackedViewController/dc227eab8d996c15b0a4baffd8771b443b917156/Screenshots/TGLStackedViewExample.gif
--------------------------------------------------------------------------------
/TGLStackedViewController.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 | s.name = 'TGLStackedViewController'
3 | s.version = '2.2.4'
4 | s.license = 'MIT'
5 | s.summary = 'A stacked view layout with gesture-based reordering using a UICollectionView -- inspired by Passbook and Reminders apps.'
6 | s.homepage = 'https://github.com/gleue/TGLStackedViewController'
7 | s.authors = { 'Tim Gleue' => 'tim@gleue-interactive.com' }
8 | s.source = { :git => 'https://github.com/gleue/TGLStackedViewController.git', :tag => s.version.to_s }
9 | s.source_files = 'TGLStackedViewController/{TGLStackedViewController,TGLExposedLayout,TGLStackedLayout}.{h,m}'
10 |
11 | s.requires_arc = true
12 | s.platform = :ios, '9.0'
13 | end
14 |
--------------------------------------------------------------------------------
/TGLStackedViewController/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 | 2.1
19 | CFBundleVersion
20 | 2.1.4
21 | NSPrincipalClass
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/TGLStackedViewController/TGLExposedLayout.h:
--------------------------------------------------------------------------------
1 | //
2 | // TGLExposedLayout.h
3 | // TGLStackedViewController
4 | //
5 | // Created by Tim Gleue on 07.04.14.
6 | // Copyright (c) 2014-2019 Tim Gleue ( http://gleue-interactive.com )
7 | //
8 | // Permission is hereby granted, free of charge, to any person obtaining a copy
9 | // of this software and associated documentation files (the "Software"), to deal
10 | // in the Software without restriction, including without limitation the rights
11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | // copies of the Software, and to permit persons to whom the Software is
13 | // furnished to do so, subject to the following conditions:
14 | //
15 | // The above copyright notice and this permission notice shall be included in
16 | // all copies or substantial portions of the Software.
17 | //
18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 | // THE SOFTWARE.
25 |
26 | #import
27 |
28 | typedef NS_ENUM(NSInteger, TGLExposedLayoutPinningMode) {
29 |
30 | TGLExposedLayoutPinningModeNone = 0, /* Do not pin unexpsed items */
31 | TGLExposedLayoutPinningModeBelow, /* Pin items below exposed item */
32 | TGLExposedLayoutPinningModeAll /* Pin all unexposed items */
33 | };
34 |
35 | /** Collection view layout showing a single exposed
36 | * item full size and adjacent items collapsed with
37 | * configurable overlap.
38 | *
39 | * Scrolling is not possible since -collectionViewContentSize
40 | * is the same as the collection view's bounds.size.
41 | */
42 | @interface TGLExposedLayout : UICollectionViewLayout
43 |
44 | /** Margins between collection view and items. Default is UIEdgeInsetsMake(40.0, 0.0, 0.0, 0.0) */
45 | @property (assign, nonatomic) UIEdgeInsets layoutMargin;
46 |
47 | /** Size of items or automatic dimensions when 0.
48 | *
49 | * If either width or height or both are set to 0 (default)
50 | * the respective dimensions are computed automatically
51 | * from the collection view's bounds minus the margins
52 | * defined in property -layoutMargin.
53 | */
54 | @property (assign, nonatomic) CGSize itemSize;
55 |
56 | /** Amount of overlap for items above exposed item. Default 10.0 */
57 | @property (assign, nonatomic) CGFloat topOverlap;
58 |
59 | /** Amount of overlap for items below exposed item. Default 10.0 */
60 | @property (assign, nonatomic) CGFloat bottomOverlap;
61 |
62 | /** Number of items overlapping below exposed item when not pinning. Default 1 */
63 | @property (assign, nonatomic) NSUInteger bottomOverlapCount;
64 |
65 | /** Layout mode for other than exposed items. Default `TGLExposedLayoutPinningModeAll` */
66 | @property (assign, nonatomic) TGLExposedLayoutPinningMode pinningMode;
67 |
68 | /** The number of items above the exposed item to be pinned or `-1` for all. Default -1 */
69 | @property (assign, nonatomic) NSInteger topPinningCount;
70 |
71 | /** The number of items below the exposed item to be pinned or `-1` for all. Default -1 */
72 | @property (assign, nonatomic) NSInteger bottomPinningCount;
73 |
74 | - (instancetype)initWithExposedItemIndex:(NSInteger)exposedItemIndex;
75 |
76 | @end
77 |
--------------------------------------------------------------------------------
/TGLStackedViewController/TGLExposedLayout.m:
--------------------------------------------------------------------------------
1 | //
2 | // TGLExposedLayout.m
3 | // TGLStackedViewController
4 | //
5 | // Created by Tim Gleue on 07.04.14.
6 | // Copyright (c) 2014-2019 Tim Gleue ( http://gleue-interactive.com )
7 | //
8 | // Permission is hereby granted, free of charge, to any person obtaining a copy
9 | // of this software and associated documentation files (the "Software"), to deal
10 | // in the Software without restriction, including without limitation the rights
11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | // copies of the Software, and to permit persons to whom the Software is
13 | // furnished to do so, subject to the following conditions:
14 | //
15 | // The above copyright notice and this permission notice shall be included in
16 | // all copies or substantial portions of the Software.
17 | //
18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 | // THE SOFTWARE.
25 |
26 | #import "TGLExposedLayout.h"
27 |
28 | @interface TGLExposedLayout ()
29 |
30 | @property (assign, nonatomic) NSInteger exposedItemIndex;
31 |
32 | @property (nonatomic, strong) NSDictionary *layoutAttributes;
33 |
34 | @end
35 |
36 | @implementation TGLExposedLayout
37 |
38 | - (instancetype)initWithExposedItemIndex:(NSInteger)exposedItemIndex {
39 |
40 | self = [super init];
41 |
42 | if (self) {
43 |
44 | self.layoutMargin = UIEdgeInsetsMake(40.0, 0.0, 0.0, 0.0);
45 | self.topOverlap = 10.0;
46 | self.bottomOverlap = 10.0;
47 | self.bottomOverlapCount = 1;
48 |
49 | self.pinningMode = TGLExposedLayoutPinningModeAll;
50 | self.topPinningCount = -1;
51 | self.bottomPinningCount = -1;
52 |
53 | self.exposedItemIndex = exposedItemIndex;
54 | }
55 |
56 | return self;
57 | }
58 |
59 | #pragma mark - Accessors
60 |
61 | - (void)setLayoutMargin:(UIEdgeInsets)margins {
62 |
63 | if (!UIEdgeInsetsEqualToEdgeInsets(margins, self.layoutMargin)) {
64 |
65 | _layoutMargin = margins;
66 |
67 | [self invalidateLayout];
68 | }
69 | }
70 |
71 | - (void)setItemSize:(CGSize)itemSize {
72 |
73 | if (!CGSizeEqualToSize(itemSize, self.itemSize)) {
74 |
75 | _itemSize = itemSize;
76 |
77 | [self invalidateLayout];
78 | }
79 | }
80 |
81 | - (void)setTopOverlap:(CGFloat)topOverlap {
82 |
83 | if (topOverlap != self.topOverlap) {
84 |
85 | _topOverlap = topOverlap;
86 |
87 | [self invalidateLayout];
88 | }
89 | }
90 |
91 | - (void)setBottomOverlap:(CGFloat)bottomOverlap {
92 |
93 | if (bottomOverlap != self.bottomOverlap) {
94 |
95 | _bottomOverlap = bottomOverlap;
96 |
97 | [self invalidateLayout];
98 | }
99 | }
100 |
101 | - (void)setBottomOverlapCount:(NSUInteger)bottomOverlapCount {
102 |
103 | if (bottomOverlapCount != self.bottomOverlapCount) {
104 |
105 | _bottomOverlapCount = bottomOverlapCount;
106 |
107 | [self invalidateLayout];
108 | }
109 | }
110 |
111 | #pragma mark - Layout computation
112 |
113 | - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset {
114 |
115 | // See http://stackoverflow.com/a/25416243
116 | //
117 | return CGPointZero;
118 | }
119 |
120 | - (CGSize)collectionViewContentSize {
121 |
122 | CGSize contentSize = self.collectionView.bounds.size;
123 |
124 | contentSize.height -= self.collectionView.contentInset.top + self.collectionView.contentInset.bottom;
125 |
126 | return contentSize;
127 | }
128 |
129 | - (void)prepareLayout {
130 |
131 | CGSize layoutSize = CGSizeMake(CGRectGetWidth(self.collectionView.bounds) - self.layoutMargin.left - self.layoutMargin.right,
132 | CGRectGetHeight(self.collectionView.bounds) - self.layoutMargin.top - self.layoutMargin.bottom);
133 |
134 | CGSize itemSize = self.itemSize;
135 |
136 | if (itemSize.width == 0.0) itemSize.width = layoutSize.width;
137 | if (itemSize.height == 0.0) itemSize.height = self.collectionViewContentSize.height - self.layoutMargin.top - self.layoutMargin.bottom;
138 |
139 | CGFloat itemHorizontalOffset = 0.5 * (layoutSize.width - itemSize.width);
140 | CGPoint itemOrigin = CGPointMake(self.layoutMargin.left + floor(itemHorizontalOffset), 0.0);
141 |
142 | NSMutableDictionary *layoutAttributes = [NSMutableDictionary dictionary];
143 | NSInteger itemCount = [self.collectionView numberOfItemsInSection:0];
144 | NSInteger bottomOverlapCount = self.bottomOverlapCount;
145 | NSInteger bottomPinningCount = MIN(itemCount - self.exposedItemIndex - 1, self.bottomPinningCount);
146 |
147 | if (bottomPinningCount < 0) bottomPinningCount = itemCount - self.exposedItemIndex - 1;
148 |
149 | NSInteger topPinningCount = self.topPinningCount;
150 |
151 | if (topPinningCount < 0) topPinningCount = self.exposedItemIndex;
152 |
153 | for (NSInteger item = 0; item < itemCount; item++) {
154 |
155 | NSIndexPath *indexPath = [NSIndexPath indexPathForItem:item inSection:0];
156 | UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
157 |
158 | // Cards overlap each other
159 | // via z depth AND transform
160 | //
161 | // See http://stackoverflow.com/questions/12659301/uicollectionview-setlayoutanimated-not-preserving-zindex
162 | //
163 | // KLUDGE: translation is along negative
164 | // z axis as not to block scroll
165 | // indicators
166 | //
167 | attributes.zIndex = item;
168 | attributes.transform3D = CATransform3DMakeTranslation(0, 0, item - itemCount);
169 |
170 | if (item < self.exposedItemIndex) {
171 |
172 | if (self.pinningMode == TGLExposedLayoutPinningModeAll) {
173 |
174 | NSInteger count = self.exposedItemIndex - item;
175 |
176 | if (count > topPinningCount) {
177 |
178 | attributes.frame = CGRectMake(itemOrigin.x, self.collectionViewContentSize.height, itemSize.width, itemSize.height);
179 | attributes.hidden = YES;
180 |
181 | } else {
182 |
183 | count += bottomPinningCount;
184 |
185 | attributes.frame = CGRectMake(itemOrigin.x, self.collectionViewContentSize.height - self.layoutMargin.bottom - count * self.bottomOverlap, itemSize.width, itemSize.height);
186 | }
187 |
188 | } else {
189 |
190 | // Items before exposed item
191 | // are aligned above top with
192 | // amount -topOverlap
193 | //
194 | attributes.frame = CGRectMake(itemOrigin.x, self.layoutMargin.top - self.topOverlap, itemSize.width, itemSize.height);
195 |
196 | // Items below first unexposed
197 | // are hidden to improve
198 | // performance
199 | //
200 | if (item < self.exposedItemIndex - 1) attributes.hidden = YES;
201 | }
202 |
203 | } else if (item == self.exposedItemIndex) {
204 |
205 | // Exposed item
206 | //
207 | attributes.frame = CGRectMake(itemOrigin.x, self.layoutMargin.top, itemSize.width, itemSize.height);
208 |
209 | } else if (self.pinningMode != TGLExposedLayoutPinningModeNone) {
210 |
211 | // Pinning lower items to bottom
212 | //
213 | if (item > self.exposedItemIndex + bottomPinningCount) {
214 |
215 | attributes.frame = CGRectMake(itemOrigin.x, self.collectionViewContentSize.height, itemSize.width, itemSize.height);
216 | attributes.hidden = YES;
217 |
218 | } else {
219 |
220 | NSInteger count = MIN(bottomPinningCount + 1, itemCount - self.exposedItemIndex) - (item - self.exposedItemIndex);
221 |
222 | attributes.frame = CGRectMake(itemOrigin.x, self.collectionViewContentSize.height - self.layoutMargin.bottom - count * self.bottomOverlap, itemSize.width, itemSize.height);
223 | }
224 |
225 | } else if (item > self.exposedItemIndex + bottomOverlapCount) {
226 |
227 | // Items following overlapping
228 | // items at bottom are hidden
229 | // to improve performance
230 | //
231 | attributes.frame = CGRectMake(itemOrigin.x, self.collectionViewContentSize.height, itemSize.width, itemSize.height);
232 | attributes.hidden = YES;
233 |
234 | } else {
235 |
236 | // At max -bottomOverlapCount
237 | // overlapping item(s) at the
238 | // bottom right below the
239 | // exposed item
240 | //
241 | NSInteger count = MIN(self.bottomOverlapCount + 1, itemCount - self.exposedItemIndex) - (item - self.exposedItemIndex);
242 |
243 | attributes.frame = CGRectMake(itemOrigin.x, self.layoutMargin.top + itemSize.height - count * self.bottomOverlap, itemSize.width, itemSize.height);
244 |
245 | // Issue #21
246 | //
247 | // Make sure overlapping cards
248 | // reach to the bottom before
249 | // being hidden
250 | //
251 | if (item == self.exposedItemIndex + bottomOverlapCount && attributes.frame.origin.y < self.collectionView.bounds.size.height - self.layoutMargin.bottom) {
252 |
253 | ++bottomOverlapCount;
254 | }
255 | }
256 |
257 | layoutAttributes[indexPath] = attributes;
258 | }
259 |
260 | self.layoutAttributes = layoutAttributes;
261 | }
262 |
263 | - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
264 |
265 | NSMutableArray *layoutAttributes = [NSMutableArray array];
266 |
267 | [self.layoutAttributes enumerateKeysAndObjectsUsingBlock:^(NSIndexPath *indexPath, UICollectionViewLayoutAttributes *attributes, BOOL *stop) {
268 |
269 | if (CGRectIntersectsRect(rect, attributes.frame)) {
270 |
271 | [layoutAttributes addObject:attributes];
272 | }
273 | }];
274 |
275 | return layoutAttributes;
276 | }
277 |
278 | - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
279 |
280 | return self.layoutAttributes[indexPath];
281 | }
282 |
283 | @end
284 |
--------------------------------------------------------------------------------
/TGLStackedViewController/TGLStackedLayout.h:
--------------------------------------------------------------------------------
1 | //
2 | // TGLStackedLayout.h
3 | // TGLStackedViewController
4 | //
5 | // Created by Tim Gleue on 07.04.14.
6 | // Copyright (c) 2014-2019 Tim Gleue ( http://gleue-interactive.com )
7 | //
8 | // Permission is hereby granted, free of charge, to any person obtaining a copy
9 | // of this software and associated documentation files (the "Software"), to deal
10 | // in the Software without restriction, including without limitation the rights
11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | // copies of the Software, and to permit persons to whom the Software is
13 | // furnished to do so, subject to the following conditions:
14 | //
15 | // The above copyright notice and this permission notice shall be included in
16 | // all copies or substantial portions of the Software.
17 | //
18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 | // THE SOFTWARE.
25 |
26 | #import
27 |
28 | @interface TGLStackedLayout : UICollectionViewLayout
29 |
30 | /** Margins between collection view and items. Default is `UIEdgeInsetsMake(20.0, 0.0, 0.0, 0.0)` */
31 | @property (nonatomic, assign) IBInspectable UIEdgeInsets layoutMargin;
32 |
33 | /** Size of items or automatic dimensions when 0.
34 | *
35 | * If either width or height or both are set to 0 (default)
36 | * the respective dimensions ares computed automatically
37 | * from the collection view's bounds minus the margins
38 | * defined in property `-layoutMargin`.
39 | */
40 | @property (nonatomic, assign) IBInspectable CGSize itemSize;
41 |
42 | /** Amount to show of each stacked item. Default is 120.0 */
43 | @property (nonatomic, assign) IBInspectable CGFloat topReveal;
44 |
45 | /** Amount of compression/expansing when scrolling bounces. Default is 0.2 */
46 | @property (nonatomic, assign) IBInspectable CGFloat bounceFactor;
47 |
48 | /** Scale factor for moving item. Default is 0.95 */
49 | @property (nonatomic, assign) IBInspectable CGFloat movingItemScaleFactor NS_DEPRECATED_IOS(7, 11);
50 |
51 | /** Allow moving item to float above of all other items. Default value is `YES` */
52 | @property (nonatomic, assign) IBInspectable BOOL movingItemOnTop;
53 |
54 | /** Set to YES to ignore -topReveal and arrange items evenly in collection view's bounds, if items do not fill entire height. Default is `NO` */
55 | @property (nonatomic, assign, getter = isFillingHeight) IBInspectable BOOL fillHeight;
56 |
57 | /** Set to YES to center a single item vertically, honoring -layoutMargin. When multiple items are present this property is ignored. Defualt is `NO` */
58 | @property (nonatomic, assign, getter = isCenteringSingleItem) IBInspectable BOOL centerSingleItem;
59 |
60 | /** Set to YES to enable bouncing even when items do not fill entire height. Default is `NO` */
61 | @property (nonatomic, assign, getter = isAlwaysBouncing) IBInspectable BOOL alwaysBounce;
62 |
63 | /** Use -contentOffset instead of collection view's actual content offset for next layout */
64 | @property (nonatomic, assign) BOOL overwriteContentOffset;
65 |
66 | /** Content offset value to replace actual value when -overwriteContentOffset is `YES` */
67 | @property (nonatomic, assign) CGPoint contentOffset;
68 |
69 | @end
70 |
--------------------------------------------------------------------------------
/TGLStackedViewController/TGLStackedLayout.m:
--------------------------------------------------------------------------------
1 | //
2 | // TGLStackedLayout.m
3 | // TGLStackedViewController
4 | //
5 | // Created by Tim Gleue on 07.04.14.
6 | // Copyright (c) 2014-2019 Tim Gleue ( http://gleue-interactive.com )
7 | //
8 | // Permission is hereby granted, free of charge, to any person obtaining a copy
9 | // of this software and associated documentation files (the "Software"), to deal
10 | // in the Software without restriction, including without limitation the rights
11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | // copies of the Software, and to permit persons to whom the Software is
13 | // furnished to do so, subject to the following conditions:
14 | //
15 | // The above copyright notice and this permission notice shall be included in
16 | // all copies or substantial portions of the Software.
17 | //
18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 | // THE SOFTWARE.
25 |
26 | #import "TGLStackedLayout.h"
27 |
28 | @interface TGLStackedLayout ()
29 |
30 | @property (nonatomic, strong) NSDictionary *layoutAttributes;
31 |
32 | // Set to YES when layout is currently arranging
33 | // items so that they evenly fill entire height
34 | //
35 | @property (nonatomic, assign) BOOL filling;
36 |
37 | @end
38 |
39 | @implementation TGLStackedLayout
40 |
41 | - (instancetype)init {
42 |
43 | self = [super init];
44 |
45 | if (self) [self initLayout];
46 |
47 | return self;
48 | }
49 |
50 | - (instancetype)initWithCoder:(NSCoder *)aDecoder {
51 |
52 | self = [super initWithCoder:aDecoder];
53 |
54 | if (self) [self initLayout];
55 |
56 | return self;
57 | }
58 |
59 | - (void)initLayout {
60 |
61 | self.layoutMargin = UIEdgeInsetsMake(20.0, 0.0, 0.0, 0.0);
62 | self.topReveal = 120.0;
63 | self.bounceFactor = 0.2;
64 | self.movingItemScaleFactor = 0.95;
65 | self.movingItemOnTop = YES;
66 | }
67 |
68 | #pragma mark - Accessors
69 |
70 | - (void)setLayoutMargin:(UIEdgeInsets)margins {
71 |
72 | if (!UIEdgeInsetsEqualToEdgeInsets(margins, self.layoutMargin)) {
73 |
74 | _layoutMargin = margins;
75 |
76 | [self invalidateLayout];
77 | }
78 | }
79 |
80 | - (void)setTopReveal:(CGFloat)topReveal {
81 |
82 | if (topReveal != self.topReveal) {
83 |
84 | _topReveal = topReveal;
85 |
86 | [self invalidateLayout];
87 | }
88 | }
89 |
90 | - (void)setItemSize:(CGSize)itemSize {
91 |
92 | if (!CGSizeEqualToSize(itemSize, self.itemSize)) {
93 |
94 | _itemSize = itemSize;
95 |
96 | [self invalidateLayout];
97 | }
98 | }
99 |
100 | - (void)setBounceFactor:(CGFloat)bounceFactor {
101 |
102 | if (bounceFactor != self.bounceFactor) {
103 |
104 | _bounceFactor = bounceFactor;
105 |
106 | [self invalidateLayout];
107 | }
108 | }
109 |
110 | - (void)setFillHeight:(BOOL)fillHeight {
111 |
112 | if (fillHeight != self.isFillingHeight) {
113 |
114 | _fillHeight = fillHeight;
115 |
116 | [self invalidateLayout];
117 | }
118 | }
119 |
120 | - (void)setAlwaysBounce:(BOOL)alwaysBounce {
121 |
122 | if (alwaysBounce != self.alwaysBounce) {
123 |
124 | _alwaysBounce = alwaysBounce;
125 |
126 | [self invalidateLayout];
127 | }
128 | }
129 |
130 | #pragma mark - Layout computation
131 |
132 | - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset {
133 |
134 | // Honor overwritten contentOffset
135 | //
136 | // See http://stackoverflow.com/a/25416243
137 | //
138 | return self.overwriteContentOffset ? self.contentOffset : proposedContentOffset;
139 | }
140 |
141 | - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
142 |
143 | return YES;
144 | }
145 |
146 | - (CGSize)collectionViewContentSize {
147 |
148 | CGSize contentSize = CGSizeMake(CGRectGetWidth(self.collectionView.bounds), self.layoutMargin.top + self.topReveal * [self.collectionView numberOfItemsInSection:0] + self.layoutMargin.bottom);
149 |
150 | if (contentSize.height < CGRectGetHeight(self.collectionView.bounds)) {
151 |
152 | contentSize.height = CGRectGetHeight(self.collectionView.bounds);
153 |
154 | // Adding an extra point of content height
155 | // enables scrolling/bouncing
156 | //
157 | if (self.isAlwaysBouncing) contentSize.height += 1.0;
158 |
159 | self.filling = self.isFillingHeight;
160 |
161 | } else {
162 |
163 | self.filling = NO;
164 | }
165 |
166 | return contentSize;
167 | }
168 |
169 | - (void)prepareLayout {
170 |
171 | // Force update of property -filling
172 | // used to decide whether to arrange
173 | // items evenly in collection view's
174 | // full height
175 | //
176 | [self collectionViewContentSize];
177 |
178 | CGSize layoutSize = CGSizeMake(CGRectGetWidth(self.collectionView.bounds) - self.layoutMargin.left - self.layoutMargin.right,
179 | CGRectGetHeight(self.collectionView.bounds) - self.layoutMargin.top - self.layoutMargin.bottom);
180 |
181 | CGFloat itemReveal = self.topReveal;
182 |
183 | if (self.filling) {
184 |
185 | itemReveal = floor(layoutSize.height / [self.collectionView numberOfItemsInSection:0]);
186 | }
187 |
188 | CGSize itemSize = self.itemSize;
189 |
190 | if (itemSize.width == 0.0) itemSize.width = layoutSize.width;
191 | if (itemSize.height == 0.0) itemSize.height = layoutSize.height;
192 |
193 | CGFloat itemHorizontalOffset = 0.5 * (layoutSize.width - itemSize.width);
194 | CGPoint itemOrigin = CGPointMake(self.layoutMargin.left + floor(itemHorizontalOffset), 0.0);
195 |
196 | // Honor overwritten contentOffset
197 | //
198 | CGPoint contentOffset = self.overwriteContentOffset ? self.contentOffset : self.collectionView.contentOffset;
199 |
200 | NSMutableDictionary *layoutAttributes = [NSMutableDictionary dictionary];
201 | UICollectionViewLayoutAttributes *previousTopOverlappingAttributes[2] = { nil, nil };
202 | NSInteger itemCount = [self.collectionView numberOfItemsInSection:0];
203 |
204 | static NSInteger firstCompressingItem = -1;
205 |
206 | for (NSInteger item = 0; item < itemCount; item++) {
207 |
208 | NSIndexPath *indexPath = [NSIndexPath indexPathForItem:item inSection:0];
209 | UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
210 |
211 | // By default all items are layed
212 | // out evenly with each revealing
213 | // only top part ...
214 | //
215 | attributes.frame = CGRectMake(itemOrigin.x, self.layoutMargin.top + itemReveal * item, itemSize.width, itemSize.height);
216 |
217 | // Cards overlap each other
218 | // via z depth AND transform
219 | //
220 | // See http://stackoverflow.com/questions/12659301/uicollectionview-setlayoutanimated-not-preserving-zindex
221 | //
222 | // KLUDGE: translation is along negative
223 | // z axis as not to block scroll
224 | // indicators
225 | //
226 | attributes.zIndex = item;
227 | attributes.transform3D = CATransform3DMakeTranslation(0, 0, item - itemCount);
228 |
229 | if (itemCount == 1 && self.isCenteringSingleItem) {
230 |
231 | // Center single item if necessary
232 | //
233 | CGRect frame = attributes.frame;
234 |
235 | frame.origin.y = self.layoutMargin.top + 0.5 * (layoutSize.height - itemSize.height);
236 |
237 | attributes.frame = frame;
238 |
239 | } else if (contentOffset.y + self.collectionView.contentInset.top < 0.0) {
240 |
241 | // Expand cells when reaching top
242 | // and user scrolls further down,
243 | // i.e. when bouncing
244 | //
245 | CGRect frame = attributes.frame;
246 |
247 | frame.origin.y -= self.bounceFactor * (contentOffset.y + self.collectionView.contentInset.top) * item;
248 |
249 | attributes.frame = frame;
250 |
251 | } else if (CGRectGetMinY(attributes.frame) < contentOffset.y + self.layoutMargin.top) {
252 |
253 | // Topmost cells overlap stack, but
254 | // are placed directly above each
255 | // other such that only one cell
256 | // is visible
257 | //
258 | CGRect frame = attributes.frame;
259 |
260 | frame.origin.y = contentOffset.y + self.layoutMargin.top;
261 |
262 | attributes.frame = frame;
263 |
264 | // Keep queue of last two items'
265 | // attributes and hide any item
266 | // below top overlapping item to
267 | // improve performance
268 | //
269 | if (previousTopOverlappingAttributes[1]) previousTopOverlappingAttributes[1].hidden = YES;
270 |
271 | previousTopOverlappingAttributes[1] = previousTopOverlappingAttributes[0];
272 | previousTopOverlappingAttributes[0] = attributes;
273 |
274 | } else if (self.collectionViewContentSize.height > CGRectGetHeight(self.collectionView.bounds) && contentOffset.y > self.collectionViewContentSize.height - CGRectGetHeight(self.collectionView.bounds)) {
275 |
276 | // Compress cells when reaching bottom
277 | // and user scrolls further up,
278 | // i.e. when bouncing
279 | //
280 | if (firstCompressingItem < 0) {
281 |
282 | firstCompressingItem = item;
283 |
284 | } else {
285 |
286 | CGRect frame = attributes.frame;
287 | CGFloat delta = contentOffset.y + CGRectGetHeight(self.collectionView.bounds) - self.collectionViewContentSize.height;
288 |
289 | frame.origin.y += self.bounceFactor * delta * (firstCompressingItem - item);
290 | frame.origin.y = MAX(frame.origin.y, contentOffset.y + self.layoutMargin.top);
291 |
292 | attributes.frame = frame;
293 | }
294 |
295 | } else {
296 |
297 | firstCompressingItem = -1;
298 | }
299 |
300 | layoutAttributes[indexPath] = attributes;
301 | }
302 |
303 | self.layoutAttributes = layoutAttributes;
304 | }
305 |
306 | - (UICollectionViewLayoutAttributes *)layoutAttributesForInteractivelyMovingItemAtIndexPath:(NSIndexPath *)indexPath withTargetPosition:(CGPoint)position {
307 |
308 | UICollectionViewLayoutAttributes *attributes = [super layoutAttributesForInteractivelyMovingItemAtIndexPath:indexPath withTargetPosition:position];
309 |
310 | if (self.movingItemOnTop) {
311 |
312 | // If moving item should float above
313 | // other items change z ordering
314 | //
315 | // NOTE: Since z transform is from -#items to 0.0
316 | // we place floating item at +1
317 | //
318 | attributes.zIndex = NSIntegerMax;
319 | attributes.transform3D = CATransform3DMakeTranslation(0.0, 0.0, 1.0);
320 | }
321 |
322 | // Apply scale factor in addition to z transform
323 | //
324 | attributes.transform3D = CATransform3DScale(attributes.transform3D, self.movingItemScaleFactor, self.movingItemScaleFactor, 1.0);
325 |
326 | return attributes;
327 | }
328 |
329 | - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
330 |
331 | NSMutableArray *layoutAttributes = [NSMutableArray array];
332 |
333 | [self.layoutAttributes enumerateKeysAndObjectsUsingBlock:^(NSIndexPath *indexPath, UICollectionViewLayoutAttributes *attributes, BOOL *stop) {
334 |
335 | if (CGRectIntersectsRect(rect, attributes.frame)) {
336 |
337 | [layoutAttributes addObject:attributes];
338 | }
339 | }];
340 |
341 | return layoutAttributes;
342 | }
343 |
344 | - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
345 |
346 | return self.layoutAttributes[indexPath];
347 | }
348 |
349 | @end
350 |
--------------------------------------------------------------------------------
/TGLStackedViewController/TGLStackedViewController.h:
--------------------------------------------------------------------------------
1 | //
2 | // TGLStackedViewController.h
3 | // TGLStackedViewController
4 | //
5 | // Created by Tim Gleue on 07.04.14.
6 | // Copyright (c) 2014-2019 Tim Gleue ( http://gleue-interactive.com )
7 | //
8 | // Permission is hereby granted, free of charge, to any person obtaining a copy
9 | // of this software and associated documentation files (the "Software"), to deal
10 | // in the Software without restriction, including without limitation the rights
11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | // copies of the Software, and to permit persons to whom the Software is
13 | // furnished to do so, subject to the following conditions:
14 | //
15 | // The above copyright notice and this permission notice shall be included in
16 | // all copies or substantial portions of the Software.
17 | //
18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 | // THE SOFTWARE.
25 |
26 | #import
27 |
28 | FOUNDATION_EXPORT double TGLStackedViewControllerVersionNumber;
29 | FOUNDATION_EXPORT const unsigned char TGLStackedViewControllerVersionString[];
30 |
31 | #import "TGLStackedLayout.h"
32 | #import "TGLExposedLayout.h"
33 |
34 | @interface TGLStackedViewController : UICollectionViewController
35 |
36 | /** The collection view layout object used when all items are collapsed.
37 | *
38 | * When using storyboards, this property is only intialized in method
39 | * `-viewDidLoad`.
40 | */
41 | @property (nonatomic, readonly, nullable) TGLStackedLayout *stackedLayout;
42 |
43 | /** The collection view layout object used when a single item is exposed. */
44 | @property (nonatomic, readonly, nullable) TGLExposedLayout *exposedLayout;
45 |
46 | /** Margins between collection view and items when exposed.
47 | *
48 | * Changes to this property take effect on next
49 | * item being selected, i.e. exposed.
50 | *
51 | * Default value is UIEdgeInsetsMake(40.0, 0.0, 0.0, 0.0)
52 | */
53 | @property (nonatomic, assign) IBInspectable UIEdgeInsets exposedLayoutMargin;
54 |
55 | /** Size of items when exposed if set to value not equal CGSizeZero.
56 | *
57 | * Changes to this property take effect on next
58 | * item being selected, i.e. exposed.
59 | *
60 | * Default value is CGSizeZero
61 | */
62 | @property (nonatomic, assign) IBInspectable CGSize exposedItemSize;
63 |
64 | /** Amount of overlap for items above exposed item.
65 | *
66 | * The value is effective only if `-exposedPinningMode`
67 | * is equal to `TGLExposedLayoutPinningModeNone` and
68 | * ignored otherwise. Changes to this property take
69 | * effect on next item being selected, i.e. exposed.
70 | *
71 | * Default value is 10.0
72 | */
73 | @property (nonatomic, assign) IBInspectable CGFloat exposedTopOverlap;
74 |
75 | /** Amount of overlap for items below exposed item.
76 | *
77 | * The value is effective only if `-exposedPinningMode`
78 | * is equal to `TGLExposedLayoutPinningModeNone` and
79 | * ignored otherwise. Changes to this property take
80 | * effect on next item being selected, i.e. exposed.
81 | *
82 | * Default value is 10.0
83 | */
84 | @property (nonatomic, assign) IBInspectable CGFloat exposedBottomOverlap;
85 |
86 | /** Number of items overlapping below exposed item.
87 | *
88 | * The value is effective only if `-exposedPinningMode`
89 | * is equal to `TGLExposedLayoutPinningModeNone` and
90 | * ignored otherwise. Changes to this property take
91 | * effect on next item being selected, i.e. exposed.
92 | *
93 | * Default value is 1
94 | */
95 | @property (nonatomic, assign) IBInspectable NSUInteger exposedBottomOverlapCount;
96 |
97 | /** Layout mode for other than exposed items.
98 | *
99 | * Controls how the items surrounding the exposed item
100 | * above and below should be layed out. When set to
101 | * `TGLExposedLayoutPinningModeNone` items are pushed to
102 | * the top and the bottom edges of the exposed item,
103 | * overlapping upwards and downwards by `-exposedTopOverlap`
104 | * and `-exposedBottomOverlap`. This is the default.
105 | *
106 | * When set to `TGLExposedLayoutPinningModeBelow` the
107 | * items above the exposed item are pushed to the exposed
108 | * item's top edge as above, while the items below are pinned
109 | * to the collection view's bottom edge, and overlapping upwards.
110 | *
111 | * When set to `TGLExposedLayoutPinningModeAll` all items but
112 | * the exposed item are pinned to the collection view's bottom
113 | * edge, and overlapping upwards.
114 | *
115 | * Default value is `TGLExposedLayoutPinningModeAll`
116 | */
117 | @property (nonatomic, assign) TGLExposedLayoutPinningMode exposedPinningMode;
118 |
119 | /** The number of items above the exposed item to be pinned.
120 | *
121 | * The value is effective only if `-exposedPinningMode`
122 | * is not equal to `TGLExposedLayoutPinningModeNone` and
123 | * ignored otherwise. Changes to this property take
124 | * effect on next item being selected, i.e. exposed.
125 | *
126 | * Default value is -1
127 | */
128 | @property (nonatomic, assign) IBInspectable NSUInteger exposedTopPinningCount;
129 |
130 | /** The number of items below the exposed item to be pinned.
131 | *
132 | * The value is effective only if `-exposedPinningMode`
133 | * is not equal to `TGLExposedLayoutPinningModeNone` and
134 | * ignored otherwise. Changes to this property take
135 | * effect on next item being selected, i.e. exposed.
136 | *
137 | * Default value is -1
138 | */
139 | @property (nonatomic, assign) IBInspectable NSUInteger exposedBottomPinningCount;
140 |
141 | /** Index path of currently exposed item.
142 | *
143 | * When the user exposes an item this property
144 | * contains the item's index path. The value
145 | * is nil if no item is exposed.
146 | *
147 | * Set this property to a valid item index path
148 | * location to expose it instead of the current
149 | * one, or set to nil to collapse all items.
150 | *
151 | * The exposed item's selected state is `YES`.
152 | *
153 | * The layout transition is animated. If no animation
154 | * is required call `-setExposedItemIndexPath:animated:`
155 | * instead.
156 | *
157 | * @see -setExposedItemIndexPath:animated:
158 | */
159 | @property (nonatomic, strong, nullable) NSIndexPath *exposedItemIndexPath;
160 |
161 | /** Allow exposed items to be interactively collapsed by a gesture.
162 | *
163 | * If `-exposedPinningMode` is set to `TGLExposedLayoutPinningModeNone`
164 | * a pinch gesture is used to interactively transition from exposed
165 | * to stacked layout. Otherwise a vertical pan gesture is used.
166 | *
167 | * The respective gesture is effective only if this property is `YES`.
168 | * Changes to this property take effect on next item being selected,
169 | * i.e. exposed.
170 | *
171 | * Default value is `YES`
172 | */
173 | @property (nonatomic, assign) IBInspectable BOOL exposedItemsAreCollapsible;
174 |
175 | /** Allow the overlapping parts of unexposed items
176 | * to be tapped and thus select another item.
177 | *
178 | * If set to `NO` (default), the currently exposed item
179 | * has to be tapped to deselect or interactively collapesed
180 | * before another item may be selected.
181 | */
182 | @property (nonatomic, assign) IBInspectable BOOL unexposedItemsAreSelectable;
183 |
184 | /** Factor used to scale items while being moved interactively.
185 | *
186 | * Default value is 0.95
187 | */
188 | @property (nonatomic, assign) IBInspectable CGFloat movingItemScaleFactor;
189 |
190 | /** Allow item being moved interactively to float above of all other items.
191 | *
192 | * Default value is `YES`
193 | */
194 | @property (nonatomic, assign) IBInspectable BOOL movingItemOnTop;
195 |
196 | /** Minimum amount of downwards panning at end of gesture to trigger collapse.
197 | *
198 | * Default value is 120.0
199 | */
200 | @property (nonatomic, assign) IBInspectable CGFloat collapsePanMinimumThreshold;
201 |
202 | /** Maximum amount of downwards panning to consider gesture transition to be complete.
203 | *
204 | * If the property value is less or equal 0.0 the exposed item's height is used.
205 | *
206 | * Default value is 0.0
207 | */
208 | @property (nonatomic, assign) IBInspectable CGFloat collapsePanMaximumThreshold;
209 |
210 | /** Minimum percentage of pinching at end of gesture to trigger collapse.
211 | *
212 | * Value 1.0 means 100%, i.e. fully pinched, and 0.0 means 0%, i.e. no pinch at all.
213 | *
214 | * Default value is 0.25
215 | */
216 | @property (nonatomic, assign) IBInspectable CGFloat collapsePinchMinimumThreshold;
217 |
218 | /** Returns the class to use when creating the exposed layout.
219 | *
220 | * If you subclass `TGLExposedLayout` overwrite this method
221 | * and return your subclass.
222 | */
223 | + (nonnull Class)exposedLayoutClass;
224 |
225 | /** Sets the currently exposed item.
226 | *
227 | * Expose the item at a valid index path location
228 | * instead of the current one, or pass to `nil`
229 | * to collapse all items.
230 | *
231 | * The resulting layout transition may be animated.
232 | *
233 | * The exposed item's selected state is `YES`.
234 | *
235 | * @param exposedItemIndexPath The index path of the item to be exposed.
236 | * @param animated If `YES` the layout transition will be animated.
237 | *
238 | * @see -exposedItemIndexPath
239 | * @see -setExposedItemIndexPath:animated:completion:
240 | */
241 | - (void)setExposedItemIndexPath:(nullable NSIndexPath *)exposedItemIndexPath animated:(BOOL)animated;
242 |
243 | /** Sets the currently exposed item.
244 | *
245 | * Expose the item at a valid index path location
246 | * instead of the current one, or pass to `nil`
247 | * to collapse all items.
248 | *
249 | * The resulting layout transition may be animated.
250 | *
251 | * The exposed item's selected state is `YES`.
252 | *
253 | * @param exposedItemIndexPath The index path of the item to be exposed.
254 | * @param animated If `YES` the layout transition will be animated.
255 | * @param completion The block to execute after the transition finishes.
256 | *
257 | * @see -exposedItemIndexPath
258 | */
259 | - (void)setExposedItemIndexPath:(nullable NSIndexPath *)exposedItemIndexPath animated:(BOOL)animated completion:(nullable void (^)(void))completion;
260 |
261 | @end
262 |
--------------------------------------------------------------------------------
/TGLStackedViewController/TGLStackedViewController.m:
--------------------------------------------------------------------------------
1 | //
2 | // TGLStackedViewController.m
3 | // TGLStackedViewController
4 | //
5 | // Created by Tim Gleue on 07.04.14.
6 | // Copyright (c) 2014-2019 Tim Gleue ( http://gleue-interactive.com )
7 | //
8 | // Permission is hereby granted, free of charge, to any person obtaining a copy
9 | // of this software and associated documentation files (the "Software"), to deal
10 | // in the Software without restriction, including without limitation the rights
11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | // copies of the Software, and to permit persons to whom the Software is
13 | // furnished to do so, subject to the following conditions:
14 | //
15 | // The above copyright notice and this permission notice shall be included in
16 | // all copies or substantial portions of the Software.
17 | //
18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 | // THE SOFTWARE.
25 |
26 | #import "TGLStackedViewController.h"
27 |
28 | @interface TGLStackedViewController ()
29 |
30 | @property (nonatomic, strong) TGLStackedLayout *stackedLayout;
31 | @property (nonatomic, strong) TGLExposedLayout *exposedLayout;
32 | @property (nonatomic, weak) UICollectionViewTransitionLayout *transitionLayout;
33 |
34 | @property (nonatomic, strong) UILongPressGestureRecognizer *moveGestureRecognizer;
35 | @property (nonatomic, strong) NSIndexPath *movingIndexPath;
36 | @property (nonatomic, strong) NSIndexPath *dragSourceIndexPath;
37 |
38 | @property (nonatomic, readonly) UIGestureRecognizer *collapseGestureRecognizer;
39 | @property (nonatomic, readonly) UIPanGestureRecognizer *collapsePanGestureRecognizer;
40 | @property (nonatomic, readonly) UIPinchGestureRecognizer *collapsePinchGestureRecognizer;
41 |
42 | @property (nonatomic, assign, getter=isFinishingInteractiveTransition) BOOL finishingInteractiveTransition;
43 | @property (nonatomic, assign, getter=isDragging) BOOL dragging;
44 |
45 | @end
46 |
47 | @implementation TGLStackedViewController
48 |
49 | @synthesize collapsePanGestureRecognizer = _collapsePanGestureRecognizer;
50 | @synthesize collapsePinchGestureRecognizer = _collapsePinchGestureRecognizer;
51 |
52 | + (Class)exposedLayoutClass {
53 |
54 | return TGLExposedLayout.class;
55 | }
56 |
57 | - (instancetype)initWithCoder:(NSCoder *)aDecoder {
58 |
59 | self = [super initWithCoder:aDecoder];
60 |
61 | if (self) [self initController];
62 |
63 | return self;
64 | }
65 |
66 | - (instancetype)initWithCollectionViewLayout:(UICollectionViewLayout *)layout {
67 |
68 | NSAssert([layout isKindOfClass:TGLStackedLayout.class], @"TGLStackedViewController collection view layout is not a TGLStackedLayout");
69 |
70 | self = [super initWithCollectionViewLayout:layout];
71 |
72 | if (self) [self initController];
73 |
74 | return self;
75 | }
76 |
77 | - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
78 |
79 | self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
80 |
81 | if (self) [self initController];
82 |
83 | return self;
84 | }
85 |
86 | - (void)initController {
87 |
88 | self.installsStandardGestureForInteractiveMovement = NO;
89 |
90 | _exposedLayoutMargin = UIEdgeInsetsMake(40.0, 0.0, 0.0, 0.0);
91 | _exposedItemSize = CGSizeZero;
92 | _exposedTopOverlap = 10.0;
93 | _exposedBottomOverlap = 10.0;
94 | _exposedBottomOverlapCount = 1;
95 |
96 | _exposedPinningMode = TGLExposedLayoutPinningModeAll;
97 | _exposedTopPinningCount = -1;
98 | _exposedBottomPinningCount = -1;
99 |
100 | _exposedItemsAreCollapsible = YES;
101 |
102 | _movingItemScaleFactor = 0.95;
103 | _movingItemOnTop = YES;
104 |
105 | _collapsePanMinimumThreshold = 120.0;
106 | _collapsePanMaximumThreshold = 0.0;
107 | _collapsePinchMinimumThreshold = 0.25;
108 | }
109 |
110 | #pragma mark - View life cycle
111 |
112 | - (void)viewDidLoad {
113 |
114 | [super viewDidLoad];
115 |
116 | NSAssert([self.collectionViewLayout isKindOfClass:TGLStackedLayout.class], @"TGLStackedViewController collection view layout is not a TGLStackedLayout");
117 |
118 | self.stackedLayout = (TGLStackedLayout *)self.collectionViewLayout;
119 |
120 | if (@available(iOS 11, *)) {
121 |
122 | // Issue #45: Use UIKit's new drag and drop API for
123 | // reordering, since interactive movement
124 | // layout attributes are not applied
125 | // correctly breaking z ordering.
126 | //
127 | self.collectionView.dragDelegate = self;
128 | self.collectionView.dragInteractionEnabled = YES;
129 |
130 | self.collectionView.dropDelegate = self;
131 |
132 | } else {
133 |
134 | self.moveGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleMovePressGesture:)];
135 | self.moveGestureRecognizer.delegate = self;
136 |
137 | [self.collectionView addGestureRecognizer:self.moveGestureRecognizer];
138 | }
139 | }
140 |
141 | #pragma mark - Accessors
142 |
143 | - (UIGestureRecognizer *)collapseGestureRecognizer {
144 |
145 | if (self.exposedLayout == nil || !self.exposedItemsAreCollapsible) return nil;
146 |
147 | if (self.exposedLayout.pinningMode > TGLExposedLayoutPinningModeNone) {
148 |
149 | return self.collapsePanGestureRecognizer;
150 |
151 | } else {
152 |
153 | return self.collapsePinchGestureRecognizer;
154 | }
155 | }
156 |
157 | - (UIPanGestureRecognizer *)collapsePanGestureRecognizer {
158 |
159 | if (_collapsePanGestureRecognizer == nil) {
160 |
161 | _collapsePanGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleCollapsePanGesture:)];
162 | _collapsePanGestureRecognizer.delegate = self;
163 | }
164 |
165 | return _collapsePanGestureRecognizer;
166 | }
167 |
168 | - (UIPinchGestureRecognizer *)collapsePinchGestureRecognizer {
169 |
170 | if (_collapsePinchGestureRecognizer == nil) {
171 |
172 | _collapsePinchGestureRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handleCollapsePinchGesture:)];
173 | _collapsePinchGestureRecognizer.delegate = self;
174 | }
175 |
176 | return _collapsePinchGestureRecognizer;
177 | }
178 |
179 | - (void)setExposedItemIndexPath:(nullable NSIndexPath *)exposedItemIndexPath {
180 |
181 | [self setExposedItemIndexPath:exposedItemIndexPath animated:YES completion:nil];
182 | }
183 |
184 | - (void)setExposedItemIndexPath:(nullable NSIndexPath *)exposedItemIndexPath animated:(BOOL)animated {
185 |
186 | [self setExposedItemIndexPath:exposedItemIndexPath animated:animated completion:nil];
187 | }
188 |
189 | - (void)setExposedItemIndexPath:(nullable NSIndexPath *)exposedItemIndexPath animated:(BOOL)animated completion:(void (^)(void))completion {
190 |
191 | if (self.exposedItemIndexPath == nil && exposedItemIndexPath) {
192 |
193 | // Exposed item while none is exposed yet
194 | //
195 | self.stackedLayout.contentOffset = self.collectionView.contentOffset;
196 |
197 | TGLExposedLayout *exposedLayout = [[[self.class exposedLayoutClass] alloc] initWithExposedItemIndex:exposedItemIndexPath.item];
198 |
199 | exposedLayout.layoutMargin = self.exposedLayoutMargin;
200 | exposedLayout.itemSize = self.exposedItemSize;
201 | exposedLayout.topOverlap = self.exposedTopOverlap;
202 | exposedLayout.bottomOverlap = self.exposedBottomOverlap;
203 | exposedLayout.bottomOverlapCount = self.exposedBottomOverlapCount;
204 |
205 | exposedLayout.pinningMode = self.exposedPinningMode;
206 | exposedLayout.topPinningCount = self.exposedTopPinningCount;
207 | exposedLayout.bottomPinningCount = self.exposedBottomPinningCount;
208 |
209 | void (^layoutcompletion) (BOOL) = ^ (BOOL finished) {
210 |
211 | // NOTE: We can use strong self references here since
212 | // the cycle is broken as soon as local variable
213 | // `layoutcompletion` goes out of scope.
214 | self.stackedLayout.overwriteContentOffset = YES;
215 | self.exposedLayout = exposedLayout;
216 |
217 | self->_exposedItemIndexPath = exposedItemIndexPath;
218 |
219 | UICollectionViewCell *exposedCell = [self.collectionView cellForItemAtIndexPath:self.exposedItemIndexPath];
220 |
221 | [self addCollapseGestureRecognizerToView:exposedCell];
222 |
223 | [[UIApplication sharedApplication] endIgnoringInteractionEvents];
224 |
225 | if (completion) completion();
226 | };
227 |
228 | [[UIApplication sharedApplication] beginIgnoringInteractionEvents];
229 |
230 | if (animated) {
231 |
232 | [self.collectionView setCollectionViewLayout:exposedLayout animated:YES completion:layoutcompletion];
233 |
234 | } else {
235 |
236 | self.collectionView.collectionViewLayout = exposedLayout;
237 |
238 | layoutcompletion(YES);
239 | }
240 |
241 | } else if (self.exposedItemIndexPath && exposedItemIndexPath && (exposedItemIndexPath.item != self.exposedItemIndexPath.item || self.unexposedItemsAreSelectable)) {
242 |
243 | // We have another exposed item and we expose the new one instead
244 | //
245 | UICollectionViewCell *exposedCell = [self.collectionView cellForItemAtIndexPath:self.exposedItemIndexPath];
246 |
247 | [self removeCollapseGestureRecognizersFromView:exposedCell];
248 |
249 | TGLExposedLayout *exposedLayout = [[TGLExposedLayout alloc] initWithExposedItemIndex:exposedItemIndexPath.item];
250 |
251 | exposedLayout.layoutMargin = self.exposedLayout.layoutMargin;
252 | exposedLayout.itemSize = self.exposedLayout.itemSize;
253 | exposedLayout.topOverlap = self.exposedLayout.topOverlap;
254 | exposedLayout.bottomOverlap = self.exposedLayout.bottomOverlap;
255 | exposedLayout.bottomOverlapCount = self.exposedLayout.bottomOverlapCount;
256 |
257 | exposedLayout.pinningMode = self.exposedLayout.pinningMode;
258 | exposedLayout.topPinningCount = self.exposedLayout.topPinningCount;
259 | exposedLayout.bottomPinningCount = self.exposedLayout.bottomPinningCount;
260 |
261 | void (^layoutcompletion) (BOOL) = ^ (BOOL finished) {
262 |
263 | // NOTE: We can use strong self references here since
264 | // the cycle is broken as soon as local variable
265 | // `layoutcompletion` goes out of scope.
266 | self.exposedLayout = exposedLayout;
267 |
268 | // Mention self explicitly here to get rid of compiler warning
269 | self->_exposedItemIndexPath = exposedItemIndexPath;
270 |
271 | UICollectionViewCell *exposedCell = [self.collectionView cellForItemAtIndexPath:self.exposedItemIndexPath];
272 |
273 | [self addCollapseGestureRecognizerToView:exposedCell];
274 |
275 | [[UIApplication sharedApplication] endIgnoringInteractionEvents];
276 |
277 | if (completion) completion();
278 | };
279 |
280 | [[UIApplication sharedApplication] beginIgnoringInteractionEvents];
281 |
282 | if (animated) {
283 |
284 | [self.collectionView setCollectionViewLayout:exposedLayout animated:YES completion:layoutcompletion];
285 |
286 | } else {
287 |
288 | self.collectionView.collectionViewLayout = exposedLayout;
289 |
290 | layoutcompletion(YES);
291 | }
292 |
293 | } else if (self.exposedItemIndexPath) {
294 |
295 | // We collapse the currently exposed item because
296 | //
297 | // 1. -exposedItemIndexPath has been set to nil or
298 | // 2. we're not allowed to collapse by selecting a new item
299 | //
300 | [self.collectionView deselectItemAtIndexPath:self.exposedItemIndexPath animated:YES];
301 |
302 | UICollectionViewCell *exposedCell = [self.collectionView cellForItemAtIndexPath:self.exposedItemIndexPath];
303 |
304 | [self removeCollapseGestureRecognizersFromView:exposedCell];
305 |
306 | self.exposedLayout = nil;
307 |
308 | _exposedItemIndexPath = nil;
309 |
310 | void (^layoutcompletion) (BOOL) = ^ (BOOL finished) {
311 |
312 | // NOTE: We can use strong self references here since
313 | // the cycle is broken as soon as local variable
314 | // `layoutcompletion` goes out of scope.
315 | self.stackedLayout.overwriteContentOffset = NO;
316 |
317 | [[UIApplication sharedApplication] endIgnoringInteractionEvents];
318 |
319 | if (completion) completion();
320 | };
321 |
322 | [[UIApplication sharedApplication] beginIgnoringInteractionEvents];
323 |
324 | if (animated) {
325 |
326 | [self.collectionView setCollectionViewLayout:self.stackedLayout animated:YES completion:layoutcompletion];
327 |
328 | } else {
329 |
330 | self.collectionView.collectionViewLayout = self.stackedLayout;
331 |
332 | layoutcompletion(YES);
333 | }
334 | } else {
335 | if (completion) completion();
336 | }
337 | }
338 |
339 | - (void)resetExposedItemIndexPath {
340 |
341 | // Set -exposedItemIndexPath to `nil` w/o triggering
342 | // any layout updates as in the setters above
343 | //
344 | _exposedItemIndexPath = nil;
345 | }
346 |
347 | #pragma mark - Actions
348 |
349 | - (IBAction)handleMovePressGesture:(UILongPressGestureRecognizer *)recognizer {
350 |
351 | static CGPoint startLocation;
352 | static CGPoint targetPosition;
353 |
354 | switch (recognizer.state) {
355 |
356 | case UIGestureRecognizerStateBegan: {
357 |
358 | startLocation = [recognizer locationInView:self.collectionView];
359 |
360 | NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:startLocation];
361 |
362 | self.stackedLayout.movingItemScaleFactor = self.movingItemScaleFactor;
363 | self.stackedLayout.movingItemOnTop = self.movingItemOnTop;
364 |
365 | if (indexPath && [self.collectionView beginInteractiveMovementForItemAtIndexPath:indexPath]) {
366 |
367 | UICollectionViewCell *movingCell = [self.collectionView cellForItemAtIndexPath:indexPath];
368 |
369 | targetPosition = movingCell.center;
370 |
371 | [self.collectionView updateInteractiveMovementTargetPosition:targetPosition];
372 |
373 | self.movingIndexPath = indexPath;
374 | }
375 |
376 | break;
377 | }
378 |
379 | case UIGestureRecognizerStateChanged: {
380 |
381 | if (self.movingIndexPath) {
382 |
383 | CGPoint currentLocation = [recognizer locationInView:self.collectionView];
384 | CGPoint newTargetPosition = targetPosition;
385 |
386 | newTargetPosition.y += (currentLocation.y - startLocation.y);
387 |
388 | [self.collectionView updateInteractiveMovementTargetPosition:newTargetPosition];
389 | }
390 |
391 | break;
392 | }
393 |
394 | case UIGestureRecognizerStateEnded: {
395 |
396 | if (self.movingIndexPath) {
397 |
398 | [self.collectionView endInteractiveMovement];
399 | [self.stackedLayout invalidateLayout];
400 |
401 | self.movingIndexPath = nil;
402 | }
403 |
404 | break;
405 | }
406 |
407 | case UIGestureRecognizerStateCancelled: {
408 |
409 | if (self.movingIndexPath) {
410 |
411 | [self.collectionView cancelInteractiveMovement];
412 | [self.stackedLayout invalidateLayout];
413 |
414 | self.movingIndexPath = nil;
415 | }
416 |
417 | break;
418 | }
419 |
420 | default:
421 |
422 | break;
423 | }
424 | }
425 |
426 | - (IBAction)handleCollapsePanGesture:(UIPanGestureRecognizer *)recognizer {
427 |
428 | static CGFloat transitionMaxThreshold;
429 | static CGFloat transitionMinThreshold;
430 |
431 | switch (recognizer.state) {
432 |
433 | case UIGestureRecognizerStateBegan: {
434 |
435 | if (self.transitionLayout == nil) {
436 |
437 | UICollectionViewCell *exposedCell = [self.collectionView cellForItemAtIndexPath:self.exposedItemIndexPath];
438 |
439 | __weak typeof(self) weakSelf = self;
440 |
441 | self.transitionLayout = [self.collectionView startInteractiveTransitionToCollectionViewLayout:self.stackedLayout completion:^ (BOOL completed, BOOL finish) {
442 |
443 | if (finish) {
444 |
445 | // We have a properly installed stacked layout here
446 |
447 | [weakSelf removeCollapseGestureRecognizersFromView:exposedCell];
448 |
449 | weakSelf.stackedLayout.overwriteContentOffset = NO;
450 | weakSelf.exposedLayout = nil;
451 |
452 | // Issue #37: Do not trigger layout update, since
453 | // everthing is fine when interactive
454 | // transition finished
455 | //
456 | [weakSelf resetExposedItemIndexPath];
457 | }
458 |
459 | // Issue #37: Re-allow item interaction when
460 | // interactive transition is done
461 | //
462 | weakSelf.transitionLayout = nil;
463 | weakSelf.finishingInteractiveTransition = NO;
464 | }];
465 |
466 | transitionMaxThreshold = (self.collapsePanMaximumThreshold > 0.0) ? self.collapsePanMaximumThreshold : CGRectGetHeight(exposedCell.bounds);
467 | transitionMinThreshold = MAX(self.collapsePanMinimumThreshold, 0.0);
468 | }
469 |
470 | break;
471 | }
472 |
473 | case UIGestureRecognizerStateChanged: {
474 |
475 | if (self.transitionLayout && self.collectionView.collectionViewLayout == self.transitionLayout && !self.isFinishingInteractiveTransition) {
476 |
477 | CGPoint currentOffset = [recognizer translationInView:self.collectionView];
478 |
479 | if (currentOffset.y >= 0.0) {
480 |
481 | self.transitionLayout.transitionProgress = MIN(currentOffset.y, transitionMaxThreshold) / transitionMaxThreshold;
482 | }
483 | }
484 |
485 | break;
486 | }
487 |
488 | case UIGestureRecognizerStateEnded: {
489 |
490 | if (self.transitionLayout && self.collectionView.collectionViewLayout == self.transitionLayout && !self.isFinishingInteractiveTransition) {
491 |
492 | // Issue #37: Prevent item interaction while
493 | // interactive transition is finishing
494 | //
495 | self.finishingInteractiveTransition = YES;
496 |
497 | CGPoint currentOffset = [recognizer translationInView:self.collectionView];
498 | CGPoint currentSpeed = [recognizer velocityInView:self.collectionView];
499 |
500 | if (currentOffset.y >= transitionMinThreshold && currentSpeed.y >= 0.0) {
501 |
502 | [self.collectionView deselectItemAtIndexPath:self.exposedItemIndexPath animated:YES];
503 | [self.collectionView finishInteractiveTransition];
504 |
505 | } else {
506 |
507 | [self.collectionView cancelInteractiveTransition];
508 | }
509 | }
510 |
511 | break;
512 | }
513 |
514 | case UIGestureRecognizerStateCancelled: {
515 |
516 | if (self.transitionLayout && self.collectionView.collectionViewLayout == self.transitionLayout && !self.isFinishingInteractiveTransition) {
517 |
518 | // Issue #37: Prevent item interaction while
519 | // interactive transition is finishing
520 | //
521 | self.finishingInteractiveTransition = YES;
522 |
523 | [self.collectionView cancelInteractiveTransition];
524 | }
525 |
526 | break;
527 | }
528 |
529 | default:
530 |
531 | break;
532 | }
533 | }
534 |
535 | - (IBAction)handleCollapsePinchGesture:(UIPinchGestureRecognizer *)recognizer {
536 |
537 | static CGFloat transitionMinThreshold;
538 |
539 | switch (recognizer.state) {
540 |
541 | case UIGestureRecognizerStateBegan: {
542 |
543 | if (self.transitionLayout == nil) {
544 |
545 | __weak typeof(self) weakSelf = self;
546 |
547 | self.transitionLayout = [self.collectionView startInteractiveTransitionToCollectionViewLayout:self.stackedLayout completion:^ (BOOL completed, BOOL finish) {
548 |
549 | if (finish) {
550 |
551 | // We have a properly installed stacked layout here
552 |
553 | UICollectionViewCell *exposedCell = [self.collectionView cellForItemAtIndexPath:weakSelf.exposedItemIndexPath];
554 |
555 | [weakSelf removeCollapseGestureRecognizersFromView:exposedCell];
556 |
557 | weakSelf.stackedLayout.overwriteContentOffset = NO;
558 | weakSelf.exposedLayout = nil;
559 |
560 | // Issue #37: Do not trigger layout update, since
561 | // everthing is fine when interactive
562 | // transition finished
563 | //
564 | [weakSelf resetExposedItemIndexPath];
565 | }
566 |
567 | // Issue #37: Re-allow item selection when
568 | // interactive transition is done
569 | //
570 | weakSelf.transitionLayout = nil;
571 | weakSelf.finishingInteractiveTransition = NO;
572 | }];
573 |
574 | transitionMinThreshold = weakSelf.collapsePinchMinimumThreshold;
575 |
576 | if (transitionMinThreshold < 0.0) transitionMinThreshold = 0.0; else if (transitionMinThreshold > 1.0) transitionMinThreshold = 1.0;
577 |
578 | transitionMinThreshold = 1.0 - transitionMinThreshold;
579 | }
580 |
581 | break;
582 | }
583 |
584 | case UIGestureRecognizerStateChanged: {
585 |
586 | if (self.transitionLayout && self.collectionView.collectionViewLayout == self.transitionLayout && !self.isFinishingInteractiveTransition) {
587 |
588 | CGFloat currentScale = recognizer.scale;
589 |
590 | if (currentScale >= 0.0 && currentScale <= 1.0) {
591 |
592 | self.transitionLayout.transitionProgress = 1.0 - currentScale;
593 | }
594 | }
595 |
596 | break;
597 | }
598 |
599 | case UIGestureRecognizerStateEnded: {
600 |
601 | if (self.transitionLayout && self.collectionView.collectionViewLayout == self.transitionLayout && !self.isFinishingInteractiveTransition) {
602 |
603 | // Issue #37: Prevent item interaction while
604 | // interactive transition is finishing
605 | //
606 | self.finishingInteractiveTransition = YES;
607 |
608 | CGFloat currentScale = recognizer.scale;
609 | CGFloat currentSpeed = recognizer.velocity;
610 |
611 | if (currentScale <= transitionMinThreshold && currentSpeed <= 0.0) {
612 |
613 | [self.collectionView deselectItemAtIndexPath:self.exposedItemIndexPath animated:YES];
614 | [self.collectionView finishInteractiveTransition];
615 |
616 | } else {
617 |
618 | [self.collectionView cancelInteractiveTransition];
619 | }
620 | }
621 |
622 | break;
623 | }
624 |
625 | case UIGestureRecognizerStateCancelled: {
626 |
627 | if (self.transitionLayout && self.collectionView.collectionViewLayout == self.transitionLayout && !self.isFinishingInteractiveTransition) {
628 |
629 | // Issue #37: Prevent item interaction while
630 | // interactive transition is finishing
631 | //
632 | self.finishingInteractiveTransition = YES;
633 |
634 | [self.collectionView cancelInteractiveTransition];
635 | }
636 |
637 | break;
638 | }
639 |
640 | default:
641 |
642 | break;
643 | }
644 | }
645 |
646 | #pragma mark - UICollectionViewDelegate protocol
647 |
648 | - (BOOL)collectionView:(UICollectionView *)collectionView shouldHighlightItemAtIndexPath:(NSIndexPath *)indexPath {
649 |
650 | // When selecting unexposed items is not allowed,
651 | // prevent them from being highlighted and thus
652 | // selected by the collection view
653 | //
654 | // Issue #37: Prevent selection, too, while interactive transition is still in progress.
655 | //
656 | // NOTE: Prevent selection while drag is in progress, too.
657 | //
658 | return (self.exposedItemIndexPath == nil || indexPath.item == self.exposedItemIndexPath.item || self.unexposedItemsAreSelectable) && self.transitionLayout == nil && !self.isDragging;
659 | }
660 |
661 | - (void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(NSIndexPath *)indexPath {
662 |
663 | // When selecting unexposed items is not allowed
664 | // make sure the currently exposed item remains
665 | // selected
666 | //
667 | if (self.exposedItemIndexPath && indexPath.item == self.exposedItemIndexPath.item) {
668 |
669 | [collectionView selectItemAtIndexPath:indexPath animated:NO scrollPosition:UICollectionViewScrollPositionNone];
670 | }
671 | }
672 |
673 | - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
674 |
675 | if (self.exposedItemIndexPath && indexPath.item == self.exposedItemIndexPath.item) {
676 |
677 | self.exposedItemIndexPath = nil;
678 |
679 | } else {
680 |
681 | self.exposedItemIndexPath = indexPath;
682 | }
683 | }
684 |
685 | #pragma mark - UICollectionViewDataSource protocol
686 |
687 | - (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
688 |
689 | // Currently, only one single section is
690 | // supported, therefore MUST NOT be != 1
691 | //
692 | return 1;
693 | }
694 |
695 | - (BOOL)collectionView:(UICollectionView *)collectionView canMoveItemAtIndexPath:(NSIndexPath *)indexPath {
696 |
697 | return (self.exposedLayout == nil && [self collectionView:self.collectionView numberOfItemsInSection:0] > 1);
698 | }
699 |
700 | #pragma mark - UICollectionViewDragDelegate protocol
701 |
702 | - (NSArray *)collectionView:(UICollectionView *)collectionView itemsForBeginningDragSession:(id)session atIndexPath:(NSIndexPath *)indexPath NS_AVAILABLE_IOS(11) {
703 |
704 | if (self.exposedLayout == nil && [self collectionView:self.collectionView numberOfItemsInSection:0] > 1) {
705 |
706 | self.dragSourceIndexPath = indexPath;
707 |
708 | NSItemProvider *provider = [NSItemProvider new];
709 | UIDragItem *item = [[UIDragItem alloc] initWithItemProvider:provider];
710 |
711 | return @[item];
712 |
713 | } else {
714 |
715 | return @[];
716 | }
717 | }
718 |
719 | - (void)collectionView:(UICollectionView *)collectionView dragSessionWillBegin:(id)session NS_AVAILABLE_IOS(11) {
720 |
721 | self.dragging = YES;
722 | }
723 |
724 | - (void)collectionView:(UICollectionView *)collectionView dragSessionDidEnd:(id)session NS_AVAILABLE_IOS(11) {
725 |
726 | self.dragging = NO;
727 | }
728 |
729 | #pragma mark - UICollectionViewDropDelegate protocol
730 |
731 | - (UICollectionViewDropProposal *)collectionView:(UICollectionView *)collectionView dropSessionDidUpdate:(id)session withDestinationIndexPath:(NSIndexPath *)destinationIndexPath NS_AVAILABLE_IOS(11) {
732 |
733 | UIDropOperation operation = session.localDragSession ? UIDropOperationMove : UIDropOperationCopy;
734 |
735 | return [[UICollectionViewDropProposal alloc] initWithDropOperation:operation intent:UICollectionViewDropIntentInsertAtDestinationIndexPath];
736 | }
737 |
738 | - (void)collectionView:(UICollectionView *)collectionView performDropWithCoordinator:(id)coordinator NS_AVAILABLE_IOS(11) {
739 |
740 | // NOTE: We handle only move interactions within
741 | // this collection view here.
742 | //
743 | // Any other drop interactions, e.g. from other view
744 | // within same app or from other app, must be handled
745 | // in a subclass since they's require customized data
746 | // source updates.
747 | //
748 | id item = coordinator.items.firstObject;
749 |
750 | if (item.sourceIndexPath) {
751 |
752 | // KLUDGE: On the very first drag when dropping
753 | // item at last position `destinationIndexPath`
754 | // is (0, count) instead of (0, count -1)
755 | //
756 | NSIndexPath *destinationIndexPath = coordinator.destinationIndexPath;
757 |
758 | if (destinationIndexPath.item >= [collectionView numberOfItemsInSection:destinationIndexPath.section]) {
759 |
760 | destinationIndexPath = [NSIndexPath indexPathForItem:([collectionView numberOfItemsInSection:destinationIndexPath.section] - 1) inSection:destinationIndexPath.section];
761 | }
762 |
763 | [collectionView performBatchUpdates:^() {
764 |
765 | [collectionView deleteItemsAtIndexPaths:@[item.sourceIndexPath]];
766 | [self collectionView:collectionView moveItemAtIndexPath:item.sourceIndexPath toIndexPath:destinationIndexPath];
767 | [collectionView insertItemsAtIndexPaths:@[destinationIndexPath]];
768 |
769 | self.dragSourceIndexPath = nil;
770 |
771 | } completion:^ (BOOL finished) {
772 |
773 | [coordinator dropItem:item.dragItem toItemAtIndexPath:destinationIndexPath];
774 | }];
775 | }
776 | }
777 |
778 | - (void)collectionView:(UICollectionView *)collectionView dropSessionDidEnd:(id)session NS_AVAILABLE_IOS(11) {
779 |
780 | // When drop is at original index path, method
781 | // `-collectionView:performDropWithCoordinator:`
782 | // is not called.
783 | //
784 | if (self.dragSourceIndexPath != nil) {
785 |
786 | // KLUDGE: Reload item to force correct layout
787 | // -- esp. regarding zIndex
788 | //
789 | [self.collectionView reloadItemsAtIndexPaths:@[self.dragSourceIndexPath]];
790 | self.dragSourceIndexPath = nil;
791 | }
792 | }
793 |
794 | #pragma mark - Helpers
795 |
796 | - (void)addCollapseGestureRecognizerToView:(UIView *)view {
797 |
798 | UIGestureRecognizer *recognizer = self.collapseGestureRecognizer;
799 |
800 | if (recognizer) [view addGestureRecognizer:recognizer];
801 | }
802 |
803 | - (void)removeCollapseGestureRecognizersFromView:(UIView *)view {
804 |
805 | // Make sure the gesture recognizers are not created lazily
806 | // when removing them. Therefore use ivar to test for presence
807 | // before removing
808 | //
809 | if (_collapsePanGestureRecognizer) [view removeGestureRecognizer:self.collapsePanGestureRecognizer];
810 | if (_collapsePinchGestureRecognizer) [view removeGestureRecognizer:self.collapsePinchGestureRecognizer];
811 | }
812 |
813 | @end
814 |
--------------------------------------------------------------------------------
/TGLStackedViewExample.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 46;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 3D4FB8341D0AE5EA001F5270 /* TGLSettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D4FB8331D0AE5EA001F5270 /* TGLSettingsViewController.m */; };
11 | 3DCA5E9A1A121D7D0079EDC9 /* Launch Screen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3DCA5E991A121D7D0079EDC9 /* Launch Screen.xib */; };
12 | 3DE7BA111D0DEE7E0035A3FD /* TGLSettingOptionsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3DE7BA101D0DEE7E0035A3FD /* TGLSettingOptionsViewController.m */; };
13 | 3DF68EFE18F31B0C00387458 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3DF68EFD18F31B0C00387458 /* Foundation.framework */; };
14 | 3DF68F0018F31B0C00387458 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3DF68EFF18F31B0C00387458 /* CoreGraphics.framework */; };
15 | 3DF68F0218F31B0C00387458 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3DF68F0118F31B0C00387458 /* UIKit.framework */; };
16 | 3DF68F0818F31B0C00387458 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3DF68F0618F31B0C00387458 /* InfoPlist.strings */; };
17 | 3DF68F0A18F31B0C00387458 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 3DF68F0918F31B0C00387458 /* main.m */; };
18 | 3DF68F0E18F31B0C00387458 /* TGLAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 3DF68F0D18F31B0C00387458 /* TGLAppDelegate.m */; };
19 | 3DF68F1118F31B0C00387458 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3DF68F0F18F31B0C00387458 /* Main.storyboard */; };
20 | 3DF68F1418F31B0C00387458 /* TGLViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3DF68F1318F31B0C00387458 /* TGLViewController.m */; };
21 | 3DF68F1618F31B0C00387458 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3DF68F1518F31B0C00387458 /* Images.xcassets */; };
22 | 3DF68F3418F31C1300387458 /* TGLCollectionViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 3DF68F3318F31C1300387458 /* TGLCollectionViewCell.m */; };
23 | 3DFACA4F1D195E8C005C8F3F /* TGLBackgroundProxyView.m in Sources */ = {isa = PBXBuildFile; fileRef = 3DFACA4E1D195E8C005C8F3F /* TGLBackgroundProxyView.m */; };
24 | D1AE4A2C1E89198A006C8E16 /* TGLExposedLayout.h in Headers */ = {isa = PBXBuildFile; fileRef = 3DBF89AC190019980041CB92 /* TGLExposedLayout.h */; settings = {ATTRIBUTES = (Public, ); }; };
25 | D1AE4A2D1E89198E006C8E16 /* TGLStackedLayout.h in Headers */ = {isa = PBXBuildFile; fileRef = 3DBF89AE190019980041CB92 /* TGLStackedLayout.h */; settings = {ATTRIBUTES = (Public, ); }; };
26 | D1C2B1F71E8927B600BBB75B /* TGLExposedLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = 3DBF89AD190019980041CB92 /* TGLExposedLayout.m */; };
27 | D1C2B1F81E8927B600BBB75B /* TGLStackedLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = 3DBF89AF190019980041CB92 /* TGLStackedLayout.m */; };
28 | D1C2B1F91E8927B600BBB75B /* TGLStackedViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3DBF89B1190019980041CB92 /* TGLStackedViewController.m */; };
29 | D1FC7DC21E85A8B1003FB98A /* TGLStackedViewController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D1FC7DBB1E85A8B1003FB98A /* TGLStackedViewController.framework */; };
30 | D1FC7DC31E85A8B1003FB98A /* TGLStackedViewController.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D1FC7DBB1E85A8B1003FB98A /* TGLStackedViewController.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
31 | D1FC7DCD1E85A930003FB98A /* TGLStackedViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 3DBF89B0190019980041CB92 /* TGLStackedViewController.h */; settings = {ATTRIBUTES = (Public, ); }; };
32 | /* End PBXBuildFile section */
33 |
34 | /* Begin PBXContainerItemProxy section */
35 | D1FC7DC01E85A8B1003FB98A /* PBXContainerItemProxy */ = {
36 | isa = PBXContainerItemProxy;
37 | containerPortal = 3DF68EF218F31B0C00387458 /* Project object */;
38 | proxyType = 1;
39 | remoteGlobalIDString = D1FC7DBA1E85A8B1003FB98A;
40 | remoteInfo = TGLStackedViewController;
41 | };
42 | /* End PBXContainerItemProxy section */
43 |
44 | /* Begin PBXCopyFilesBuildPhase section */
45 | D1FC7DC71E85A8B1003FB98A /* Embed Frameworks */ = {
46 | isa = PBXCopyFilesBuildPhase;
47 | buildActionMask = 2147483647;
48 | dstPath = "";
49 | dstSubfolderSpec = 10;
50 | files = (
51 | D1FC7DC31E85A8B1003FB98A /* TGLStackedViewController.framework in Embed Frameworks */,
52 | );
53 | name = "Embed Frameworks";
54 | runOnlyForDeploymentPostprocessing = 0;
55 | };
56 | /* End PBXCopyFilesBuildPhase section */
57 |
58 | /* Begin PBXFileReference section */
59 | 3D4FB8321D0AE5EA001F5270 /* TGLSettingsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGLSettingsViewController.h; sourceTree = ""; };
60 | 3D4FB8331D0AE5EA001F5270 /* TGLSettingsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGLSettingsViewController.m; sourceTree = ""; };
61 | 3DBF89AC190019980041CB92 /* TGLExposedLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGLExposedLayout.h; sourceTree = ""; };
62 | 3DBF89AD190019980041CB92 /* TGLExposedLayout.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGLExposedLayout.m; sourceTree = ""; };
63 | 3DBF89AE190019980041CB92 /* TGLStackedLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGLStackedLayout.h; sourceTree = ""; };
64 | 3DBF89AF190019980041CB92 /* TGLStackedLayout.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGLStackedLayout.m; sourceTree = ""; };
65 | 3DBF89B0190019980041CB92 /* TGLStackedViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGLStackedViewController.h; sourceTree = ""; };
66 | 3DBF89B1190019980041CB92 /* TGLStackedViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGLStackedViewController.m; sourceTree = ""; };
67 | 3DCA5E991A121D7D0079EDC9 /* Launch Screen.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = "Launch Screen.xib"; sourceTree = ""; };
68 | 3DE7BA101D0DEE7E0035A3FD /* TGLSettingOptionsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGLSettingOptionsViewController.m; sourceTree = ""; };
69 | 3DE7BA121D0DEE870035A3FD /* TGLSettingOptionsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGLSettingOptionsViewController.h; sourceTree = ""; };
70 | 3DF68EFA18F31B0C00387458 /* TGLStackedViewExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TGLStackedViewExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
71 | 3DF68EFD18F31B0C00387458 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
72 | 3DF68EFF18F31B0C00387458 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; };
73 | 3DF68F0118F31B0C00387458 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; };
74 | 3DF68F0518F31B0C00387458 /* TGLStackedViewExample-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "TGLStackedViewExample-Info.plist"; sourceTree = ""; };
75 | 3DF68F0718F31B0C00387458 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; };
76 | 3DF68F0918F31B0C00387458 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; };
77 | 3DF68F0B18F31B0C00387458 /* TGLStackedViewExample-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "TGLStackedViewExample-Prefix.pch"; sourceTree = ""; };
78 | 3DF68F0C18F31B0C00387458 /* TGLAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TGLAppDelegate.h; sourceTree = ""; };
79 | 3DF68F0D18F31B0C00387458 /* TGLAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TGLAppDelegate.m; sourceTree = ""; };
80 | 3DF68F1018F31B0C00387458 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
81 | 3DF68F1218F31B0C00387458 /* TGLViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TGLViewController.h; sourceTree = ""; };
82 | 3DF68F1318F31B0C00387458 /* TGLViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TGLViewController.m; sourceTree = ""; };
83 | 3DF68F1518F31B0C00387458 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; };
84 | 3DF68F3218F31C1300387458 /* TGLCollectionViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGLCollectionViewCell.h; sourceTree = ""; };
85 | 3DF68F3318F31C1300387458 /* TGLCollectionViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGLCollectionViewCell.m; sourceTree = ""; };
86 | 3DFACA4D1D195E8C005C8F3F /* TGLBackgroundProxyView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGLBackgroundProxyView.h; sourceTree = ""; };
87 | 3DFACA4E1D195E8C005C8F3F /* TGLBackgroundProxyView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGLBackgroundProxyView.m; sourceTree = ""; };
88 | D1FC7DBB1E85A8B1003FB98A /* TGLStackedViewController.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TGLStackedViewController.framework; sourceTree = BUILT_PRODUCTS_DIR; };
89 | D1FC7DBE1E85A8B1003FB98A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
90 | /* End PBXFileReference section */
91 |
92 | /* Begin PBXFrameworksBuildPhase section */
93 | 3DF68EF718F31B0C00387458 /* Frameworks */ = {
94 | isa = PBXFrameworksBuildPhase;
95 | buildActionMask = 2147483647;
96 | files = (
97 | 3DF68F0018F31B0C00387458 /* CoreGraphics.framework in Frameworks */,
98 | 3DF68F0218F31B0C00387458 /* UIKit.framework in Frameworks */,
99 | 3DF68EFE18F31B0C00387458 /* Foundation.framework in Frameworks */,
100 | D1FC7DC21E85A8B1003FB98A /* TGLStackedViewController.framework in Frameworks */,
101 | );
102 | runOnlyForDeploymentPostprocessing = 0;
103 | };
104 | D1FC7DB71E85A8B1003FB98A /* Frameworks */ = {
105 | isa = PBXFrameworksBuildPhase;
106 | buildActionMask = 2147483647;
107 | files = (
108 | );
109 | runOnlyForDeploymentPostprocessing = 0;
110 | };
111 | /* End PBXFrameworksBuildPhase section */
112 |
113 | /* Begin PBXGroup section */
114 | 3DBF89AB190019980041CB92 /* TGLStackedViewController */ = {
115 | isa = PBXGroup;
116 | children = (
117 | D1FC7DBE1E85A8B1003FB98A /* Info.plist */,
118 | 3DBF89AC190019980041CB92 /* TGLExposedLayout.h */,
119 | 3DBF89AD190019980041CB92 /* TGLExposedLayout.m */,
120 | 3DBF89AE190019980041CB92 /* TGLStackedLayout.h */,
121 | 3DBF89AF190019980041CB92 /* TGLStackedLayout.m */,
122 | 3DBF89B0190019980041CB92 /* TGLStackedViewController.h */,
123 | 3DBF89B1190019980041CB92 /* TGLStackedViewController.m */,
124 | );
125 | path = TGLStackedViewController;
126 | sourceTree = "";
127 | };
128 | 3DF68EF118F31B0C00387458 = {
129 | isa = PBXGroup;
130 | children = (
131 | 3DBF89AB190019980041CB92 /* TGLStackedViewController */,
132 | 3DF68F0318F31B0C00387458 /* TGLStackedViewExample */,
133 | 3DF68EFC18F31B0C00387458 /* Frameworks */,
134 | 3DF68EFB18F31B0C00387458 /* Products */,
135 | );
136 | sourceTree = "";
137 | };
138 | 3DF68EFB18F31B0C00387458 /* Products */ = {
139 | isa = PBXGroup;
140 | children = (
141 | 3DF68EFA18F31B0C00387458 /* TGLStackedViewExample.app */,
142 | D1FC7DBB1E85A8B1003FB98A /* TGLStackedViewController.framework */,
143 | );
144 | name = Products;
145 | sourceTree = "";
146 | };
147 | 3DF68EFC18F31B0C00387458 /* Frameworks */ = {
148 | isa = PBXGroup;
149 | children = (
150 | 3DF68EFD18F31B0C00387458 /* Foundation.framework */,
151 | 3DF68EFF18F31B0C00387458 /* CoreGraphics.framework */,
152 | 3DF68F0118F31B0C00387458 /* UIKit.framework */,
153 | );
154 | name = Frameworks;
155 | sourceTree = "";
156 | };
157 | 3DF68F0318F31B0C00387458 /* TGLStackedViewExample */ = {
158 | isa = PBXGroup;
159 | children = (
160 | 3DF68F0C18F31B0C00387458 /* TGLAppDelegate.h */,
161 | 3DF68F0D18F31B0C00387458 /* TGLAppDelegate.m */,
162 | 3DFACA4D1D195E8C005C8F3F /* TGLBackgroundProxyView.h */,
163 | 3DFACA4E1D195E8C005C8F3F /* TGLBackgroundProxyView.m */,
164 | 3DF68F0F18F31B0C00387458 /* Main.storyboard */,
165 | 3DF68F3218F31C1300387458 /* TGLCollectionViewCell.h */,
166 | 3DF68F3318F31C1300387458 /* TGLCollectionViewCell.m */,
167 | 3D4FB8321D0AE5EA001F5270 /* TGLSettingsViewController.h */,
168 | 3D4FB8331D0AE5EA001F5270 /* TGLSettingsViewController.m */,
169 | 3DE7BA121D0DEE870035A3FD /* TGLSettingOptionsViewController.h */,
170 | 3DE7BA101D0DEE7E0035A3FD /* TGLSettingOptionsViewController.m */,
171 | 3DF68F1218F31B0C00387458 /* TGLViewController.h */,
172 | 3DF68F1318F31B0C00387458 /* TGLViewController.m */,
173 | 3DF68F1518F31B0C00387458 /* Images.xcassets */,
174 | 3DF68F0418F31B0C00387458 /* Supporting Files */,
175 | );
176 | path = TGLStackedViewExample;
177 | sourceTree = "";
178 | };
179 | 3DF68F0418F31B0C00387458 /* Supporting Files */ = {
180 | isa = PBXGroup;
181 | children = (
182 | 3DCA5E991A121D7D0079EDC9 /* Launch Screen.xib */,
183 | 3DF68F0518F31B0C00387458 /* TGLStackedViewExample-Info.plist */,
184 | 3DF68F0618F31B0C00387458 /* InfoPlist.strings */,
185 | 3DF68F0918F31B0C00387458 /* main.m */,
186 | 3DF68F0B18F31B0C00387458 /* TGLStackedViewExample-Prefix.pch */,
187 | );
188 | name = "Supporting Files";
189 | sourceTree = "";
190 | };
191 | /* End PBXGroup section */
192 |
193 | /* Begin PBXHeadersBuildPhase section */
194 | D1FC7DB81E85A8B1003FB98A /* Headers */ = {
195 | isa = PBXHeadersBuildPhase;
196 | buildActionMask = 2147483647;
197 | files = (
198 | D1FC7DCD1E85A930003FB98A /* TGLStackedViewController.h in Headers */,
199 | D1AE4A2C1E89198A006C8E16 /* TGLExposedLayout.h in Headers */,
200 | D1AE4A2D1E89198E006C8E16 /* TGLStackedLayout.h in Headers */,
201 | );
202 | runOnlyForDeploymentPostprocessing = 0;
203 | };
204 | /* End PBXHeadersBuildPhase section */
205 |
206 | /* Begin PBXNativeTarget section */
207 | 3DF68EF918F31B0C00387458 /* TGLStackedViewExample */ = {
208 | isa = PBXNativeTarget;
209 | buildConfigurationList = 3DF68F2C18F31B0C00387458 /* Build configuration list for PBXNativeTarget "TGLStackedViewExample" */;
210 | buildPhases = (
211 | 3DF68EF618F31B0C00387458 /* Sources */,
212 | 3DF68EF718F31B0C00387458 /* Frameworks */,
213 | 3DF68EF818F31B0C00387458 /* Resources */,
214 | D1FC7DC71E85A8B1003FB98A /* Embed Frameworks */,
215 | );
216 | buildRules = (
217 | );
218 | dependencies = (
219 | D1FC7DC11E85A8B1003FB98A /* PBXTargetDependency */,
220 | );
221 | name = TGLStackedViewExample;
222 | productName = CollectionTest;
223 | productReference = 3DF68EFA18F31B0C00387458 /* TGLStackedViewExample.app */;
224 | productType = "com.apple.product-type.application";
225 | };
226 | D1FC7DBA1E85A8B1003FB98A /* TGLStackedViewController */ = {
227 | isa = PBXNativeTarget;
228 | buildConfigurationList = D1FC7DC61E85A8B1003FB98A /* Build configuration list for PBXNativeTarget "TGLStackedViewController" */;
229 | buildPhases = (
230 | D1FC7DB61E85A8B1003FB98A /* Sources */,
231 | D1FC7DB71E85A8B1003FB98A /* Frameworks */,
232 | D1FC7DB81E85A8B1003FB98A /* Headers */,
233 | D1FC7DB91E85A8B1003FB98A /* Resources */,
234 | );
235 | buildRules = (
236 | );
237 | dependencies = (
238 | );
239 | name = TGLStackedViewController;
240 | productName = TGLStackedViewController;
241 | productReference = D1FC7DBB1E85A8B1003FB98A /* TGLStackedViewController.framework */;
242 | productType = "com.apple.product-type.framework";
243 | };
244 | /* End PBXNativeTarget section */
245 |
246 | /* Begin PBXProject section */
247 | 3DF68EF218F31B0C00387458 /* Project object */ = {
248 | isa = PBXProject;
249 | attributes = {
250 | CLASSPREFIX = TGL;
251 | LastUpgradeCheck = 1100;
252 | ORGANIZATIONNAME = "Tim Gleue • interactive software";
253 | TargetAttributes = {
254 | D1FC7DBA1E85A8B1003FB98A = {
255 | CreatedOnToolsVersion = 8.2.1;
256 | ProvisioningStyle = Automatic;
257 | };
258 | };
259 | };
260 | buildConfigurationList = 3DF68EF518F31B0C00387458 /* Build configuration list for PBXProject "TGLStackedViewExample" */;
261 | compatibilityVersion = "Xcode 3.2";
262 | developmentRegion = en;
263 | hasScannedForEncodings = 0;
264 | knownRegions = (
265 | en,
266 | Base,
267 | );
268 | mainGroup = 3DF68EF118F31B0C00387458;
269 | productRefGroup = 3DF68EFB18F31B0C00387458 /* Products */;
270 | projectDirPath = "";
271 | projectRoot = "";
272 | targets = (
273 | 3DF68EF918F31B0C00387458 /* TGLStackedViewExample */,
274 | D1FC7DBA1E85A8B1003FB98A /* TGLStackedViewController */,
275 | );
276 | };
277 | /* End PBXProject section */
278 |
279 | /* Begin PBXResourcesBuildPhase section */
280 | 3DF68EF818F31B0C00387458 /* Resources */ = {
281 | isa = PBXResourcesBuildPhase;
282 | buildActionMask = 2147483647;
283 | files = (
284 | 3DF68F1618F31B0C00387458 /* Images.xcassets in Resources */,
285 | 3DCA5E9A1A121D7D0079EDC9 /* Launch Screen.xib in Resources */,
286 | 3DF68F0818F31B0C00387458 /* InfoPlist.strings in Resources */,
287 | 3DF68F1118F31B0C00387458 /* Main.storyboard in Resources */,
288 | );
289 | runOnlyForDeploymentPostprocessing = 0;
290 | };
291 | D1FC7DB91E85A8B1003FB98A /* Resources */ = {
292 | isa = PBXResourcesBuildPhase;
293 | buildActionMask = 2147483647;
294 | files = (
295 | );
296 | runOnlyForDeploymentPostprocessing = 0;
297 | };
298 | /* End PBXResourcesBuildPhase section */
299 |
300 | /* Begin PBXSourcesBuildPhase section */
301 | 3DF68EF618F31B0C00387458 /* Sources */ = {
302 | isa = PBXSourcesBuildPhase;
303 | buildActionMask = 2147483647;
304 | files = (
305 | 3DFACA4F1D195E8C005C8F3F /* TGLBackgroundProxyView.m in Sources */,
306 | 3DE7BA111D0DEE7E0035A3FD /* TGLSettingOptionsViewController.m in Sources */,
307 | 3DF68F0A18F31B0C00387458 /* main.m in Sources */,
308 | 3DF68F3418F31C1300387458 /* TGLCollectionViewCell.m in Sources */,
309 | 3DF68F0E18F31B0C00387458 /* TGLAppDelegate.m in Sources */,
310 | 3D4FB8341D0AE5EA001F5270 /* TGLSettingsViewController.m in Sources */,
311 | 3DF68F1418F31B0C00387458 /* TGLViewController.m in Sources */,
312 | );
313 | runOnlyForDeploymentPostprocessing = 0;
314 | };
315 | D1FC7DB61E85A8B1003FB98A /* Sources */ = {
316 | isa = PBXSourcesBuildPhase;
317 | buildActionMask = 2147483647;
318 | files = (
319 | D1C2B1F91E8927B600BBB75B /* TGLStackedViewController.m in Sources */,
320 | D1C2B1F71E8927B600BBB75B /* TGLExposedLayout.m in Sources */,
321 | D1C2B1F81E8927B600BBB75B /* TGLStackedLayout.m in Sources */,
322 | );
323 | runOnlyForDeploymentPostprocessing = 0;
324 | };
325 | /* End PBXSourcesBuildPhase section */
326 |
327 | /* Begin PBXTargetDependency section */
328 | D1FC7DC11E85A8B1003FB98A /* PBXTargetDependency */ = {
329 | isa = PBXTargetDependency;
330 | target = D1FC7DBA1E85A8B1003FB98A /* TGLStackedViewController */;
331 | targetProxy = D1FC7DC01E85A8B1003FB98A /* PBXContainerItemProxy */;
332 | };
333 | /* End PBXTargetDependency section */
334 |
335 | /* Begin PBXVariantGroup section */
336 | 3DF68F0618F31B0C00387458 /* InfoPlist.strings */ = {
337 | isa = PBXVariantGroup;
338 | children = (
339 | 3DF68F0718F31B0C00387458 /* en */,
340 | );
341 | name = InfoPlist.strings;
342 | sourceTree = "";
343 | };
344 | 3DF68F0F18F31B0C00387458 /* Main.storyboard */ = {
345 | isa = PBXVariantGroup;
346 | children = (
347 | 3DF68F1018F31B0C00387458 /* Base */,
348 | );
349 | name = Main.storyboard;
350 | sourceTree = "";
351 | };
352 | /* End PBXVariantGroup section */
353 |
354 | /* Begin XCBuildConfiguration section */
355 | 3DF68F2A18F31B0C00387458 /* Debug */ = {
356 | isa = XCBuildConfiguration;
357 | buildSettings = {
358 | ALWAYS_SEARCH_USER_PATHS = NO;
359 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = 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 | ENABLE_STRICT_OBJC_MSGSEND = YES;
386 | ENABLE_TESTABILITY = YES;
387 | GCC_C_LANGUAGE_STANDARD = gnu99;
388 | GCC_DYNAMIC_NO_PIC = NO;
389 | GCC_NO_COMMON_BLOCKS = YES;
390 | GCC_OPTIMIZATION_LEVEL = 0;
391 | GCC_PREPROCESSOR_DEFINITIONS = (
392 | "DEBUG=1",
393 | "$(inherited)",
394 | );
395 | GCC_SYMBOLS_PRIVATE_EXTERN = NO;
396 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
397 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
398 | GCC_WARN_UNDECLARED_SELECTOR = YES;
399 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
400 | GCC_WARN_UNUSED_FUNCTION = YES;
401 | GCC_WARN_UNUSED_VARIABLE = YES;
402 | IPHONEOS_DEPLOYMENT_TARGET = 9.0;
403 | ONLY_ACTIVE_ARCH = YES;
404 | SDKROOT = iphoneos;
405 | };
406 | name = Debug;
407 | };
408 | 3DF68F2B18F31B0C00387458 /* Release */ = {
409 | isa = XCBuildConfiguration;
410 | buildSettings = {
411 | ALWAYS_SEARCH_USER_PATHS = NO;
412 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
413 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
414 | CLANG_CXX_LIBRARY = "libc++";
415 | CLANG_ENABLE_MODULES = YES;
416 | CLANG_ENABLE_OBJC_ARC = YES;
417 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
418 | CLANG_WARN_BOOL_CONVERSION = YES;
419 | CLANG_WARN_COMMA = YES;
420 | CLANG_WARN_CONSTANT_CONVERSION = YES;
421 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
422 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
423 | CLANG_WARN_EMPTY_BODY = YES;
424 | CLANG_WARN_ENUM_CONVERSION = YES;
425 | CLANG_WARN_INFINITE_RECURSION = YES;
426 | CLANG_WARN_INT_CONVERSION = YES;
427 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
428 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
429 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
430 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
431 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
432 | CLANG_WARN_STRICT_PROTOTYPES = YES;
433 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
434 | CLANG_WARN_UNREACHABLE_CODE = YES;
435 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
436 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
437 | COPY_PHASE_STRIP = YES;
438 | ENABLE_NS_ASSERTIONS = NO;
439 | ENABLE_STRICT_OBJC_MSGSEND = YES;
440 | GCC_C_LANGUAGE_STANDARD = gnu99;
441 | GCC_NO_COMMON_BLOCKS = YES;
442 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
443 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
444 | GCC_WARN_UNDECLARED_SELECTOR = YES;
445 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
446 | GCC_WARN_UNUSED_FUNCTION = YES;
447 | GCC_WARN_UNUSED_VARIABLE = YES;
448 | IPHONEOS_DEPLOYMENT_TARGET = 9.0;
449 | SDKROOT = iphoneos;
450 | VALIDATE_PRODUCT = YES;
451 | };
452 | name = Release;
453 | };
454 | 3DF68F2D18F31B0C00387458 /* Debug */ = {
455 | isa = XCBuildConfiguration;
456 | buildSettings = {
457 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
458 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = "";
459 | CODE_SIGN_IDENTITY = "iPhone Developer";
460 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
461 | CURRENT_PROJECT_VERSION = 2.2.4;
462 | GCC_PRECOMPILE_PREFIX_HEADER = YES;
463 | GCC_PREFIX_HEADER = "TGLStackedViewExample/TGLStackedViewExample-Prefix.pch";
464 | INFOPLIST_FILE = "$(SRCROOT)/TGLStackedViewExample/TGLStackedViewExample-Info.plist";
465 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
466 | PRODUCT_BUNDLE_IDENTIFIER = "com.gleue-interactive.${PRODUCT_NAME:rfc1034identifier}";
467 | PRODUCT_NAME = TGLStackedViewExample;
468 | PROVISIONING_PROFILE = "";
469 | WRAPPER_EXTENSION = app;
470 | };
471 | name = Debug;
472 | };
473 | 3DF68F2E18F31B0C00387458 /* Release */ = {
474 | isa = XCBuildConfiguration;
475 | buildSettings = {
476 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
477 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = "";
478 | CODE_SIGN_IDENTITY = "iPhone Developer";
479 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
480 | CURRENT_PROJECT_VERSION = 2.2.4;
481 | GCC_PRECOMPILE_PREFIX_HEADER = YES;
482 | GCC_PREFIX_HEADER = "TGLStackedViewExample/TGLStackedViewExample-Prefix.pch";
483 | INFOPLIST_FILE = "$(SRCROOT)/TGLStackedViewExample/TGLStackedViewExample-Info.plist";
484 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
485 | PRODUCT_BUNDLE_IDENTIFIER = "com.gleue-interactive.${PRODUCT_NAME:rfc1034identifier}";
486 | PRODUCT_NAME = TGLStackedViewExample;
487 | PROVISIONING_PROFILE = "";
488 | WRAPPER_EXTENSION = app;
489 | };
490 | name = Release;
491 | };
492 | D1FC7DC41E85A8B1003FB98A /* Debug */ = {
493 | isa = XCBuildConfiguration;
494 | buildSettings = {
495 | CLANG_ANALYZER_NONNULL = YES;
496 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
497 | CODE_SIGN_IDENTITY = "";
498 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
499 | CURRENT_PROJECT_VERSION = 1;
500 | DEBUG_INFORMATION_FORMAT = dwarf;
501 | DEFINES_MODULE = YES;
502 | DYLIB_COMPATIBILITY_VERSION = 1;
503 | DYLIB_CURRENT_VERSION = 1;
504 | DYLIB_INSTALL_NAME_BASE = "@rpath";
505 | INFOPLIST_FILE = TGLStackedViewController/Info.plist;
506 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
507 | IPHONEOS_DEPLOYMENT_TARGET = 10.2;
508 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
509 | MTL_ENABLE_DEBUG_INFO = YES;
510 | PRODUCT_BUNDLE_IDENTIFIER = "com.gleue-interactive.TGLStackedViewController";
511 | PRODUCT_NAME = "$(TARGET_NAME)";
512 | SKIP_INSTALL = YES;
513 | TARGETED_DEVICE_FAMILY = "1,2";
514 | VERSIONING_SYSTEM = "apple-generic";
515 | VERSION_INFO_PREFIX = "";
516 | };
517 | name = Debug;
518 | };
519 | D1FC7DC51E85A8B1003FB98A /* Release */ = {
520 | isa = XCBuildConfiguration;
521 | buildSettings = {
522 | CLANG_ANALYZER_NONNULL = YES;
523 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
524 | CODE_SIGN_IDENTITY = "";
525 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
526 | COPY_PHASE_STRIP = NO;
527 | CURRENT_PROJECT_VERSION = 1;
528 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
529 | DEFINES_MODULE = YES;
530 | DYLIB_COMPATIBILITY_VERSION = 1;
531 | DYLIB_CURRENT_VERSION = 1;
532 | DYLIB_INSTALL_NAME_BASE = "@rpath";
533 | INFOPLIST_FILE = TGLStackedViewController/Info.plist;
534 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
535 | IPHONEOS_DEPLOYMENT_TARGET = 10.2;
536 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
537 | MTL_ENABLE_DEBUG_INFO = NO;
538 | PRODUCT_BUNDLE_IDENTIFIER = "com.gleue-interactive.TGLStackedViewController";
539 | PRODUCT_NAME = "$(TARGET_NAME)";
540 | SKIP_INSTALL = YES;
541 | TARGETED_DEVICE_FAMILY = "1,2";
542 | VERSIONING_SYSTEM = "apple-generic";
543 | VERSION_INFO_PREFIX = "";
544 | };
545 | name = Release;
546 | };
547 | /* End XCBuildConfiguration section */
548 |
549 | /* Begin XCConfigurationList section */
550 | 3DF68EF518F31B0C00387458 /* Build configuration list for PBXProject "TGLStackedViewExample" */ = {
551 | isa = XCConfigurationList;
552 | buildConfigurations = (
553 | 3DF68F2A18F31B0C00387458 /* Debug */,
554 | 3DF68F2B18F31B0C00387458 /* Release */,
555 | );
556 | defaultConfigurationIsVisible = 0;
557 | defaultConfigurationName = Release;
558 | };
559 | 3DF68F2C18F31B0C00387458 /* Build configuration list for PBXNativeTarget "TGLStackedViewExample" */ = {
560 | isa = XCConfigurationList;
561 | buildConfigurations = (
562 | 3DF68F2D18F31B0C00387458 /* Debug */,
563 | 3DF68F2E18F31B0C00387458 /* Release */,
564 | );
565 | defaultConfigurationIsVisible = 0;
566 | defaultConfigurationName = Release;
567 | };
568 | D1FC7DC61E85A8B1003FB98A /* Build configuration list for PBXNativeTarget "TGLStackedViewController" */ = {
569 | isa = XCConfigurationList;
570 | buildConfigurations = (
571 | D1FC7DC41E85A8B1003FB98A /* Debug */,
572 | D1FC7DC51E85A8B1003FB98A /* Release */,
573 | );
574 | defaultConfigurationIsVisible = 0;
575 | defaultConfigurationName = Release;
576 | };
577 | /* End XCConfigurationList section */
578 | };
579 | rootObject = 3DF68EF218F31B0C00387458 /* Project object */;
580 | }
581 |
--------------------------------------------------------------------------------
/TGLStackedViewExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/TGLStackedViewExample.xcodeproj/xcshareddata/xcschemes/TGLStackedViewController.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
44 |
50 |
51 |
52 |
53 |
59 |
60 |
66 |
67 |
68 |
69 |
71 |
72 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/TGLStackedViewExample/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
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 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
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 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
--------------------------------------------------------------------------------
/TGLStackedViewExample/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "20x20",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "20x20",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "29x29",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "29x29",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "40x40",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "40x40",
31 | "scale" : "3x"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "size" : "60x60",
36 | "scale" : "2x"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "size" : "60x60",
41 | "scale" : "3x"
42 | },
43 | {
44 | "idiom" : "ios-marketing",
45 | "size" : "1024x1024",
46 | "scale" : "1x"
47 | }
48 | ],
49 | "info" : {
50 | "version" : 1,
51 | "author" : "xcode"
52 | }
53 | }
--------------------------------------------------------------------------------
/TGLStackedViewExample/Images.xcassets/Background.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "resizing" : {
5 | "mode" : "9-part",
6 | "center" : {
7 | "mode" : "tile",
8 | "width" : 1,
9 | "height" : 1
10 | },
11 | "cap-insets" : {
12 | "bottom" : 10,
13 | "top" : 10,
14 | "right" : 10,
15 | "left" : 10
16 | }
17 | },
18 | "idiom" : "universal",
19 | "filename" : "background.png",
20 | "scale" : "1x"
21 | },
22 | {
23 | "resizing" : {
24 | "mode" : "9-part",
25 | "center" : {
26 | "mode" : "tile",
27 | "width" : 1,
28 | "height" : 1
29 | },
30 | "cap-insets" : {
31 | "bottom" : 21,
32 | "top" : 20,
33 | "right" : 21,
34 | "left" : 20
35 | }
36 | },
37 | "idiom" : "universal",
38 | "filename" : "background@2x.png",
39 | "scale" : "2x"
40 | },
41 | {
42 | "idiom" : "universal",
43 | "scale" : "3x"
44 | }
45 | ],
46 | "info" : {
47 | "version" : 1,
48 | "author" : "xcode"
49 | },
50 | "properties" : {
51 | "template-rendering-intent" : "template"
52 | }
53 | }
--------------------------------------------------------------------------------
/TGLStackedViewExample/Images.xcassets/Background.imageset/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gleue/TGLStackedViewController/dc227eab8d996c15b0a4baffd8771b443b917156/TGLStackedViewExample/Images.xcassets/Background.imageset/background.png
--------------------------------------------------------------------------------
/TGLStackedViewExample/Images.xcassets/Background.imageset/background@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gleue/TGLStackedViewController/dc227eab8d996c15b0a4baffd8771b443b917156/TGLStackedViewExample/Images.xcassets/Background.imageset/background@2x.png
--------------------------------------------------------------------------------
/TGLStackedViewExample/Images.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/TGLStackedViewExample/Launch Screen.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/TGLStackedViewExample/TGLAppDelegate.h:
--------------------------------------------------------------------------------
1 | //
2 | // TGLAppDelegate.h
3 | // TGLStackedViewExample
4 | //
5 | // Created by Tim Gleue on 07.04.14.
6 | // Copyright (c) 2014-2019 Tim Gleue ( http://gleue-interactive.com )
7 | //
8 | // Permission is hereby granted, free of charge, to any person obtaining a copy
9 | // of this software and associated documentation files (the "Software"), to deal
10 | // in the Software without restriction, including without limitation the rights
11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | // copies of the Software, and to permit persons to whom the Software is
13 | // furnished to do so, subject to the following conditions:
14 | //
15 | // The above copyright notice and this permission notice shall be included in
16 | // all copies or substantial portions of the Software.
17 | //
18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 | // THE SOFTWARE.
25 |
26 | #import
27 |
28 | @interface TGLAppDelegate : UIResponder
29 |
30 | @property (strong, nonatomic) UIWindow *window;
31 |
32 | @end
33 |
--------------------------------------------------------------------------------
/TGLStackedViewExample/TGLAppDelegate.m:
--------------------------------------------------------------------------------
1 | //
2 | // TGLAppDelegate.m
3 | // TGLStackedViewExample
4 | //
5 | // Created by Tim Gleue on 07.04.14.
6 | // Copyright (c) 2014-2019 Tim Gleue ( http://gleue-interactive.com )
7 | //
8 | // Permission is hereby granted, free of charge, to any person obtaining a copy
9 | // of this software and associated documentation files (the "Software"), to deal
10 | // in the Software without restriction, including without limitation the rights
11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | // copies of the Software, and to permit persons to whom the Software is
13 | // furnished to do so, subject to the following conditions:
14 | //
15 | // The above copyright notice and this permission notice shall be included in
16 | // all copies or substantial portions of the Software.
17 | //
18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 | // THE SOFTWARE.
25 |
26 | #import "TGLAppDelegate.h"
27 |
28 | @implementation TGLAppDelegate
29 |
30 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
31 | {
32 | // Override point for customization after application launch.
33 | return YES;
34 | }
35 |
36 | - (void)applicationWillResignActive:(UIApplication *)application
37 | {
38 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
39 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
40 | }
41 |
42 | - (void)applicationDidEnterBackground:(UIApplication *)application
43 | {
44 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
45 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
46 | }
47 |
48 | - (void)applicationWillEnterForeground:(UIApplication *)application
49 | {
50 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
51 | }
52 |
53 | - (void)applicationDidBecomeActive:(UIApplication *)application
54 | {
55 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
56 | }
57 |
58 | - (void)applicationWillTerminate:(UIApplication *)application
59 | {
60 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
61 | }
62 |
63 | @end
64 |
--------------------------------------------------------------------------------
/TGLStackedViewExample/TGLBackgroundProxyView.h:
--------------------------------------------------------------------------------
1 | //
2 | // TGLBackgroundProxyView.h
3 | // TGLStackedViewExample
4 | //
5 | // Created by Tim Gleue on 21.06.16.
6 | // Copyright © 2016-2019 Tim Gleue • interactive software. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | @interface TGLBackgroundProxyView : UIView
12 |
13 | @property (nonatomic, weak) UIView *targetView;
14 |
15 | @end
16 |
--------------------------------------------------------------------------------
/TGLStackedViewExample/TGLBackgroundProxyView.m:
--------------------------------------------------------------------------------
1 | //
2 | // TGLBackgroundProxyView.m
3 | // TGLStackedViewExample
4 | //
5 | // Created by Tim Gleue on 21.06.16.
6 | // Copyright © 2016-2019 Tim Gleue • interactive software. All rights reserved.
7 | //
8 |
9 | #import "TGLBackgroundProxyView.h"
10 |
11 | @implementation TGLBackgroundProxyView
12 |
13 | - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
14 |
15 | // Return target view subview during hit testing,
16 | // thus making unreachable target interactable
17 | //
18 | return [self.targetView hitTest:point withEvent:event];
19 | }
20 |
21 | @end
22 |
--------------------------------------------------------------------------------
/TGLStackedViewExample/TGLCollectionViewCell.h:
--------------------------------------------------------------------------------
1 | //
2 | // TGLCollectionViewCell.h
3 | // TGLStackedViewExample
4 | //
5 | // Created by Tim Gleue on 07.04.14.
6 | // Copyright (c) 2014-2019 Tim Gleue ( http://gleue-interactive.com )
7 | //
8 | // Permission is hereby granted, free of charge, to any person obtaining a copy
9 | // of this software and associated documentation files (the "Software"), to deal
10 | // in the Software without restriction, including without limitation the rights
11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | // copies of the Software, and to permit persons to whom the Software is
13 | // furnished to do so, subject to the following conditions:
14 | //
15 | // The above copyright notice and this permission notice shall be included in
16 | // all copies or substantial portions of the Software.
17 | //
18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 | // THE SOFTWARE.
25 |
26 | #import
27 |
28 | @interface TGLCollectionViewCell : UICollectionViewCell
29 |
30 | @property (copy, nonatomic) NSString *title;
31 | @property (copy, nonatomic) UIColor *color;
32 |
33 | @end
34 |
--------------------------------------------------------------------------------
/TGLStackedViewExample/TGLCollectionViewCell.m:
--------------------------------------------------------------------------------
1 | //
2 | // TGLCollectionViewCell.m
3 | // TGLStackedViewExample
4 | //
5 | // Created by Tim Gleue on 07.04.14.
6 | // Copyright (c) 2014-2019 Tim Gleue ( http://gleue-interactive.com )
7 | //
8 | // Permission is hereby granted, free of charge, to any person obtaining a copy
9 | // of this software and associated documentation files (the "Software"), to deal
10 | // in the Software without restriction, including without limitation the rights
11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | // copies of the Software, and to permit persons to whom the Software is
13 | // furnished to do so, subject to the following conditions:
14 | //
15 | // The above copyright notice and this permission notice shall be included in
16 | // all copies or substantial portions of the Software.
17 | //
18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 | // THE SOFTWARE.
25 |
26 | #import
27 |
28 | #import "TGLCollectionViewCell.h"
29 |
30 | @interface TGLCollectionViewCell ()
31 |
32 | @property (weak, nonatomic) IBOutlet UIImageView *imageView;
33 | @property (weak, nonatomic) IBOutlet UILabel *nameLabel;
34 |
35 | @end
36 |
37 | @implementation TGLCollectionViewCell
38 |
39 | - (void)awakeFromNib {
40 |
41 | [super awakeFromNib];
42 |
43 | self.imageView.tintColor = self.color;
44 | self.imageView.layer.borderColor = self.nameLabel.highlightedTextColor.CGColor;
45 |
46 | self.nameLabel.text = self.title;
47 | }
48 |
49 | #pragma mark - Accessors
50 |
51 | - (void)setTitle:(NSString *)title {
52 |
53 | _title = [title copy];
54 |
55 | self.nameLabel.text = self.title;
56 | }
57 |
58 | - (void)setColor:(UIColor *)color {
59 |
60 | _color = [color copy];
61 |
62 | self.imageView.tintColor = self.color;
63 | }
64 |
65 | - (void)setSelected:(BOOL)selected {
66 |
67 | [super setSelected:selected];
68 |
69 | self.imageView.layer.borderWidth = self.isSelected ? 2.0 : 0.0;
70 | self.imageView.layer.cornerRadius = self.isSelected ? 8.0 : 0.0;
71 | }
72 |
73 | @end
74 |
--------------------------------------------------------------------------------
/TGLStackedViewExample/TGLSettingOptionsViewController.h:
--------------------------------------------------------------------------------
1 | //
2 | // TGLSettingOptionsViewController.h
3 | // TGLStackedViewExample
4 | //
5 | // Created by Tim Gleue on 12.06.16.
6 | // Copyright © 2016-2019 Tim Gleue • interactive software. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | @class TGLSettingOptionsViewController;
12 |
13 | @protocol TGLSettingOptionsViewControllerDelegate
14 |
15 | @optional
16 |
17 | - (void)optionsViewController:(nonnull TGLSettingOptionsViewController *)controller didSelectValue:(nonnull NSValue *)value;
18 |
19 | @end
20 |
21 | @interface TGLSettingOptionsViewController : UITableViewController
22 |
23 | @property (nonatomic, weak, nullable) id delegate;
24 |
25 | @property (nonatomic, strong, nullable) NSArray *names;
26 | @property (nonatomic, strong, nullable) NSArray *values;
27 |
28 | @property (nonatomic, strong, nullable) NSValue *selectedValue;
29 | @property (nonatomic, strong, nullable) NSIndexPath *optionIndexPath;
30 |
31 | @end
32 |
--------------------------------------------------------------------------------
/TGLStackedViewExample/TGLSettingOptionsViewController.m:
--------------------------------------------------------------------------------
1 | //
2 | // TGLSettingOptionsViewController.m
3 | // TGLStackedViewExample
4 | //
5 | // Created by Tim Gleue on 12.06.16.
6 | // Copyright © 2016-2019 Tim Gleue • interactive software. All rights reserved.
7 | //
8 |
9 | #import "TGLSettingOptionsViewController.h"
10 | #import "TGLViewController.h"
11 |
12 | #pragma mark - TGLSettingsTableViewCell interfaces
13 |
14 | @interface TGLOptionValueTableViewCell : UITableViewCell
15 |
16 | @end
17 |
18 | #pragma mark - TGLSettingOptionsViewController
19 |
20 | @interface TGLSettingOptionsViewController ()
21 |
22 | @end
23 |
24 | @implementation TGLSettingOptionsViewController
25 |
26 | #pragma mark - View life cycle
27 |
28 | - (void)viewDidLoad {
29 |
30 | [super viewDidLoad];
31 | }
32 |
33 | #pragma mark - UITableViewDataSource protocol
34 |
35 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
36 |
37 | return 1;
38 | }
39 |
40 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
41 |
42 | return MIN(self.names.count, self.values.count);
43 | }
44 |
45 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
46 |
47 | TGLOptionValueTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"OptionCell" forIndexPath:indexPath];
48 |
49 | cell.textLabel.text = NSLocalizedString(self.names[indexPath.row], nil);
50 | cell.accessoryType = ([self.values[indexPath.row] isEqualToValue:self.selectedValue]) ? UITableViewCellAccessoryCheckmark : UITableViewCellAccessoryNone;
51 |
52 | return cell;
53 | }
54 |
55 | #pragma mark - UITableViewDelegate protocol
56 |
57 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
58 |
59 | [tableView deselectRowAtIndexPath:indexPath animated:NO];
60 |
61 | for (NSInteger row = 0; row < [tableView numberOfRowsInSection:indexPath.section]; row++) {
62 |
63 | UITableViewCell *cell = [tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:row inSection:indexPath.section]];
64 |
65 | if (cell.accessoryType == UITableViewCellAccessoryCheckmark) {
66 |
67 | cell.accessoryType = UITableViewCellAccessoryNone;
68 | break;
69 | }
70 | }
71 |
72 | UITableViewCell *cell = [tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:indexPath.row inSection:indexPath.section]];
73 |
74 | cell.accessoryType = UITableViewCellAccessoryCheckmark;
75 |
76 | self.selectedValue = self.values[indexPath.row];
77 |
78 | if ([self.delegate respondsToSelector:@selector(optionsViewController:didSelectValue:)]) {
79 |
80 | [self.delegate optionsViewController:self didSelectValue:self.selectedValue];
81 | }
82 | }
83 |
84 | @end
85 |
86 | #pragma mark - TGLOptionValueTableViewCell implementations
87 |
88 | @implementation TGLOptionValueTableViewCell
89 | @end
90 |
--------------------------------------------------------------------------------
/TGLStackedViewExample/TGLSettingsViewController.h:
--------------------------------------------------------------------------------
1 | //
2 | // TGLSettingsViewController.h
3 | // TGLStackedViewExample
4 | //
5 | // Created by Tim Gleue on 10.06.16.
6 | // Copyright © 2016-2019 Tim Gleue • interactive software. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | @interface TGLSettingsViewController : UITableViewController
12 |
13 | @end
14 |
--------------------------------------------------------------------------------
/TGLStackedViewExample/TGLSettingsViewController.m:
--------------------------------------------------------------------------------
1 | //
2 | // TGLSettingsViewController.m
3 | // TGLStackedViewExample
4 | //
5 | // Created by Tim Gleue on 10.06.16.
6 | // Copyright © 2016-2019 Tim Gleue • interactive software. All rights reserved.
7 | //
8 |
9 | #import "TGLSettingsViewController.h"
10 | #import "TGLSettingOptionsViewController.h"
11 | #import "TGLViewController.h"
12 |
13 | #pragma mark - TGLSettingsTableViewCell interfaces
14 |
15 | @interface TGLSettingsTableViewCell : UITableViewCell
16 |
17 | @property (nonatomic, copy) NSString *keyPath;
18 | @property (nonatomic, strong) NSIndexPath *indexPath;
19 | @property (nonatomic, strong) NSMutableDictionary *valuesDict;
20 |
21 | @end
22 |
23 | @interface TGLSwitchTableViewCell : TGLSettingsTableViewCell
24 |
25 | @property (weak, nonatomic) IBOutlet UILabel *switchLabel;
26 | @property (weak, nonatomic) IBOutlet UISwitch *switchControl;
27 |
28 | - (void)setSwitchValue:(BOOL)value;
29 |
30 | @end
31 |
32 | @interface TGLStepperTableViewCell : TGLSettingsTableViewCell
33 |
34 | @property (nonatomic, copy) NSString *labelFormat;
35 | @property (nonatomic, assign) NSNumberFormatterStyle numberStyle;
36 |
37 | @property (weak, nonatomic) IBOutlet UILabel *stepperLabel;
38 | @property (weak, nonatomic) IBOutlet UIStepper *stepperControl;
39 |
40 | - (void)setStepperValue:(double)value;
41 |
42 | @end
43 |
44 | @interface TGLOptionsTableViewCell : TGLSettingsTableViewCell
45 |
46 | @end
47 |
48 | #pragma mark - TGLSettingsViewController
49 |
50 | @interface TGLSettingsViewController ()
51 |
52 | @property (nonatomic, strong) NSArray *sections;
53 | @property (nonatomic, strong) NSMutableDictionary *values;
54 |
55 | @end
56 |
57 | static NSString * const TGLSettingsSectionHeaderTitleKey = @"headerTitle";
58 | static NSString * const TGLSettingsSectionRowArrayKey = @"sectionRows";
59 |
60 | static NSString * const TGLSettingsRowTypeKey = @"rowType";
61 | static NSString * const TGLSettingsRowTypeSwitch = @"switch";
62 | static NSString * const TGLSettingsRowTypeStepper = @"stepper";
63 | static NSString * const TGLSettingsRowTypeOptions = @"options";
64 | static NSString * const TGLSettingsRowDefaultValueKey = @"defaultValue";
65 | static NSString * const TGLSettingsRowKeyPathKey = @"keyPath";
66 |
67 | static NSString * const TGLSettingsSwitchRowTitleKey = @"title";
68 |
69 | static NSString * const TGLSettingsStepperRowTitleFormatKey = @"titleFormat";
70 | static NSString * const TGLSettingsStepperRowNumberStyleKey = @"numberStyle";
71 | static NSString * const TGLSettingsStepperRowMinValueKey = @"minValue";
72 | static NSString * const TGLSettingsStepperRowMaxValueKey = @"maxValue";
73 | static NSString * const TGLSettingsStepperRowValueFactorKey = @"valueFactor";
74 |
75 | static NSString * const TGLSettingsOptionsRowTitleKey = @"title";
76 | static NSString * const TGLSettingsOptionsRowValuesArrayKey = @"optionValues";
77 | static NSString * const TGLSettingsOptionsRowOptionNameKey = @"name";
78 | static NSString * const TGLSettingsOptionsRowOptionValueKey = @"value";
79 |
80 | @implementation TGLSettingsViewController
81 |
82 | #pragma mark - View life cycle
83 |
84 | - (void)viewDidLoad {
85 |
86 | [super viewDidLoad];
87 |
88 | NSArray *navigationRows = @[ @{ TGLSettingsRowTypeKey: TGLSettingsRowTypeSwitch, TGLSettingsSwitchRowTitleKey: @"Hides Navigation Bar", TGLSettingsRowDefaultValueKey: @(YES), TGLSettingsRowKeyPathKey: @"%N.navigationBarHidden" },
89 | @{ TGLSettingsRowTypeKey: TGLSettingsRowTypeSwitch, TGLSettingsSwitchRowTitleKey: @"Hides Toolbar", TGLSettingsRowDefaultValueKey: @(YES), TGLSettingsRowKeyPathKey: @"%N.toolbarHidden" } ];
90 |
91 | NSDictionary *navigationSection = @{ TGLSettingsSectionHeaderTitleKey: @"Navigation Controller", TGLSettingsSectionRowArrayKey: navigationRows };
92 |
93 | NSArray *controllerRows = @[ @{ TGLSettingsRowTypeKey: TGLSettingsRowTypeSwitch, TGLSettingsSwitchRowTitleKey: @"Adjust Scroll View Insets", TGLSettingsRowDefaultValueKey: @(NO), TGLSettingsRowKeyPathKey: @"%S.automaticallyAdjustsScrollViewInsets" },
94 | @{ TGLSettingsRowTypeKey: TGLSettingsRowTypeSwitch, TGLSettingsSwitchRowTitleKey: @"Shows Scroll Indicators", TGLSettingsRowDefaultValueKey: @(NO), TGLSettingsRowKeyPathKey: @"%S.showsVerticalScrollIndicator" },
95 | @{ TGLSettingsRowTypeKey: TGLSettingsRowTypeSwitch, TGLSettingsSwitchRowTitleKey: @"Shows Background View", TGLSettingsRowDefaultValueKey: @(NO), TGLSettingsRowKeyPathKey: @"%S.showsBackgroundView" },
96 | @{ TGLSettingsRowTypeKey: TGLSettingsRowTypeStepper, TGLSettingsStepperRowTitleFormatKey: @"%@ Cards", TGLSettingsStepperRowNumberStyleKey: @(NSNumberFormatterDecimalStyle), TGLSettingsRowDefaultValueKey: @(20), TGLSettingsStepperRowMinValueKey: @(0), TGLSettingsStepperRowMaxValueKey: @(100), TGLSettingsRowKeyPathKey: @"%S.cardCount" },
97 | @{ TGLSettingsRowTypeKey: TGLSettingsRowTypeStepper, TGLSettingsStepperRowTitleFormatKey: @"%@ Card Height", TGLSettingsStepperRowNumberStyleKey: @(NSNumberFormatterDecimalStyle), TGLSettingsRowDefaultValueKey: @(320), TGLSettingsStepperRowMinValueKey: @(0), TGLSettingsStepperRowMaxValueKey: @(1000), TGLSettingsRowKeyPathKey: @"%S.cardSize.height" } ];
98 |
99 | NSDictionary *controllerSection = @{ TGLSettingsSectionHeaderTitleKey: @"Stacked View Controller", TGLSettingsSectionRowArrayKey: controllerRows };
100 |
101 | CGRect statusFrame = [[UIApplication sharedApplication] statusBarFrame];
102 |
103 | NSArray *stackedRows = @[ @{ TGLSettingsRowTypeKey: TGLSettingsRowTypeStepper, TGLSettingsStepperRowTitleFormatKey: @"%@ Top Margin", TGLSettingsStepperRowNumberStyleKey: @(NSNumberFormatterDecimalStyle), TGLSettingsRowDefaultValueKey: @(statusFrame.size.height), TGLSettingsStepperRowMinValueKey: @(0), TGLSettingsStepperRowMaxValueKey: @(200), TGLSettingsRowKeyPathKey: @"%S.stackedLayoutMargin.top" },
104 | @{ TGLSettingsRowTypeKey: TGLSettingsRowTypeStepper, TGLSettingsStepperRowTitleFormatKey: @"%@ Left Margin", TGLSettingsStepperRowNumberStyleKey: @(NSNumberFormatterDecimalStyle), TGLSettingsRowDefaultValueKey: @(0), TGLSettingsStepperRowMinValueKey: @(0), TGLSettingsStepperRowMaxValueKey: @(100), TGLSettingsRowKeyPathKey: @"%S.stackedLayoutMargin.left" },
105 | @{ TGLSettingsRowTypeKey: TGLSettingsRowTypeStepper, TGLSettingsStepperRowTitleFormatKey: @"%@ Right Margin", TGLSettingsStepperRowNumberStyleKey: @(NSNumberFormatterDecimalStyle), TGLSettingsRowDefaultValueKey: @(0), TGLSettingsStepperRowMinValueKey: @(0), TGLSettingsStepperRowMaxValueKey: @(100), TGLSettingsRowKeyPathKey: @"%S.stackedLayoutMargin.right" },
106 | @{ TGLSettingsRowTypeKey: TGLSettingsRowTypeStepper, TGLSettingsStepperRowTitleFormatKey: @"%@ Top Reveal", TGLSettingsStepperRowNumberStyleKey: @(NSNumberFormatterDecimalStyle), TGLSettingsRowDefaultValueKey: @(120), TGLSettingsStepperRowMinValueKey: @(1), TGLSettingsStepperRowMaxValueKey: @(500), TGLSettingsRowKeyPathKey: @"%S.stackedTopReveal" },
107 | @{ TGLSettingsRowTypeKey: TGLSettingsRowTypeSwitch, TGLSettingsSwitchRowTitleKey: @"Fill Height", TGLSettingsRowDefaultValueKey: @(YES), TGLSettingsRowKeyPathKey: @"%S.stackedFillHeight" },
108 | @{ TGLSettingsRowTypeKey: TGLSettingsRowTypeSwitch, TGLSettingsSwitchRowTitleKey: @"Center Single Item", TGLSettingsRowDefaultValueKey: @(NO), TGLSettingsRowKeyPathKey: @"%S.stackedCenterSingleItem" },
109 | @{ TGLSettingsRowTypeKey: TGLSettingsRowTypeSwitch, TGLSettingsSwitchRowTitleKey: @"Always Bounce", TGLSettingsRowDefaultValueKey: @(YES), TGLSettingsRowKeyPathKey: @"%S.stackedAlwaysBounce" },
110 | @{ TGLSettingsRowTypeKey: TGLSettingsRowTypeStepper, TGLSettingsStepperRowTitleFormatKey: @"%@ Bounce Factor", TGLSettingsStepperRowNumberStyleKey: @(NSNumberFormatterPercentStyle), TGLSettingsRowDefaultValueKey: @(20), TGLSettingsStepperRowMinValueKey: @(0), TGLSettingsStepperRowMaxValueKey: @(200), TGLSettingsStepperRowValueFactorKey: @(0.01), TGLSettingsRowKeyPathKey: @"%S.stackedBounceFactor" },
111 | @{ TGLSettingsRowTypeKey: TGLSettingsRowTypeStepper, TGLSettingsStepperRowTitleFormatKey: @"%@ Moving Scale", TGLSettingsStepperRowNumberStyleKey: @(NSNumberFormatterPercentStyle), TGLSettingsRowDefaultValueKey: @(95), TGLSettingsStepperRowMinValueKey: @(0), TGLSettingsStepperRowMaxValueKey: @(200), TGLSettingsStepperRowValueFactorKey: @(0.01), TGLSettingsRowKeyPathKey: @"%S.movingItemScaleFactor" },
112 | @{ TGLSettingsRowTypeKey: TGLSettingsRowTypeSwitch, TGLSettingsSwitchRowTitleKey: @"Moving Item on Top", TGLSettingsRowDefaultValueKey: @(YES), TGLSettingsRowKeyPathKey: @"%S.movingItemOnTop" } ];
113 |
114 | NSDictionary *stackedSection = @{ TGLSettingsSectionHeaderTitleKey: @"Stacked Layout", TGLSettingsSectionRowArrayKey: stackedRows };
115 |
116 | NSArray *exposedPinningOptions = @[ @{ TGLSettingsOptionsRowOptionNameKey: @"Pin All", TGLSettingsOptionsRowOptionValueKey: @(2) }, @{ TGLSettingsOptionsRowOptionNameKey: @"Pin Below", TGLSettingsOptionsRowOptionValueKey: @(1) }, @{ TGLSettingsOptionsRowOptionNameKey: @"Pin None", TGLSettingsOptionsRowOptionValueKey: @(0) } ];
117 |
118 | NSArray *exposedRows = @[ @{ TGLSettingsRowTypeKey: TGLSettingsRowTypeStepper, TGLSettingsStepperRowTitleFormatKey: @"%@ Top Margin", TGLSettingsStepperRowNumberStyleKey: @(NSNumberFormatterDecimalStyle), TGLSettingsRowDefaultValueKey: @(statusFrame.size.height + 20), TGLSettingsStepperRowMinValueKey: @(0), TGLSettingsStepperRowMaxValueKey: @(200), TGLSettingsRowKeyPathKey: @"%S.exposedLayoutMargin.top" },
119 | @{ TGLSettingsRowTypeKey: TGLSettingsRowTypeStepper, TGLSettingsStepperRowTitleFormatKey: @"%@ Left Margin", TGLSettingsStepperRowNumberStyleKey: @(NSNumberFormatterDecimalStyle), TGLSettingsRowDefaultValueKey: @(0), TGLSettingsStepperRowMinValueKey: @(0), TGLSettingsStepperRowMaxValueKey: @(100), TGLSettingsRowKeyPathKey: @"%S.exposedLayoutMargin.left" },
120 | @{ TGLSettingsRowTypeKey: TGLSettingsRowTypeStepper, TGLSettingsStepperRowTitleFormatKey: @"%@ Right Margin", TGLSettingsStepperRowNumberStyleKey: @(NSNumberFormatterDecimalStyle), TGLSettingsRowDefaultValueKey: @(0), TGLSettingsStepperRowMinValueKey: @(0), TGLSettingsStepperRowMaxValueKey: @(100), TGLSettingsRowKeyPathKey: @"%S.exposedLayoutMargin.right" },
121 | @{ TGLSettingsRowTypeKey: TGLSettingsRowTypeOptions, TGLSettingsOptionsRowTitleKey: @"Pinning Mode", TGLSettingsRowDefaultValueKey: @(2), TGLSettingsRowKeyPathKey: @"%S.exposedPinningMode", TGLSettingsOptionsRowValuesArrayKey: exposedPinningOptions },
122 | @{ TGLSettingsRowTypeKey: TGLSettingsRowTypeStepper, TGLSettingsStepperRowTitleFormatKey: @"%@ Top Pinning Count", TGLSettingsStepperRowNumberStyleKey: @(NSNumberFormatterDecimalStyle), TGLSettingsRowDefaultValueKey: @(-1), TGLSettingsStepperRowMinValueKey: @(-1), TGLSettingsStepperRowMaxValueKey: @(10), TGLSettingsRowKeyPathKey: @"%S.exposedTopPinningCount" },
123 | @{ TGLSettingsRowTypeKey: TGLSettingsRowTypeStepper, TGLSettingsStepperRowTitleFormatKey: @"%@ Bottom Pinning Count", TGLSettingsStepperRowNumberStyleKey: @(NSNumberFormatterDecimalStyle), TGLSettingsRowDefaultValueKey: @(-1), TGLSettingsStepperRowMinValueKey: @(-1), TGLSettingsStepperRowMaxValueKey: @(10), TGLSettingsRowKeyPathKey: @"%S.exposedBottomPinningCount" },
124 | @{ TGLSettingsRowTypeKey: TGLSettingsRowTypeStepper, TGLSettingsStepperRowTitleFormatKey: @"%@ Top Overlap", TGLSettingsStepperRowNumberStyleKey: @(NSNumberFormatterDecimalStyle), TGLSettingsRowDefaultValueKey: @(10), TGLSettingsStepperRowMinValueKey: @(0), TGLSettingsStepperRowMaxValueKey: @(100), TGLSettingsRowKeyPathKey: @"%S.exposedTopOverlap" },
125 | @{ TGLSettingsRowTypeKey: TGLSettingsRowTypeStepper, TGLSettingsStepperRowTitleFormatKey: @"%@ Bottom Overlap", TGLSettingsStepperRowNumberStyleKey: @(NSNumberFormatterDecimalStyle), TGLSettingsRowDefaultValueKey: @(10), TGLSettingsStepperRowMinValueKey: @(0), TGLSettingsStepperRowMaxValueKey: @(100), TGLSettingsRowKeyPathKey: @"%S.exposedBottomOverlap" },
126 | @{ TGLSettingsRowTypeKey: TGLSettingsRowTypeStepper, TGLSettingsStepperRowTitleFormatKey: @"%@ Bottom Overlap Count", TGLSettingsStepperRowNumberStyleKey: @(NSNumberFormatterDecimalStyle), TGLSettingsRowDefaultValueKey: @(1), TGLSettingsStepperRowMinValueKey: @(0), TGLSettingsStepperRowMaxValueKey: @(10), TGLSettingsRowKeyPathKey: @"%S.exposedBottomOverlapCount" },
127 | @{ TGLSettingsRowTypeKey: TGLSettingsRowTypeSwitch, TGLSettingsSwitchRowTitleKey: @"Collapsible Exposed Item", TGLSettingsRowDefaultValueKey: @(YES), TGLSettingsRowKeyPathKey: @"%S.exposedItemsAreCollapsible" },
128 | @{ TGLSettingsRowTypeKey: TGLSettingsRowTypeSwitch, TGLSettingsSwitchRowTitleKey: @"Selectable Unexposed Items", TGLSettingsRowDefaultValueKey: @(NO), TGLSettingsRowKeyPathKey: @"%S.unexposedItemsAreSelectable" } ];
129 |
130 | NSDictionary *exposedSection = @{ TGLSettingsSectionHeaderTitleKey: @"Exposed Layout", TGLSettingsSectionRowArrayKey: exposedRows };
131 |
132 | self.sections = @[ navigationSection, controllerSection, stackedSection, exposedSection ];
133 |
134 | [self resetSettings:nil];
135 | }
136 |
137 | #pragma mark - Navigation
138 |
139 | - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
140 |
141 | if ([segue.identifier isEqualToString:@"ShowExample"]) {
142 |
143 | [self applyValues:self.values forSections:self.sections ToSegue:segue];
144 |
145 | UINavigationController *navigationController = segue.destinationViewController;
146 | TGLViewController *stackedController = (TGLViewController *)navigationController.topViewController;
147 |
148 | stackedController.doubleTapToClose = navigationController.navigationBarHidden;
149 |
150 | if (!stackedController.doubleTapToClose) {
151 |
152 | stackedController.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(closeStackedController:)];
153 | }
154 |
155 | } else if ([segue.identifier isEqualToString:@"ShowOptions"]) {
156 |
157 | NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
158 | NSDictionary *rowDict = [self rowDictionaryForIndexPath:indexPath fromSections:self.sections];
159 | NSArray *optionValues = rowDict[TGLSettingsOptionsRowValuesArrayKey];
160 |
161 | NSMutableArray *names = [NSMutableArray array];
162 | NSMutableArray *values = [NSMutableArray array];
163 |
164 | for (NSDictionary *optionDict in optionValues) {
165 |
166 | NSString *optionName = optionDict[TGLSettingsOptionsRowOptionNameKey];
167 |
168 | [names addObject:optionName];
169 |
170 | NSValue *optionValue = optionDict[TGLSettingsOptionsRowOptionValueKey];
171 |
172 | [values addObject:optionValue];
173 | }
174 |
175 | TGLSettingOptionsViewController *optionsController = segue.destinationViewController;
176 |
177 | optionsController.names = names;
178 | optionsController.values = values;
179 | optionsController.selectedValue = self.values[indexPath];
180 | optionsController.optionIndexPath = indexPath;
181 |
182 | optionsController.delegate = self;
183 | }
184 | }
185 |
186 | #pragma mark - Actions
187 |
188 | - (IBAction)resetSettings:(id)sender {
189 |
190 | self.values = [self dictionaryOfDefaultValuesFromSections:self.sections];
191 |
192 | [self.tableView reloadData];
193 | }
194 |
195 | - (IBAction)closeStackedController:(id)sender {
196 |
197 | [self dismissViewControllerAnimated:YES completion:nil];
198 | }
199 |
200 | #pragma mark - UITableViewDataSource protocol
201 |
202 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
203 |
204 | return self.sections.count;
205 | }
206 |
207 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
208 |
209 | NSDictionary *sectionDict = self.sections[section];
210 | NSArray *sectionRows = sectionDict[TGLSettingsSectionRowArrayKey];
211 |
212 | return sectionRows.count;
213 | }
214 |
215 | - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
216 |
217 | NSDictionary *sectionDict = self.sections[section];
218 |
219 | return NSLocalizedString(sectionDict[TGLSettingsSectionHeaderTitleKey], nil);
220 | }
221 |
222 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
223 |
224 | NSDictionary *rowDict = [self rowDictionaryForIndexPath:indexPath fromSections:self.sections];
225 |
226 | NSString *rowType = rowDict[TGLSettingsRowTypeKey];
227 |
228 | if ([rowType isEqualToString:TGLSettingsRowTypeSwitch]) {
229 |
230 | TGLSwitchTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"SwitchCell" forIndexPath:indexPath];
231 | NSString *title = rowDict[TGLSettingsSwitchRowTitleKey];
232 |
233 | cell.switchLabel.text = NSLocalizedString(title, nil);
234 |
235 | [cell setSwitchValue:[self.values[indexPath] boolValue]];
236 |
237 | cell.keyPath = rowDict[TGLSettingsRowKeyPathKey];
238 | cell.indexPath = indexPath;
239 | cell.valuesDict = self.values;
240 |
241 | return cell;
242 |
243 | } else if ([rowType isEqualToString:TGLSettingsRowTypeStepper]) {
244 |
245 | TGLStepperTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"StepperCell" forIndexPath:indexPath];
246 |
247 | cell.labelFormat = rowDict[TGLSettingsStepperRowTitleFormatKey];
248 | cell.numberStyle = (NSNumberFormatterStyle)[rowDict[TGLSettingsStepperRowNumberStyleKey] integerValue];
249 |
250 | cell.stepperControl.minimumValue = [rowDict[TGLSettingsStepperRowMinValueKey] doubleValue];
251 | cell.stepperControl.maximumValue = [rowDict[TGLSettingsStepperRowMaxValueKey] doubleValue];
252 |
253 | [cell setStepperValue:[self.values[indexPath] doubleValue]];
254 |
255 | cell.keyPath = rowDict[TGLSettingsRowKeyPathKey];
256 | cell.indexPath = indexPath;
257 | cell.valuesDict = self.values;
258 |
259 | return cell;
260 |
261 | } else if ([rowType isEqualToString:TGLSettingsRowTypeOptions]) {
262 |
263 | TGLOptionsTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"OptionsCell" forIndexPath:indexPath];
264 | NSString *title = rowDict[TGLSettingsSwitchRowTitleKey];
265 | NSString *name = [self nameForOptionWithValue:self.values[indexPath] inOptionValuesArray:rowDict[TGLSettingsOptionsRowValuesArrayKey]];
266 |
267 | cell.textLabel.text = NSLocalizedString(title, nil);
268 | cell.detailTextLabel.text = NSLocalizedString(name, nil);
269 |
270 | cell.keyPath = rowDict[TGLSettingsRowKeyPathKey];
271 | cell.indexPath = indexPath;
272 | cell.valuesDict = self.values;
273 |
274 | return cell;
275 |
276 | } else {
277 |
278 | return nil;
279 | }
280 | }
281 |
282 | #pragma mark - TGLSettingOptionsViewControllerDelegate protocol
283 |
284 | - (void)optionsViewController:(TGLSettingOptionsViewController *)controller didSelectValue:(NSValue *)value {
285 |
286 | NSIndexPath *indexPath = controller.optionIndexPath;
287 |
288 | self.values[indexPath] = value;
289 |
290 | [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
291 | }
292 |
293 | #pragma mark - Helpers
294 |
295 | - (NSMutableDictionary *)dictionaryOfDefaultValuesFromSections:(NSArray *)sections {
296 |
297 | NSMutableDictionary *valuesDict = [NSMutableDictionary dictionary];
298 |
299 | NSInteger section = 0;
300 |
301 | for (NSDictionary *sectionDict in sections) {
302 |
303 | NSInteger row = 0;
304 |
305 | for (NSDictionary *rowDict in sectionDict[TGLSettingsSectionRowArrayKey]) {
306 |
307 | NSIndexPath *rowIndexPath = [NSIndexPath indexPathForRow:row inSection:section];
308 |
309 | valuesDict[rowIndexPath] = rowDict[TGLSettingsRowDefaultValueKey];
310 |
311 | row += 1;
312 | }
313 |
314 | section += 1;
315 | }
316 |
317 | return valuesDict;
318 | }
319 |
320 | - (void)applyValues:(NSDictionary *)values forSections:(NSArray *)sections ToSegue:(UIStoryboardSegue *)segue {
321 |
322 | NSInteger section = 0;
323 |
324 | for (NSDictionary *sectionDict in sections) {
325 |
326 | NSInteger row = 0;
327 |
328 | for (NSDictionary *rowDict in sectionDict[TGLSettingsSectionRowArrayKey]) {
329 |
330 | NSString *rowType = rowDict[TGLSettingsRowTypeKey];
331 | NSString *rowKeyPath = rowDict[TGLSettingsRowKeyPathKey];
332 |
333 | rowKeyPath = [rowKeyPath stringByReplacingOccurrencesOfString:@"%N" withString:@"destinationViewController"];
334 | rowKeyPath = [rowKeyPath stringByReplacingOccurrencesOfString:@"%S" withString:@"destinationViewController.topViewController"];
335 |
336 | NSIndexPath *rowIndexPath = [NSIndexPath indexPathForRow:row inSection:section];
337 |
338 | if ([rowType isEqualToString:TGLSettingsRowTypeSwitch]) {
339 |
340 | [segue setValue:@([values[rowIndexPath] boolValue]) forKeyPath:rowKeyPath];
341 |
342 | } else if ([rowType isEqualToString:TGLSettingsRowTypeStepper]) {
343 |
344 | double value = [values[rowIndexPath] doubleValue];
345 |
346 | if (rowDict[TGLSettingsStepperRowValueFactorKey]) {
347 |
348 | value *= [rowDict[TGLSettingsStepperRowValueFactorKey] doubleValue];
349 | }
350 |
351 | [segue setValue:@(value) forKeyPath:rowKeyPath];
352 |
353 | } else if ([rowType isEqualToString:TGLSettingsRowTypeOptions]) {
354 |
355 | [segue setValue:values[rowIndexPath] forKeyPath:rowKeyPath];
356 | }
357 |
358 | row += 1;
359 | }
360 |
361 | section += 1;
362 | }
363 | }
364 |
365 | - (NSDictionary *)rowDictionaryForIndexPath:(NSIndexPath *)indexPath fromSections:(NSArray *)sections {
366 |
367 | NSDictionary *sectionDict = sections[indexPath.section];
368 | NSArray *sectionRows = sectionDict[TGLSettingsSectionRowArrayKey];
369 |
370 | return sectionRows[indexPath.row];
371 | }
372 |
373 | - (NSString *)nameForOptionWithValue:(NSValue *)value inOptionValuesArray:(NSArray *)options {
374 |
375 | for (NSDictionary *optionDict in options) {
376 |
377 | NSValue *optionValue = optionDict[TGLSettingsOptionsRowOptionValueKey];
378 |
379 | if ([optionValue isEqualToValue:value]) return optionDict[TGLSettingsOptionsRowOptionNameKey];
380 | }
381 |
382 | return nil;
383 | }
384 |
385 | @end
386 |
387 | #pragma mark - TGLSettingsTableViewCell implementations
388 |
389 | @implementation TGLSettingsTableViewCell
390 | @end
391 |
392 | @implementation TGLSwitchTableViewCell
393 |
394 | - (void)setSwitchValue:(BOOL)value {
395 |
396 | self.switchControl.on = value;
397 | }
398 |
399 | - (IBAction)switchValueChanged:(id)sender {
400 |
401 | self.valuesDict[self.indexPath] = @(self.switchControl.on);
402 | }
403 |
404 | @end
405 |
406 | @implementation TGLStepperTableViewCell
407 |
408 | - (void)setStepperValue:(double)value {
409 |
410 | self.stepperControl.value = value;
411 |
412 | [self updateLabel];
413 | }
414 |
415 | - (IBAction)stepperValueChanged:(id)sender {
416 |
417 | self.valuesDict[self.indexPath] = @(self.stepperControl.value);
418 |
419 | [self updateLabel];
420 | }
421 |
422 | - (void)updateLabel {
423 |
424 | double value = self.stepperControl.value;
425 |
426 | if (self.numberStyle == NSNumberFormatterPercentStyle) value /= 100.0;
427 |
428 | NSString *localizedFormat = NSLocalizedString(self.labelFormat, nil);
429 |
430 | self.stepperLabel.text = [NSString stringWithFormat:localizedFormat, [NSNumberFormatter localizedStringFromNumber:@(value) numberStyle:self.numberStyle]];
431 | }
432 |
433 | @end
434 |
435 | @implementation TGLOptionsTableViewCell
436 | @end
437 |
--------------------------------------------------------------------------------
/TGLStackedViewExample/TGLStackedViewExample-Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleDisplayName
8 | TGLStacked
9 | CFBundleExecutable
10 | ${EXECUTABLE_NAME}
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | ${PRODUCT_NAME}
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | 2.2
21 | CFBundleSignature
22 | ????
23 | CFBundleVersion
24 | $(CURRENT_PROJECT_VERSION)
25 | LSRequiresIPhoneOS
26 |
27 | UILaunchStoryboardName
28 | Launch Screen
29 | UIMainStoryboardFile
30 | Main
31 | UIRequiredDeviceCapabilities
32 |
33 | armv7
34 |
35 | UIStatusBarStyle
36 | UIStatusBarStyleLightContent
37 | UISupportedInterfaceOrientations
38 |
39 | UIInterfaceOrientationPortrait
40 | UIInterfaceOrientationLandscapeLeft
41 | UIInterfaceOrientationLandscapeRight
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/TGLStackedViewExample/TGLStackedViewExample-Prefix.pch:
--------------------------------------------------------------------------------
1 | //
2 | // Prefix header
3 | //
4 | // The contents of this file are implicitly included at the beginning of every source file.
5 | //
6 |
7 | #import
8 |
9 | #ifndef __IPHONE_5_0
10 | #warning "This project uses features only available in iOS SDK 5.0 and later."
11 | #endif
12 |
13 | #ifdef __OBJC__
14 | #import
15 | #import
16 | #endif
17 |
--------------------------------------------------------------------------------
/TGLStackedViewExample/TGLViewController.h:
--------------------------------------------------------------------------------
1 | //
2 | // TGLViewController.h
3 | // TGLStackedViewExample
4 | //
5 | // Created by Tim Gleue on 07.04.14.
6 | // Copyright (c) 2014-2019 Tim Gleue ( http://gleue-interactive.com )
7 | //
8 | // Permission is hereby granted, free of charge, to any person obtaining a copy
9 | // of this software and associated documentation files (the "Software"), to deal
10 | // in the Software without restriction, including without limitation the rights
11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | // copies of the Software, and to permit persons to whom the Software is
13 | // furnished to do so, subject to the following conditions:
14 | //
15 | // The above copyright notice and this permission notice shall be included in
16 | // all copies or substantial portions of the Software.
17 | //
18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 | // THE SOFTWARE.
25 |
26 | #import
27 |
28 | #import "TGLStackedViewController.h"
29 |
30 | @interface TGLViewController : TGLStackedViewController
31 |
32 | @property (nonatomic, assign) BOOL showsBackgroundView;
33 | @property (nonatomic, assign) BOOL showsVerticalScrollIndicator;
34 |
35 | @property (nonatomic, assign) NSInteger cardCount;
36 | @property (nonatomic, assign) CGSize cardSize;
37 |
38 | @property (nonatomic, assign) UIEdgeInsets stackedLayoutMargin;
39 | @property (nonatomic, assign) CGFloat stackedTopReveal;
40 | @property (nonatomic, assign) CGFloat stackedBounceFactor;
41 | @property (nonatomic, assign) BOOL stackedFillHeight;
42 | @property (nonatomic, assign) BOOL stackedCenterSingleItem;
43 | @property (nonatomic, assign) BOOL stackedAlwaysBounce;
44 |
45 | @property (nonatomic, assign) BOOL doubleTapToClose;
46 |
47 | @end
48 |
--------------------------------------------------------------------------------
/TGLStackedViewExample/TGLViewController.m:
--------------------------------------------------------------------------------
1 | //
2 | // TGLViewController.m
3 | // TGLStackedViewExample
4 | //
5 | // Created by Tim Gleue on 07.04.14.
6 | // Copyright (c) 2014-2019 Tim Gleue ( http://gleue-interactive.com )
7 | //
8 | // Permission is hereby granted, free of charge, to any person obtaining a copy
9 | // of this software and associated documentation files (the "Software"), to deal
10 | // in the Software without restriction, including without limitation the rights
11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | // copies of the Software, and to permit persons to whom the Software is
13 | // furnished to do so, subject to the following conditions:
14 | //
15 | // The above copyright notice and this permission notice shall be included in
16 | // all copies or substantial portions of the Software.
17 | //
18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 | // THE SOFTWARE.
25 |
26 | #import "TGLViewController.h"
27 | #import "TGLCollectionViewCell.h"
28 | #import "TGLBackgroundProxyView.h"
29 |
30 | @interface UIColor (randomColor)
31 |
32 | + (UIColor *)randomColor;
33 |
34 | @end
35 |
36 | @implementation UIColor (randomColor)
37 |
38 | + (UIColor *)randomColor {
39 |
40 | CGFloat comps[3];
41 |
42 | for (int i = 0; i < 3; i++) {
43 |
44 | NSUInteger r = arc4random_uniform(256);
45 | comps[i] = (CGFloat)r/255.f;
46 | }
47 |
48 | return [UIColor colorWithRed:comps[0] green:comps[1] blue:comps[2] alpha:1.0];
49 | }
50 |
51 | @end
52 |
53 | @interface TGLViewController ()
54 |
55 | @property (nonatomic, weak) IBOutlet UIBarButtonItem *deselectItem;
56 | @property (nonatomic, strong) IBOutlet UIView *collectionViewBackground;
57 | @property (nonatomic, weak) IBOutlet UIButton *backgroundButton;
58 |
59 | @property (nonatomic, strong, readonly) NSMutableArray *cards;
60 |
61 | @property (nonatomic, strong) NSTimer *dismissTimer;
62 |
63 | @end
64 |
65 | @implementation TGLViewController
66 |
67 | @synthesize cards = _cards;
68 |
69 | - (instancetype)initWithCoder:(NSCoder *)aDecoder {
70 |
71 | self = [super initWithCoder:aDecoder];
72 |
73 | if (self) {
74 |
75 | _cardCount = 20;
76 | _cardSize = CGSizeZero;
77 |
78 | _stackedLayoutMargin = UIEdgeInsetsMake(20.0, 0.0, 0.0, 0.0);
79 | _stackedTopReveal = 120.0;
80 | _stackedBounceFactor = 0.2;
81 | _stackedFillHeight = NO;
82 | _stackedCenterSingleItem = NO;
83 | _stackedAlwaysBounce = NO;
84 | }
85 |
86 | return self;
87 | }
88 |
89 | - (void)dealloc {
90 |
91 | [self stopDismissTimer];
92 | }
93 |
94 | #pragma mark - View life cycle
95 |
96 | - (void)viewDidLoad {
97 |
98 | [super viewDidLoad];
99 |
100 | // KLUDGE: Using the collection view's `-backgroundView`
101 | // results in layout glitches when transitioning
102 | // between stacked and exposed layouts.
103 | // Therefore we add our background in between
104 | // the collection view and the view controller's
105 | // wrapper view.
106 | //
107 | self.collectionViewBackground.hidden = !self.showsBackgroundView;
108 | self.collectionViewBackground.translatesAutoresizingMaskIntoConstraints = NO;
109 |
110 | [self.view insertSubview:self.collectionViewBackground belowSubview:self.collectionView];
111 |
112 | [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-0-[background]-0-|" options:0 metrics:nil views:@{ @"background": self.collectionViewBackground }]];
113 | [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-0-[background]-0-|" options:0 metrics:nil views:@{ @"background": self.collectionViewBackground }]];
114 |
115 | // KLUDGE: Since our background is below the collection
116 | // view it won't receive any touch events.
117 | // Therefore we install an invisible/empty proxy
118 | // view as the collection view's `-backgroundView`
119 | // with the sole purpose to forward events to
120 | // our background view.
121 | //
122 | TGLBackgroundProxyView *backgroundProxy = [[TGLBackgroundProxyView alloc] init];
123 |
124 | backgroundProxy.targetView = self.collectionViewBackground;
125 | backgroundProxy.hidden = self.collectionViewBackground.hidden;
126 |
127 | self.collectionView.backgroundView = backgroundProxy;
128 | self.collectionView.showsVerticalScrollIndicator = self.showsVerticalScrollIndicator;
129 |
130 | self.exposedItemSize = self.cardSize;
131 |
132 | self.stackedLayout.itemSize = self.exposedItemSize;
133 | self.stackedLayout.layoutMargin = self.stackedLayoutMargin;
134 | self.stackedLayout.topReveal = self.stackedTopReveal;
135 | self.stackedLayout.bounceFactor = self.stackedBounceFactor;
136 | self.stackedLayout.fillHeight = self.stackedFillHeight;
137 | self.stackedLayout.centerSingleItem = self.stackedCenterSingleItem;
138 | self.stackedLayout.alwaysBounce = self.stackedAlwaysBounce;
139 |
140 | if (self.doubleTapToClose) {
141 |
142 | UITapGestureRecognizer *recognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleDoubleTap:)];
143 |
144 | recognizer.delaysTouchesBegan = YES;
145 | recognizer.numberOfTapsRequired = 2;
146 |
147 | [self.collectionView addGestureRecognizer:recognizer];
148 | }
149 | }
150 |
151 | - (void)viewDidAppear:(BOOL)animated {
152 |
153 | [super viewDidAppear:animated];
154 |
155 | if (!self.collectionViewBackground.hidden) {
156 |
157 | // KLUDGE: Make collection view transparent
158 | // to let background view show through
159 | //
160 | // See also: -viewDidLoad
161 | //
162 | self.collectionView.backgroundColor = [UIColor clearColor];
163 | }
164 |
165 | if (self.doubleTapToClose) {
166 |
167 | UIAlertController *alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Double Tap to Close", nil)
168 | message:nil
169 | preferredStyle:UIAlertControllerStyleAlert];
170 |
171 | __weak typeof(self) weakSelf = self;
172 |
173 | UIAlertAction *action = [UIAlertAction actionWithTitle:NSLocalizedString(@"OK", nil)
174 | style:UIAlertActionStyleDefault
175 | handler:^ (UIAlertAction *action) {
176 |
177 | [weakSelf.dismissTimer invalidate];
178 | weakSelf.dismissTimer = nil;
179 | }];
180 |
181 | [alert addAction:action];
182 |
183 | [self presentViewController:alert animated:YES completion:^ (void) {
184 |
185 | self.dismissTimer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(dismissTimerFired:) userInfo:nil repeats:NO];
186 | }];
187 | }
188 | }
189 |
190 | - (UIStatusBarStyle)preferredStatusBarStyle {
191 |
192 | return UIStatusBarStyleLightContent;
193 | }
194 |
195 | #pragma mark - Accessors
196 |
197 | - (void)setCardCount:(NSInteger)cardCount {
198 |
199 | if (cardCount != _cardCount) {
200 |
201 | _cardCount = cardCount;
202 |
203 | _cards = nil;
204 |
205 | if (self.isViewLoaded) [self.collectionView reloadData];
206 | }
207 | }
208 |
209 | - (NSMutableArray *)cards {
210 |
211 | if (_cards == nil) {
212 |
213 | _cards = [NSMutableArray array];
214 |
215 | // Adjust the number of cards here
216 | //
217 | for (NSInteger i = 1; i <= self.cardCount; i++) {
218 |
219 | NSDictionary *card = @{ @"name" : [NSString stringWithFormat:@"Card #%d", (int)i], @"color" : [UIColor randomColor] };
220 |
221 | [_cards addObject:card];
222 | }
223 |
224 | }
225 |
226 | return _cards;
227 | }
228 |
229 | #pragma mark - Key-Value Coding
230 |
231 | - (void)setValue:(id)value forKeyPath:(nonnull NSString *)keyPath {
232 |
233 | // Add key-value coding capabilities for some extra properties
234 | //
235 | if ([keyPath hasPrefix:@"cardSize."]) {
236 |
237 | CGSize cardSize = self.cardSize;
238 |
239 | if ([keyPath hasSuffix:@".width"]) {
240 |
241 | cardSize.width = [value doubleValue];
242 |
243 | } else if ([keyPath hasSuffix:@".height"]) {
244 |
245 | cardSize.height = [value doubleValue];
246 | }
247 |
248 | self.cardSize = cardSize;
249 |
250 | } else if ([keyPath containsString:@"edLayoutMargin."]) {
251 |
252 | NSString *layoutKey = [keyPath componentsSeparatedByString:@"."].firstObject;
253 | UIEdgeInsets layoutMargin = [layoutKey isEqualToString:@"stackedLayoutMargin"] ? self.stackedLayoutMargin : self.exposedLayoutMargin;
254 |
255 | if ([keyPath hasSuffix:@".top"]) {
256 |
257 | layoutMargin.top = [value doubleValue];
258 |
259 | } else if ([keyPath hasSuffix:@".left"]) {
260 |
261 | layoutMargin.left = [value doubleValue];
262 |
263 | } else if ([keyPath hasSuffix:@".right"]) {
264 |
265 | layoutMargin.right = [value doubleValue];
266 | }
267 |
268 | [self setValue:[NSValue valueWithUIEdgeInsets:layoutMargin] forKey:layoutKey];
269 |
270 | } else {
271 |
272 | [super setValue:value forKeyPath:keyPath];
273 | }
274 | }
275 |
276 | #pragma mark - Actions
277 |
278 | - (IBAction)backgroundButtonTapped:(id)sender {
279 |
280 | UIAlertController *alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Background Button Tapped", nil)
281 | message:nil
282 | preferredStyle:UIAlertControllerStyleAlert];
283 |
284 | UIAlertAction *action = [UIAlertAction actionWithTitle:NSLocalizedString(@"OK", nil)
285 | style:UIAlertActionStyleDefault
286 | handler: nil];
287 |
288 | [alert addAction:action];
289 |
290 | [self presentViewController:alert animated:YES completion:nil];
291 | }
292 |
293 | - (IBAction)handleDoubleTap:(UITapGestureRecognizer *)recognizer {
294 |
295 | [self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
296 | }
297 |
298 | - (IBAction)dismissTimerFired:(NSTimer *)timer {
299 |
300 | if (timer == self.dismissTimer && self.presentedViewController) {
301 |
302 | [self dismissViewControllerAnimated:YES completion:^ (void) {
303 |
304 | [self stopDismissTimer];
305 | }];
306 | }
307 | }
308 |
309 | - (IBAction)collapseExposedItem:(id)sender {
310 |
311 | self.exposedItemIndexPath = nil;
312 | }
313 |
314 | #pragma mark - UICollectionViewDataSource protocol
315 |
316 | - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
317 |
318 | return self.cards.count;
319 | }
320 |
321 | - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
322 |
323 | TGLCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"CardCell" forIndexPath:indexPath];
324 | NSDictionary *card = self.cards[indexPath.item];
325 |
326 | cell.title = card[@"name"];
327 | cell.color = card[@"color"];
328 |
329 | return cell;
330 | }
331 |
332 | - (void)collectionView:(UICollectionView *)collectionView moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath {
333 |
334 | // Update data source when moving cards around
335 | //
336 | NSDictionary *card = self.cards[sourceIndexPath.item];
337 |
338 | [self.cards removeObjectAtIndex:sourceIndexPath.item];
339 | [self.cards insertObject:card atIndex:destinationIndexPath.item];
340 | }
341 |
342 | #pragma mark - UICollectionViewDragDelegate protocol
343 |
344 | - (NSArray *)collectionView:(UICollectionView *)collectionView itemsForBeginningDragSession:(id)session atIndexPath:(NSIndexPath *)indexPath NS_AVAILABLE_IOS(11) {
345 |
346 | NSArray *dragItems = [super collectionView:collectionView itemsForBeginningDragSession:session atIndexPath:indexPath];
347 |
348 | // Attach custom drag previews preserving
349 | // cards' rounded corners
350 | //
351 | for (UIDragItem *item in dragItems) {
352 |
353 | item.previewProvider = ^UIDragPreview * {
354 |
355 | UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
356 |
357 | UIDragPreviewParameters *parameters = [[UIDragPreviewParameters alloc] init];
358 | UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:cell.bounds cornerRadius:10.0];
359 |
360 | parameters.visiblePath = path;
361 |
362 | return [[UIDragPreview alloc] initWithView:cell parameters:parameters];
363 | };
364 | }
365 |
366 | return dragItems;
367 | }
368 |
369 | - (UIDragPreviewParameters *)collectionView:(UICollectionView *)collectionView dragPreviewParametersForItemAtIndexPath:(NSIndexPath *)indexPath NS_AVAILABLE_IOS(11) {
370 |
371 | // This seems to be necessary, to preserve
372 | // cards' rounded corners during lift animation
373 | //
374 | UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
375 |
376 | UIDragPreviewParameters *parameters = [[UIDragPreviewParameters alloc] init];
377 | UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:cell.bounds cornerRadius:10.0];
378 |
379 | parameters.visiblePath = path;
380 |
381 | return parameters;
382 | }
383 |
384 | #pragma mark - Helpers
385 |
386 | - (void)stopDismissTimer {
387 |
388 | [self.dismissTimer invalidate];
389 | self.dismissTimer = nil;
390 | }
391 |
392 | @end
393 |
--------------------------------------------------------------------------------
/TGLStackedViewExample/en.lproj/InfoPlist.strings:
--------------------------------------------------------------------------------
1 | /* Localized versions of Info.plist keys */
2 |
3 |
--------------------------------------------------------------------------------
/TGLStackedViewExample/main.m:
--------------------------------------------------------------------------------
1 | //
2 | // main.m
3 | // TGLStackedViewExample
4 | //
5 | // Created by Tim Gleue on 07.04.14.
6 | // Copyright (c) 2014 Tim Gleue • interactive software. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | #import "TGLAppDelegate.h"
12 |
13 | int main(int argc, char * argv[])
14 | {
15 | @autoreleasepool {
16 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([TGLAppDelegate class]));
17 | }
18 | }
19 |
--------------------------------------------------------------------------------