├── .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 | [![Platform](https://img.shields.io/cocoapods/p/TGLStackedViewController.svg?maxAge=86400)](http://cocoadocs.org/docsets/TGLStackedViewController) 2 | [![Tag](https://img.shields.io/github/tag/gleue/TGLStackedViewController.svg?maxAge=86400)](https://github.com/gleue/TGLStackedViewController/tags) 3 | [![CocoaPods](https://img.shields.io/badge/CocoaPods-compatible-4BC51D.svg?style=flat)](https://cocoapods.org) 4 | [![Carthage](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 5 | [![License](https://img.shields.io/github/license/gleue/TGLStackedViewController.svg?maxAge=86400)](https://opensource.org/licenses/MIT) 6 | [![Downloads](https://img.shields.io/cocoapods/dt/TGLStackedViewController.svg?maxAge=86400)](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 | TGLStackedViewExample 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 | 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 | 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 | 100 | 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 | 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 | 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 | 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 | 25 | 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 | --------------------------------------------------------------------------------