├── Example ├── StackedViewKitExample │ ├── en.lproj │ │ └── InfoPlist.strings │ ├── Images │ │ ├── glow.png │ │ ├── 08-chat.png │ │ ├── 11-clock.png │ │ ├── 15-tags.png │ │ ├── background.png │ │ ├── error │ │ │ ├── error.png │ │ │ ├── error.psd │ │ │ └── error@2x.png │ │ └── NewGlow │ │ │ ├── NewGlow.png │ │ │ ├── NewGlow.psd │ │ │ └── NewGlow@2x.png │ ├── PSStackedViewExample-Prefix.pch │ ├── main.m │ ├── ExampleMenuRootController.h │ ├── ExampleViewController2.h │ ├── ExampleViewController1.h │ ├── MenuTableViewCell.h │ ├── UIImage+OverlayColor.h │ ├── AppDelegate.h │ ├── PSStackedViewExample-Info.plist │ ├── ExampleViewController1.m │ ├── AppDelegate.m │ ├── UIImage+OverlayColor.m │ ├── ExampleViewController2.m │ ├── MenuTableViewCell.m │ ├── ExampleMenuRootController.m │ └── ExampleViewController1.xib └── PSStackedViewExample.xcodeproj │ └── project.pbxproj ├── .gitignore ├── PSStackedView ├── PSStackedViewGlobal.m ├── PSStackedView.h ├── UIView+PSSizes.h ├── UIViewController+PSStackedView.h ├── PSStackedViewDelegate.h ├── PSSVContainerView.h ├── UIViewController+PSStackedView.m ├── UIView+PSSizes.m ├── PSStackedViewGlobal.h ├── PSStackedViewController.h ├── PSSVContainerView.m └── PSStackedViewController.m ├── PSStackedView.podspec ├── LICENSE └── README.md /Example/StackedViewKitExample/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /Example/StackedViewKitExample/Images/glow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/PSStackedView/master/Example/StackedViewKitExample/Images/glow.png -------------------------------------------------------------------------------- /Example/StackedViewKitExample/Images/08-chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/PSStackedView/master/Example/StackedViewKitExample/Images/08-chat.png -------------------------------------------------------------------------------- /Example/StackedViewKitExample/Images/11-clock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/PSStackedView/master/Example/StackedViewKitExample/Images/11-clock.png -------------------------------------------------------------------------------- /Example/StackedViewKitExample/Images/15-tags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/PSStackedView/master/Example/StackedViewKitExample/Images/15-tags.png -------------------------------------------------------------------------------- /Example/StackedViewKitExample/Images/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/PSStackedView/master/Example/StackedViewKitExample/Images/background.png -------------------------------------------------------------------------------- /Example/StackedViewKitExample/Images/error/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/PSStackedView/master/Example/StackedViewKitExample/Images/error/error.png -------------------------------------------------------------------------------- /Example/StackedViewKitExample/Images/error/error.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/PSStackedView/master/Example/StackedViewKitExample/Images/error/error.psd -------------------------------------------------------------------------------- /Example/StackedViewKitExample/Images/error/error@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/PSStackedView/master/Example/StackedViewKitExample/Images/error/error@2x.png -------------------------------------------------------------------------------- /Example/StackedViewKitExample/Images/NewGlow/NewGlow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/PSStackedView/master/Example/StackedViewKitExample/Images/NewGlow/NewGlow.png -------------------------------------------------------------------------------- /Example/StackedViewKitExample/Images/NewGlow/NewGlow.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/PSStackedView/master/Example/StackedViewKitExample/Images/NewGlow/NewGlow.psd -------------------------------------------------------------------------------- /Example/StackedViewKitExample/Images/NewGlow/NewGlow@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/PSStackedView/master/Example/StackedViewKitExample/Images/NewGlow/NewGlow@2x.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | build 3 | *.pbxuser 4 | !default.pbxuser 5 | *.mode1v3 6 | !default.mode1v3 7 | *.mode2v3 8 | !default.mode2v3 9 | *.perspectivev3 10 | !default.perspectivev3 11 | *.xcworkspace 12 | !default.xcworkspace 13 | xcuserdata 14 | profile 15 | *.moved-aside 16 | 17 | # osx noise 18 | .DS_Store 19 | -------------------------------------------------------------------------------- /PSStackedView/PSStackedViewGlobal.m: -------------------------------------------------------------------------------- 1 | // 2 | // PSStackedViewGlobal.m 3 | // PSStackedView 4 | // 5 | // Created by Peter Steinberger on 9/8/11. 6 | // Copyright (c) 2011 Peter Steinberger. All rights reserved. 7 | // 8 | 9 | #import "PSStackedView.h" 10 | 11 | PSSVLogLevel kPSSVDebugLogLevel = PSSVLogLevelError; -------------------------------------------------------------------------------- /Example/StackedViewKitExample/PSStackedViewExample-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header for all source files of the 'PSStackedViewExample' target in the 'PSStackedViewExample' project 3 | // 4 | 5 | #import 6 | #ifdef __OBJC__ 7 | #import 8 | #import 9 | #endif 10 | -------------------------------------------------------------------------------- /Example/StackedViewKitExample/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // PSStackedViewExample 4 | // 5 | // Created by Peter Steinberger on 7/14/11. 6 | // Copyright 2011 Peter Steinberger. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | int main(int argc, char *argv[]) 12 | { 13 | @autoreleasepool { 14 | int retVal = UIApplicationMain(argc, argv, nil, @"AppDelegate"); 15 | return retVal; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Example/StackedViewKitExample/ExampleMenuRootController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleMenuRootController.h 3 | // PSStackedViewExample 4 | // 5 | // Created by Peter Steinberger on 7/18/11. 6 | // Copyright 2011 Peter Steinberger. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface ExampleMenuRootController : UIViewController { 12 | UITableView *menuTable_; 13 | NSArray *cellContents_; 14 | } 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /Example/StackedViewKitExample/ExampleViewController2.h: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleViewController2.h 3 | // PSStackedViewExample 4 | // 5 | // Created by Peter Steinberger on 7/14/11. 6 | // Copyright 2011 Peter Steinberger. All rights reserved. 7 | // 8 | 9 | #include "PSStackedViewDelegate.h" 10 | 11 | @interface ExampleViewController2 : UITableViewController { 12 | NSUInteger indexNumber_; 13 | } 14 | 15 | @property(nonatomic, assign) NSUInteger indexNumber; 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /Example/StackedViewKitExample/ExampleViewController1.h: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleViewController1.h 3 | // PSStackedViewExample 4 | // 5 | // Created by Peter Steinberger on 7/14/11. 6 | // Copyright 2011 Peter Steinberger. All rights reserved. 7 | // 8 | 9 | #include "PSStackedViewDelegate.h" 10 | 11 | @interface ExampleViewController1 : UIViewController 12 | 13 | @property(nonatomic, strong) IBOutlet UILabel *indexNumberLabel; 14 | @property(nonatomic, assign) NSUInteger indexNumber; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /PSStackedView.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'PSStackedView' 3 | s.version = '0.1' 4 | s.platform = :ios 5 | s.summary = 'Open source implementation of Twitter/iPad stacked ui - done right.' 6 | s.homepage = 'https://github.com/steipete/PSStackedView' 7 | s.author = { 'Peter Steinberger' => 'steipete@gmail.com' } 8 | s.source = { :git => 'https://github.com/steipete/PSStackedView.git', :tag => '0.1' } 9 | 10 | s.source_files = 'PSStackedView' 11 | s.framework = 'QuartzCore' 12 | s.clean_paths = 'Example' 13 | end 14 | -------------------------------------------------------------------------------- /Example/StackedViewKitExample/MenuTableViewCell.h: -------------------------------------------------------------------------------- 1 | // 2 | // MenuTableViewCell.h 3 | // PSStackedViewExample 4 | // 5 | // Created by Peter Steinberger on 7/14/11. 6 | // Copyright 2011 Peter Steinberger. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface MenuTableViewCell : UITableViewCell { 12 | UIImageView *glowView; 13 | UIImage *savedImage; 14 | } 15 | 16 | @property(nonatomic,strong) UIImageView *glowView; 17 | @property(nonatomic,strong) UIView *disabledView; 18 | @property(nonatomic) BOOL enabled; 19 | 20 | @end 21 | -------------------------------------------------------------------------------- /Example/StackedViewKitExample/UIImage+OverlayColor.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+OverlayColor.h 3 | // PSStackedViewExample 4 | // 5 | // Created by Gregory Combs on 7/28/11. 6 | // Copyright 2011 Peter Steinberger. All rights reserved. 7 | // 8 | // Adapted from Dave Batton's answer on StackOverflow: 9 | // http://stackoverflow.com/questions/1223340/iphone-how-do-you-color-an-image 10 | 11 | #import 12 | 13 | 14 | @interface UIImage(OverlayColor) 15 | 16 | + (UIImage *)invertImageNamed:(NSString *)name; 17 | 18 | - (UIImage *)imageWithOverlayColor:(UIColor *)color; 19 | 20 | @end 21 | -------------------------------------------------------------------------------- /PSStackedView/PSStackedView.h: -------------------------------------------------------------------------------- 1 | // 2 | // PSStackedView.h 3 | // PSStackedView 4 | // 5 | // Created by Peter Steinberger on 7/14/11. 6 | // Copyright 2011 Peter Steinberger. All rights reserved. 7 | // 8 | 9 | #import "PSStackedViewDelegate.h" 10 | #import "PSStackedViewController.h" 11 | #import "PSSVContainerView.h" 12 | #import "UIViewController+PSStackedView.h" 13 | 14 | enum { 15 | PSSVLogLevelNothing, 16 | PSSVLogLevelError, 17 | PSSVLogLevelInfo, 18 | PSSVLogLevelVerbose 19 | }typedef PSSVLogLevel; 20 | 21 | extern PSSVLogLevel kPSSVDebugLogLevel; // defaults to PSSVLogLevelError 22 | -------------------------------------------------------------------------------- /PSStackedView/UIView+PSSizes.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+PSSizes.h 3 | // PSStackedView 4 | // 5 | // Created by Peter Steinberger on 7/14/11. 6 | // Copyright 2011 Peter Steinberger. All rights reserved. 7 | // 8 | 9 | 10 | @interface UIView (PSSizes) 11 | 12 | @property (nonatomic) CGFloat left; 13 | @property (nonatomic) CGFloat top; 14 | @property (nonatomic) CGFloat right; 15 | @property (nonatomic) CGFloat bottom; 16 | @property (nonatomic) CGFloat width; 17 | @property (nonatomic) CGFloat height; 18 | 19 | @property (nonatomic) CGPoint origin; 20 | @property (nonatomic) CGSize size; 21 | 22 | 23 | @end 24 | -------------------------------------------------------------------------------- /Example/StackedViewKitExample/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // PSStackedViewExample 4 | // 5 | // Created by Peter Steinberger on 7/14/11. 6 | // Copyright 2011 Peter Steinberger. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "PSStackedView.h" 11 | 12 | #define XAppDelegate ((AppDelegate *)[[UIApplication sharedApplication] delegate]) 13 | 14 | @class PSStackedViewController; 15 | 16 | @interface AppDelegate : NSObject { 17 | PSStackedViewController *stackController_; 18 | } 19 | 20 | @property (nonatomic, strong) UIWindow *window; 21 | @property (nonatomic, strong, readonly) PSStackedViewController *stackController; 22 | 23 | @end 24 | -------------------------------------------------------------------------------- /PSStackedView/UIViewController+PSStackedView.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+PSStackedView.h 3 | // 3MobileTV 4 | // 5 | // Created by Peter Steinberger on 9/16/11. 6 | // Copyright (c) 2011 Hutchison. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "PSStackedViewGlobal.h" 11 | 12 | @class PSSVContainerView, PSStackedViewController; 13 | 14 | /// category for PSStackedView extensions 15 | @interface UIViewController (PSStackedView) 16 | 17 | - (CGFloat)stackWidth; 18 | - (void)setStackWidth:(CGFloat)stackWidth; 19 | 20 | /// returns the containerView, where view controllers are embedded 21 | - (PSSVContainerView *)containerView; 22 | 23 | /// returns the stack controller if the viewController is embedded 24 | - (PSStackedViewController *)stackController; 25 | 26 | @end -------------------------------------------------------------------------------- /PSStackedView/PSStackedViewDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // PSStackedViewDelegate.h 3 | // PSStackedView 4 | // 5 | // Created by Peter Steinberger on 7/14/11. 6 | // Copyright 2011 Peter Steinberger. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @class PSStackedViewController; 12 | 13 | @protocol PSStackedViewDelegate 14 | 15 | @optional 16 | 17 | /// viewController will be inserted 18 | - (void)stackedView:(PSStackedViewController *)stackedView willInsertViewController:(UIViewController *)viewController; 19 | 20 | /// viewController has been inserted 21 | - (void)stackedView:(PSStackedViewController *)stackedView didInsertViewController:(UIViewController *)viewController; 22 | 23 | /// viewController will be removed 24 | - (void)stackedView:(PSStackedViewController *)stackedView willRemoveViewController:(UIViewController *)viewController; 25 | 26 | /// viewController has been removed 27 | - (void)stackedView:(PSStackedViewController *)stackedView didRemoveViewController:(UIViewController *)viewController; 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | If not noted otherwise in the file header, the project uses the MIT license. 2 | 3 | Copyright (c) 2011, Peter Steinberger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Example/StackedViewKitExample/PSStackedViewExample-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | ${PRODUCT_NAME} 9 | CFBundleExecutable 10 | ${EXECUTABLE_NAME} 11 | CFBundleIconFile 12 | 13 | CFBundleIdentifier 14 | com.psstackedview.${PRODUCT_NAME:rfc1034identifier} 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | ${PRODUCT_NAME} 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | 1.0 23 | CFBundleSignature 24 | ???? 25 | CFBundleVersion 26 | 1.0 27 | LSRequiresIPhoneOS 28 | 29 | UISupportedInterfaceOrientations~ipad 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationPortraitUpsideDown 33 | UIInterfaceOrientationLandscapeLeft 34 | UIInterfaceOrientationLandscapeRight 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /PSStackedView/PSSVContainerView.h: -------------------------------------------------------------------------------- 1 | // 2 | // PSContainerView.h 3 | // PSStackedView 4 | // 5 | // Created by Peter Steinberger on 7/17/11. 6 | // Copyright 2011 Peter Steinberger. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | enum { 13 | PSSVSideNone = 0x0, 14 | PSSVSideRight = 0x01, 15 | PSSVSideLeft = 0x02 16 | }typedef PSSVSide; 17 | 18 | 19 | @interface PSSVContainerView : UIView { 20 | UIView *transparentView_; 21 | CGFloat originalWidth_; 22 | PSSVSide shadow_; 23 | UIViewController *controller_; 24 | CAGradientLayer *leftShadowLayer_; 25 | CAGradientLayer *innerShadowLayer_; 26 | CAGradientLayer *rightShadowLayer_; 27 | } 28 | 29 | + (PSSVContainerView *)containerViewWithController:(UIViewController *)controller; 30 | 31 | /// limit to max width 32 | - (CGFloat)limitToMaxWidth:(CGFloat)maxWidth; 33 | 34 | /// add rounded masks. 35 | /// currently unused, because this needs offscreen-rendering, which is crazy slow 36 | /// as a workaround, fake te rounded corners yourself 37 | - (void)addMaskToCorners:(UIRectCorner)corners; 38 | - (void)removeMask; 39 | 40 | /// update shadow 41 | - (void)updateContainer; 42 | 43 | /// set shadow sides 44 | @property(nonatomic, assign) PSSVSide shadow; 45 | 46 | /// view controller that is being incapsulated 47 | @property(nonatomic, strong) UIViewController *controller; 48 | 49 | /// darken down the view if it's not fully visible 50 | @property(nonatomic, assign) CGFloat darkRatio; 51 | 52 | @end 53 | -------------------------------------------------------------------------------- /Example/StackedViewKitExample/ExampleViewController1.m: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleViewController1.m 3 | // PSStackedViewExample 4 | // 5 | // Created by Peter Steinberger on 7/14/11. 6 | // Copyright 2011 Peter Steinberger. All rights reserved. 7 | // 8 | 9 | #import "ExampleViewController1.h" 10 | #import "PSStackedView.h" 11 | 12 | @implementation ExampleViewController1 13 | 14 | @synthesize indexNumber, indexNumberLabel; 15 | 16 | - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { 17 | if ((self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil])) { 18 | 19 | // random color 20 | self.view.backgroundColor = [UIColor colorWithRed:((float)rand())/RAND_MAX green:((float)rand())/RAND_MAX blue:((float)rand())/RAND_MAX alpha:1.0]; 21 | } 22 | return self; 23 | } 24 | 25 | #pragma mark - View lifecycle 26 | 27 | - (void)viewDidLoad { 28 | [super viewDidLoad]; 29 | 30 | self.view.width = PSIsIpad() ? 600 : 150; 31 | } 32 | 33 | - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { 34 | // Return YES for supported orientations 35 | return (interfaceOrientation == UIInterfaceOrientationPortrait); 36 | } 37 | 38 | - (void)setIndexNumber:(NSUInteger)anIndexNumber { 39 | self.indexNumberLabel.text = [NSString stringWithFormat:@"%d", anIndexNumber]; 40 | } 41 | 42 | 43 | /////////////////////////////////////////////////////////////////////////////////////////////////// 44 | #pragma mark - PSStackedViewDelegate 45 | 46 | - (NSUInteger)stackableMinWidth; { 47 | return 100; 48 | } 49 | 50 | 51 | @end 52 | -------------------------------------------------------------------------------- /Example/StackedViewKitExample/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // PSStackedViewExample 4 | // 5 | // Created by Peter Steinberger on 7/14/11. 6 | // Copyright 2011 Peter Steinberger. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | #import "ExampleMenuRootController.h" 11 | 12 | @interface AppDelegate () 13 | @property (nonatomic, strong) PSStackedViewController *stackController; 14 | @end 15 | 16 | @implementation AppDelegate 17 | 18 | @synthesize window = _window; 19 | @synthesize stackController = stackController_; 20 | 21 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions; { 22 | 23 | self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; 24 | self.window.backgroundColor = [UIColor blackColor]; // really should be default 25 | 26 | // set root controller as stack controller 27 | ExampleMenuRootController *menuController = [[ExampleMenuRootController alloc] init]; 28 | self.stackController = [[PSStackedViewController alloc] initWithRootViewController:menuController]; 29 | self.window.rootViewController = self.stackController; 30 | [self.window makeKeyAndVisible]; 31 | 32 | return YES; 33 | } 34 | 35 | 36 | - (void)applicationWillResignActive:(UIApplication *)application; { 37 | } 38 | 39 | - (void)applicationDidEnterBackground:(UIApplication *)application { 40 | } 41 | 42 | - (void)applicationWillEnterForeground:(UIApplication *)application { 43 | } 44 | 45 | - (void)applicationDidBecomeActive:(UIApplication *)application { 46 | } 47 | 48 | - (void)applicationWillTerminate:(UIApplication *)application { 49 | } 50 | 51 | @end 52 | -------------------------------------------------------------------------------- /PSStackedView/UIViewController+PSStackedView.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+PSStackedView.m 3 | // 3MobileTV 4 | // 5 | // Created by Peter Steinberger on 9/16/11. 6 | // Copyright (c) 2011 Hutchison. All rights reserved. 7 | // 8 | 9 | #import "UIViewController+PSStackedView.h" 10 | #import "PSSVContainerView.h" 11 | #import "PSStackedViewController.h" 12 | #import 13 | 14 | #define kPSSVAssociatedStackViewControllerWidth @"kPSSVAssociatedStackViewControllerWidth" 15 | 16 | @implementation UIViewController (PSStackedView) 17 | 18 | // returns the containerView, where view controllers are embedded 19 | - (PSSVContainerView *)containerView; { 20 | return ([self.view.superview isKindOfClass:[PSSVContainerView class]] ? (PSSVContainerView *)self.view.superview : nil); 21 | } 22 | 23 | // returns the stack controller if the viewController is embedded 24 | - (PSStackedViewController *)stackController; { 25 | PSStackedViewController *stackController = objc_getAssociatedObject(self, kPSSVAssociatedStackViewControllerKey); 26 | return stackController; 27 | } 28 | 29 | #ifdef ALLOW_SWIZZLING_NAVIGATIONCONTROLLER 30 | /// to maintain minimal changes for your app, we can do some clever swizzling here. 31 | - (UINavigationController *)navigationControllerSwizzled { 32 | if (!self.navigationControllerSwizzled) { 33 | return (UINavigationController *)self.stackController; 34 | }else { 35 | return self.navigationController; 36 | } 37 | } 38 | #endif 39 | 40 | - (CGFloat)stackWidth { 41 | NSNumber *stackWidthNumber = objc_getAssociatedObject(self, kPSSVAssociatedStackViewControllerWidth); 42 | CGFloat stackWidth = stackWidthNumber ? [stackWidthNumber floatValue] : 0.f; 43 | return stackWidth; 44 | } 45 | 46 | - (void)setStackWidth:(CGFloat)stackWidth { 47 | NSNumber *stackWidthNumber = [NSNumber numberWithFloat:stackWidth]; 48 | objc_setAssociatedObject(self, kPSSVAssociatedStackViewControllerWidth, stackWidthNumber, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 49 | } 50 | 51 | @end -------------------------------------------------------------------------------- /Example/StackedViewKitExample/UIImage+OverlayColor.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+OverlayColor.m 3 | // PSStackedViewExample 4 | // 5 | // Created by Gregory Combs on 7/28/11. 6 | // Copyright 2011 Peter Steinberger. All rights reserved. 7 | // 8 | // Adapted from Dave Batton's answer on StackOverflow: 9 | // http://stackoverflow.com/questions/1223340/iphone-how-do-you-color-an-image 10 | 11 | #import "UIImage+OverlayColor.h" 12 | 13 | 14 | @implementation UIImage(OverlayColor) 15 | 16 | // I'm not completely happy with this. It looks like there's a little blurring on some images. (Greg) 17 | 18 | - (UIImage *)imageWithOverlayColor:(UIColor *)color 19 | { 20 | CGRect rect = CGRectMake(0.0f, 0.0f, self.size.width, self.size.height); 21 | 22 | if (UIGraphicsBeginImageContextWithOptions) { 23 | CGFloat imageScale = 1.0f; 24 | if ([self respondsToSelector:@selector(scale)]) 25 | imageScale = self.scale; 26 | UIGraphicsBeginImageContextWithOptions(self.size, NO, imageScale); 27 | } 28 | else { 29 | UIGraphicsBeginImageContext(self.size); 30 | } 31 | 32 | [self drawInRect:rect]; 33 | 34 | CGContextRef context = UIGraphicsGetCurrentContext(); 35 | CGContextSetBlendMode(context, kCGBlendModeSourceIn); 36 | 37 | CGContextSetFillColorWithColor(context, color.CGColor); 38 | CGContextFillRect(context, rect); 39 | 40 | UIImage *outImage = UIGraphicsGetImageFromCurrentImageContext(); 41 | UIGraphicsEndImageContext(); 42 | 43 | return outImage; 44 | } 45 | 46 | + (UIImage *)invertImageNamed:(NSString *)name { 47 | 48 | UIColor *offWhite = [UIColor colorWithRed:(245.f/255.f) 49 | green:(245.f/255.f) 50 | blue:(245.f/255.f) 51 | alpha:1.f]; 52 | 53 | // offWhite looks nice on a scroll view background, however, 54 | // a suitable alternative is [UIColor whiteColor] 55 | return [[UIImage imageNamed:name] imageWithOverlayColor:offWhite]; 56 | } 57 | 58 | @end 59 | -------------------------------------------------------------------------------- /PSStackedView/UIView+PSSizes.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+PSSizes.m 3 | // PSStackedView 4 | // 5 | // Created by Peter Steinberger on 7/14/11. 6 | // Copyright 2011 Peter Steinberger. All rights reserved. 7 | // 8 | 9 | #import "UIView+PSSizes.h" 10 | 11 | @implementation UIView (PSSizes) 12 | 13 | - (CGFloat)left { 14 | return self.frame.origin.x; 15 | } 16 | 17 | - (void)setLeft:(CGFloat)x { 18 | CGRect frame = self.frame; 19 | frame.origin.x = x; 20 | self.frame = frame; 21 | } 22 | 23 | - (CGFloat)top { 24 | return self.frame.origin.y; 25 | } 26 | 27 | - (void)setTop:(CGFloat)y { 28 | CGRect frame = self.frame; 29 | frame.origin.y = y; 30 | self.frame = frame; 31 | } 32 | 33 | - (CGFloat)right { 34 | return self.frame.origin.x + self.frame.size.width; 35 | } 36 | 37 | - (void)setRight:(CGFloat)right { 38 | CGRect frame = self.frame; 39 | frame.origin.x = right - frame.size.width; 40 | self.frame = frame; 41 | } 42 | 43 | - (CGFloat)bottom { 44 | return self.frame.origin.y + self.frame.size.height; 45 | } 46 | 47 | - (void)setBottom:(CGFloat)bottom { 48 | CGRect frame = self.frame; 49 | frame.origin.y = bottom - frame.size.height; 50 | self.frame = frame; 51 | } 52 | 53 | - (CGFloat)width { 54 | return self.frame.size.width; 55 | } 56 | 57 | - (void)setWidth:(CGFloat)width { 58 | CGRect frame = self.frame; 59 | frame.size.width = width; 60 | self.frame = frame; 61 | } 62 | 63 | - (CGFloat)height { 64 | return self.frame.size.height; 65 | } 66 | 67 | - (void)setHeight:(CGFloat)height { 68 | CGRect frame = self.frame; 69 | frame.size.height = height; 70 | self.frame = frame; 71 | } 72 | 73 | - (CGPoint)origin { 74 | return self.frame.origin; 75 | } 76 | 77 | - (void)setOrigin:(CGPoint)origin { 78 | CGRect frame = self.frame; 79 | frame.origin = origin; 80 | self.frame = frame; 81 | } 82 | 83 | - (CGSize)size { 84 | return self.frame.size; 85 | } 86 | 87 | - (void)setSize:(CGSize)size { 88 | CGRect frame = self.frame; 89 | frame.size = size; 90 | self.frame = frame; 91 | } 92 | 93 | @end 94 | -------------------------------------------------------------------------------- /PSStackedView/PSStackedViewGlobal.h: -------------------------------------------------------------------------------- 1 | // 2 | // PSStackedViewGlobal.h 3 | // PSStackedView 4 | // 5 | // Created by Peter Steinberger on 7/14/11. 6 | // Copyright 2011 Peter Steinberger. All rights reserved. 7 | // 8 | 9 | #import "UIView+PSSizes.h" 10 | 11 | // Swizzles UIViewController's navigationController property. DANGER, WILL ROBINSON! 12 | // Only swizzles if a PSStackedViewRootController is created, and also works in peaceful 13 | // coexistance to UINavigationController. 14 | //#define ALLOW_SWIZZLING_NAVIGATIONCONTROLLER 15 | 16 | #define kPSSVAssociatedStackViewControllerKey @"kPSSVAssociatedStackViewController" 17 | 18 | #define kPSSVStackedViewKitDebugEnabled 19 | 20 | #define PSIsIpad() ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) 21 | #define PSAppStatusBarOrientation ([[UIApplication sharedApplication] statusBarOrientation]) 22 | #define PSIsPortrait() UIInterfaceOrientationIsPortrait(PSAppStatusBarOrientation) 23 | #define PSIsLandscape() UIInterfaceOrientationIsLandscape(PSAppStatusBarOrientation) 24 | 25 | #ifdef kPSSVStackedViewKitDebugEnabled 26 | #define PSSVLogVerbose(fmt, ...) do { if(kPSSVDebugLogLevel >= PSSVLogLevelVerbose) NSLog((@"%s/%d " fmt), __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__); }while(0) 27 | #define PSSVLog(fmt, ...) do { if(kPSSVDebugLogLevel >= PSSVLogLevelInfo) NSLog((@"%s/%d " fmt), __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__); }while(0) 28 | #define PSSVLogError(fmt, ...) do { if(kPSSVDebugLogLevel >= PSSVLogLevelError) NSLog((@"Error: %s/%d " fmt), __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__); }while(0) 29 | #else 30 | #define PSSVLogVerbose(...) 31 | #define PSSVLog(...) 32 | #define PSVSLogError(...) 33 | #endif 34 | 35 | 36 | #ifndef kCFCoreFoundationVersionNumber_iPhoneOS_4_0 37 | #define kCFCoreFoundationVersionNumber_iPhoneOS_4_0 550.32 38 | #endif 39 | #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 40000 40 | #define IF_IOS4_OR_GREATER(...) \ 41 | if (kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber_iPhoneOS_4_0) \ 42 | { \ 43 | __VA_ARGS__ \ 44 | } 45 | #else 46 | #define IF_IOS4_OR_GREATER(...) 47 | #endif 48 | 49 | #define IF_PRE_IOS4(...) \ 50 | if (kCFCoreFoundationVersionNumber < kCFCoreFoundationVersionNumber_iPhoneOS_4_0) \ 51 | { \ 52 | __VA_ARGS__ \ 53 | } 54 | 55 | #ifndef kCFCoreFoundationVersionNumber_iPhoneOS_5_0 56 | #define kCFCoreFoundationVersionNumber_iPhoneOS_5_0 674.0 57 | #endif 58 | #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 40000 59 | #define IF_IOS5_OR_GREATER(...) \ 60 | if (kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber_iPhoneOS_5_0) \ 61 | { \ 62 | __VA_ARGS__ \ 63 | } 64 | #else 65 | #define IF_IOS5_OR_GREATER(...) 66 | #endif 67 | 68 | #define IF_PRE_IOS5(...) \ 69 | if (kCFCoreFoundationVersionNumber < kCFCoreFoundationVersionNumber_iPhoneOS_5_0) \ 70 | { \ 71 | __VA_ARGS__ \ 72 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## PSStackedView - put your UIViewControllers in a stack, inspired by Twitter's iPAD UI. 2 | 3 | After reviewing other stacked implementations, i wrote my own solutions from scratch. 4 | This one lets you add plain UIViewControllers to a PSStackedViewRootViewController, working much like a UINavigationController. 5 | 6 | All the hard parts, moving, shadows, rounded borders is taken care of. 7 | 8 | I made it for the iPad-Version of the popular austrian TV app ["3MobileTV"](http://itunes.apple.com/at/app/3mobiletv/id404154552?mt=8). 9 | (You need an austrian 3-SIM to test it, but you can check out the screenshots to get the idea what's possible with it). 10 | 11 | Currently there is a positioning bug with small view controllers, I'll fix that in the foreseeable future. Otherwise, it's pretty much a drop-in-replacement for UINavigationController, using regular UIViewControllers. It supports iOS4 upwards, with some special support for iOS5's new view controller containment coming. 12 | 13 | It works on the iPad and the iPhone, but the _concept_ is better suited for the iPad. 14 | 15 | ![PSStackedView](http://f.cl.ly/items/2O1p18263a2Q27223R3h/Screen%20Shot%202011-11-01%20at%206.03.02%20PM.png) 16 | 17 | ... and custom-skinned, you can build pretty hot interfaces: 18 | 19 | [![PSStackedView](http://f.cl.ly/items/2Z0w0D1P0y1h2N1V3d1t/mzl.svmxiutd.png)](http://itunes.apple.com/at/app/3mobiletv/id404154552?mt=8) 20 | 21 | ## Getting Started 22 | 23 | Much like UINavigationController, it's a good idea to put your PSStackedViewRootController in the AppDelegate: 24 | 25 | ```objc 26 | @property (nonatomic, retain) PSStackedViewRootController *stackController; 27 | ``` 28 | 29 | Create the stack in application:didFinishLaunchingWithOptions: 30 | 31 | ```objc 32 | ExampleMenuRootController *menuController = [[[ExampleMenuRootController alloc] init] autorelease]; 33 | self.stackController = [[[PSStackedViewRootController alloc] initWithRootViewController:menuController] autorelease]; 34 | [self.stackController pushViewController:demoViewController fromViewController:nil animated:NO]; 35 | window.rootViewController = self.stackController; 36 | ``` 37 | 38 | PSStackedViewRootController's rootViewController is in the background and its left part is always visible. Adjust the size with leftInset and largeLeftInset. 39 | 40 | ## Roadmap 41 | - Add (conditional) support for the new child view controller system in iOS5 42 | - Appledoc 43 | - lots more 44 | 45 | ## License 46 | Licensed under MIT. Use it for whatever you want, in commercial apps or open source. 47 | I just wand a little contribution somewhere in your about box. 48 | 49 | ## Alternatives 50 | There are some open source and commerical stacked implementations out there, yet none of them were flexible enough to fit my needs. 51 | Special thanks to Cocoacontrols for [this article](http://cocoacontrols.com/posts/how-to-build-the-twitter-ipad-user-experience). 52 | 53 | * [StackScrollView](https://github.com/raweng/StackScrollView) (BSD) 54 | * [CLCascade](https://github.com/creativelabs/CLCascade) (Apache 2.0) 55 | * [stackcordion.git](https://github.com/openfinancedev/stackcordion.git) (CCPL) -------------------------------------------------------------------------------- /Example/StackedViewKitExample/ExampleViewController2.m: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleViewController2.m 3 | // PSStackedViewExample 4 | // 5 | // Created by Peter Steinberger on 7/14/11. 6 | // Copyright 2011 Peter Steinberger. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | #import "ExampleViewController1.h" 11 | #import "ExampleViewController2.h" 12 | 13 | @implementation ExampleViewController2 14 | 15 | @synthesize indexNumber = indexNumber_; 16 | 17 | - (id)initWithStyle:(UITableViewStyle)style { 18 | if ((self = [super initWithStyle:style])) { 19 | 20 | // random color 21 | self.view.backgroundColor = [UIColor colorWithRed:((float)rand())/RAND_MAX green:((float)rand())/RAND_MAX blue:((float)rand())/RAND_MAX alpha:1.0]; 22 | 23 | } 24 | return self; 25 | } 26 | 27 | 28 | #pragma mark - View lifecycle 29 | 30 | - (void)viewDidLoad { 31 | [super viewDidLoad]; 32 | 33 | self.view.width = PSIsIpad() ? 550 : 100; 34 | } 35 | 36 | 37 | - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { 38 | // Return YES for supported orientations 39 | return (interfaceOrientation == UIInterfaceOrientationPortrait); 40 | } 41 | 42 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { 43 | return 1; 44 | } 45 | 46 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 47 | return 100; 48 | } 49 | 50 | // Customize the appearance of table view cells. 51 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 52 | static NSString *CellIdentifier = @"Example2Cell"; 53 | 54 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; 55 | if (cell == nil) { 56 | cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]; 57 | } 58 | 59 | cell.textLabel.text = [NSString stringWithFormat:@"[%d] Cell %d", self.indexNumber, indexPath.row]; 60 | 61 | return cell; 62 | } 63 | 64 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { 65 | UIViewController *viewController; 66 | if (indexPath.row == 0) { 67 | viewController = [[ExampleViewController1 alloc] initWithNibName:@"ExampleViewController1" bundle:nil]; 68 | }else { 69 | viewController = [[ExampleViewController2 alloc] initWithStyle:UITableViewStylePlain]; 70 | } 71 | 72 | [XAppDelegate.stackController pushViewController:viewController fromViewController:self animated:YES]; 73 | ((ExampleViewController1 *)viewController).indexNumber = [[XAppDelegate.stackController viewControllers] count] - 1; 74 | } 75 | 76 | - (void)setIndexNumber:(NSUInteger)anIndexNumber { 77 | indexNumber_ = anIndexNumber; 78 | [self.tableView reloadData]; 79 | } 80 | 81 | 82 | 83 | /////////////////////////////////////////////////////////////////////////////////////////////////// 84 | #pragma mark - PSStackedViewDelegate 85 | 86 | - (NSUInteger)stackableMinWidth; { 87 | return 100; 88 | } 89 | 90 | 91 | @end 92 | -------------------------------------------------------------------------------- /Example/StackedViewKitExample/MenuTableViewCell.m: -------------------------------------------------------------------------------- 1 | // 2 | // MenuTableViewCell.m 3 | // PSStackedViewExample 4 | // 5 | // Created by Peter Steinberger on 7/14/11. 6 | // Copyright 2011 Peter Steinberger. All rights reserved. 7 | // 8 | 9 | #import "MenuTableViewCell.h" 10 | 11 | @implementation MenuTableViewCell 12 | 13 | @synthesize glowView; 14 | @synthesize disabledView; 15 | @synthesize enabled; 16 | 17 | - (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { 18 | if ((self = [super initWithStyle:style reuseIdentifier:reuseIdentifier])) { 19 | savedImage = nil; 20 | enabled = YES; 21 | 22 | self.clipsToBounds = YES; 23 | 24 | UIView* bgView = [[UIView alloc] init]; 25 | bgView.backgroundColor = [UIColor colorWithWhite:0.f alpha:0.25f]; 26 | self.selectedBackgroundView = bgView; 27 | 28 | self.textLabel.font = [UIFont boldSystemFontOfSize:[UIFont systemFontSize]]; 29 | self.textLabel.shadowOffset = CGSizeMake(0, 2); 30 | self.textLabel.shadowColor = [UIColor colorWithWhite:0 alpha:0.25]; 31 | 32 | self.imageView.contentMode = UIViewContentModeCenter; 33 | 34 | UIView *topLine = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 1)]; 35 | topLine.backgroundColor = [UIColor colorWithWhite:0.5 alpha:0.25]; 36 | [self.textLabel.superview addSubview:topLine]; 37 | 38 | UIView *bottomLine = [[UIView alloc] initWithFrame:CGRectMake(0, 43, 200, 1)]; 39 | bottomLine.backgroundColor = [UIColor colorWithWhite:0 alpha:0.25]; 40 | [self.textLabel.superview addSubview:bottomLine]; 41 | 42 | glowView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 30, 43)]; 43 | glowView.image = [UIImage imageNamed:@"NewGlow"]; 44 | glowView.hidden = YES; 45 | [self addSubview:glowView]; 46 | } 47 | return self; 48 | } 49 | 50 | - (void)layoutSubviews { 51 | [super layoutSubviews]; 52 | 53 | self.textLabel.frame = CGRectMake(75, 0, 125, 43); 54 | self.imageView.frame = CGRectMake(0, 0, 70, 43); 55 | } 56 | 57 | - (void)setSelected:(BOOL)sel animated:(BOOL)animated { 58 | [super setSelected:sel animated:animated]; 59 | 60 | if (sel) { 61 | self.glowView.hidden = NO; 62 | self.textLabel.textColor = [UIColor whiteColor]; 63 | } 64 | else { 65 | self.glowView.hidden = YES; 66 | self.textLabel.textColor = [UIColor colorWithRed:(188.f/255.f) 67 | green:(188.f/255.f) 68 | blue:(188.f/255.f) 69 | alpha:1.f]; 70 | } 71 | } 72 | 73 | - (void)setEnabled:(BOOL)newValue { 74 | enabled = newValue; 75 | 76 | if (self.enabled) { 77 | if (self.disabledView) { 78 | // Remove the "dimmed" view, if there is one. (see below) 79 | [self.disabledView removeFromSuperview]; 80 | self.disabledView = nil; 81 | } 82 | 83 | if (savedImage) { 84 | self.imageView.image = savedImage; 85 | savedImage = nil; 86 | } 87 | 88 | // Reenable user interaction and selection ability 89 | self.selectionStyle = UITableViewCellSelectionStyleBlue; 90 | self.userInteractionEnabled = YES; 91 | } 92 | else { 93 | /* Create the appearance of a "dimmed" table cell, with a standard error icon */ 94 | UIView *newView = [[UIView alloc] initWithFrame:self.bounds]; 95 | newView.backgroundColor = [UIColor colorWithWhite:.5f alpha:.5f]; 96 | 97 | if (self.imageView.image) { 98 | savedImage = self.imageView.image; 99 | self.imageView.image = [UIImage imageNamed:@"error"]; 100 | } 101 | else { 102 | UIImageView *error = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"error"]]; 103 | CGFloat imgDim = 24.f; 104 | // set the error image's frame origin to be on the far right side of the table view cell 105 | CGRect frm = CGRectMake(195.f - imgDim , roundf((self.bounds.size.height/2) - (imgDim/2)), imgDim, imgDim); 106 | error.frame = frm; 107 | [newView addSubview:error]; 108 | } 109 | [self addSubview:newView]; 110 | [self bringSubviewToFront:newView]; 111 | self.disabledView = newView; 112 | 113 | // Disable future user interaction and selections 114 | self.selectionStyle = UITableViewCellSelectionStyleNone; 115 | self.userInteractionEnabled = NO; 116 | 117 | // Turn off any current selections/highlights 118 | if (self.selected) { 119 | self.selected = NO; 120 | } 121 | if (self.highlighted) { 122 | self.highlighted = NO; 123 | } 124 | } 125 | [self setNeedsDisplay]; 126 | } 127 | 128 | 129 | @end 130 | -------------------------------------------------------------------------------- /PSStackedView/PSStackedViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // SVStackRootController.h 3 | // PSStackedView 4 | // 5 | // Created by Peter Steinberger on 7/14/11. 6 | // Copyright 2011 Peter Steinberger. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "PSStackedViewGlobal.h" 11 | #import "PSStackedViewDelegate.h" 12 | 13 | /// grid snapping options 14 | enum { 15 | SVSnapOptionNearest, 16 | SVSnapOptionLeft, 17 | SVSnapOptionRight 18 | } typedef PSSVSnapOption; 19 | 20 | /// StackController hosing a backside rootViewController and the stacked controllers 21 | @interface PSStackedViewController : UIViewController 22 | 23 | /// the root controller gets the whole background view 24 | - (id)initWithRootViewController:(UIViewController *)rootViewController; 25 | 26 | /// Uses a horizontal slide transition. Has no effect if the view controller is already in the stack. 27 | /// baseViewController is used to remove subviews if a previous controller invokes a new view. can be nil. 28 | - (void)pushViewController:(UIViewController *)viewController fromViewController:(UIViewController *)baseViewController animated:(BOOL)animated; 29 | 30 | /// pushes the view controller, sets the last current vc as parent controller 31 | - (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated; 32 | 33 | /// remove top view controller from stack, return it 34 | - (UIViewController *)popViewControllerAnimated:(BOOL)animated; 35 | 36 | /// remove specific view controller. returns false if controller is not on top of stack 37 | - (BOOL)popViewController:(UIViewController *)controller animated:(BOOL)animated; 38 | 39 | /// remove view controllers until 'viewController' is found 40 | - (NSArray *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated; 41 | 42 | /// removes all view controller 43 | - (NSArray *)popToRootViewControllerAnimated:(BOOL)animated; 44 | 45 | /// return all controllers of certain class 46 | - (NSArray *)controllersForClass:(Class)theClass; 47 | 48 | /// can we collapse (= hide) view controllers? Only collapses until screen width is used 49 | - (NSUInteger)canCollapseStack; 50 | 51 | /// can the stack be further expanded (are some views stacked?) 52 | - (NSUInteger)canExpandStack; 53 | 54 | /// moves view controller stack to the left, potentially hiding older VCs (increases firstVisibleIndex) 55 | - (NSUInteger)collapseStack:(NSInteger)steps animated:(BOOL)animated; 56 | 57 | /// move view controller stack to the right, showing older VCs (decreases firstVisibleIndex) 58 | - (NSUInteger)expandStack:(NSInteger)steps animated:(BOOL)animated; 59 | 60 | /// align stack to nearest grid 61 | - (void)alignStackAnimated:(BOOL)animated; 62 | 63 | /// expands/collapses stack until entered index is topmost right 64 | - (void)displayViewControllerIndexOnRightMost:(NSInteger)index animated:(BOOL)animated; 65 | 66 | /// expands/collapses stack until entered controller is topmost right 67 | - (BOOL)displayViewControllerOnRightMost:(UIViewController *)vc animated:(BOOL)animated; 68 | 69 | /// return view controllers that follow a certain view controller. Helper function. 70 | - (NSArray *)viewControllersAfterViewController:(UIViewController *)viewController; 71 | 72 | /// index of current view controller. Supports search within UINavigationControllers. 73 | - (NSInteger)indexOfViewController:(UIViewController *)viewController; 74 | 75 | /// event delegate 76 | @property(nonatomic, unsafe_unretained) id delegate; 77 | 78 | /// root view controller, always displayed behind stack 79 | @property(nonatomic, strong, readonly) UIViewController *rootViewController; 80 | 81 | /// The top(last) view controller on the stack. 82 | @property(nonatomic, readonly, strong) UIViewController *topViewController; 83 | 84 | /// first view controller 85 | @property(nonatomic, readonly, strong) UIViewController *firstViewController; 86 | 87 | /// represents current state via floating point. shows edge attaches, menu docking, etc 88 | @property(nonatomic, readonly, assign) CGFloat floatIndex; 89 | 90 | /// view controllers visible. NOT KVO compliant, is calculated on demand. 91 | @property(nonatomic, readonly, strong) NSArray *visibleViewControllers; 92 | 93 | @property(nonatomic, readonly, strong) NSArray *fullyVisibleViewControllers; 94 | 95 | /// index of first currently visible view controller [calculated] 96 | @property(nonatomic, assign, readonly) NSInteger firstVisibleIndex; 97 | 98 | /// index of last currently visible view controller [calculated] 99 | @property(nonatomic, assign, readonly) NSInteger lastVisibleIndex; 100 | 101 | /// array of all current view controllers, sorted 102 | @property(nonatomic, strong, readonly) NSArray *viewControllers; 103 | 104 | /// pangesture recognizer used 105 | @property(nonatomic, strong) UIPanGestureRecognizer *panRecognizer; 106 | 107 | /// enable if you show another object in fullscreen, but stacked view still thinks it's displayed 108 | /// reduces animations to a minimum to get smoother reactions on frontmost view. 109 | @property(nonatomic, assign, getter=isReducingAnimations) BOOL reduceAnimations; 110 | 111 | /// left inset thats always visible. Defaults to 60. 112 | @property(nonatomic, assign) NSUInteger leftInset; 113 | /// animate setting of the left inset that is always visible 114 | - (void)setLeftInset:(NSUInteger)leftInset animated:(BOOL)animated; 115 | 116 | /// large left inset. is visible to show you the full menu width. Defaults to 200. 117 | @property(nonatomic, assign) NSUInteger largeLeftInset; 118 | /// animate setting of large left inset 119 | - (void)setLargeLeftInset:(NSUInteger)largeLeftInset animated:(BOOL)animated; 120 | 121 | // compatibility with UINavigationBar -- returns nil 122 | #ifdef ALLOW_SWIZZLING_NAVIGATIONCONTROLLER 123 | @property(nonatomic, assign) UINavigationBar *navigationBar; 124 | #endif 125 | 126 | @end 127 | -------------------------------------------------------------------------------- /Example/StackedViewKitExample/ExampleMenuRootController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleMenuRootController.m 3 | // PSStackedViewExample 4 | // 5 | // Created by Peter Steinberger on 7/18/11. 6 | // Copyright 2011 Peter Steinberger. All rights reserved. 7 | // 8 | 9 | #import "PSStackedView.h" 10 | #import "AppDelegate.h" 11 | #import "MenuTableViewCell.h" 12 | #import "ExampleMenuRootController.h" 13 | #import "ExampleViewController1.h" 14 | #import "ExampleViewController2.h" 15 | #import "UIImage+OverlayColor.h" 16 | 17 | #include 18 | 19 | #define kMenuWidth 200 20 | #define kCellText @"CellText" 21 | #define kCellImage @"CellImage" 22 | 23 | @interface ExampleMenuRootController() 24 | @property (nonatomic, strong) UITableView *menuTable; 25 | @property (nonatomic, strong) NSArray *cellContents; 26 | @end 27 | 28 | @implementation ExampleMenuRootController 29 | 30 | @synthesize menuTable = menuTable_; 31 | @synthesize cellContents = cellContents_; 32 | 33 | /////////////////////////////////////////////////////////////////////////////////////////////////// 34 | #pragma mark - NSObject 35 | 36 | 37 | 38 | /////////////////////////////////////////////////////////////////////////////////////////////////// 39 | #pragma mark - UIView 40 | 41 | - (void)viewDidLoad { 42 | [super viewDidLoad]; 43 | NSLog(@"load example view, frame: %@", NSStringFromCGRect(self.view.frame)); 44 | 45 | #if 0 46 | self.view.layer.borderColor = [UIColor greenColor].CGColor; 47 | self.view.layer.borderWidth = 2.f; 48 | #endif 49 | 50 | // add example background 51 | self.view.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"background"]]; 52 | 53 | // prepare menu content 54 | NSMutableArray *contents = [[NSMutableArray alloc] init]; 55 | [contents addObject:[NSDictionary dictionaryWithObjectsAndKeys:[UIImage invertImageNamed:@"08-chat"], kCellImage, NSLocalizedString(@"Example1",@""), kCellText, nil]]; 56 | [contents addObject:[NSDictionary dictionaryWithObjectsAndKeys:[UIImage invertImageNamed:@"11-clock"], kCellImage, NSLocalizedString(@"Example2",@""), kCellText, nil]]; 57 | [contents addObject:[NSDictionary dictionaryWithObjectsAndKeys:[UIImage invertImageNamed:@"15-tags"], kCellImage, NSLocalizedString(@" ",@""), kCellText, nil]]; 58 | [contents addObject:[NSDictionary dictionaryWithObjectsAndKeys:[UIImage invertImageNamed:@"08-chat"], kCellImage, NSLocalizedString(@"<- Collapse",@""), kCellText, nil]]; 59 | [contents addObject:[NSDictionary dictionaryWithObjectsAndKeys:[UIImage invertImageNamed:@"11-clock"], kCellImage, NSLocalizedString(@"Expand ->",@""), kCellText, nil]]; 60 | [contents addObject:[NSDictionary dictionaryWithObjectsAndKeys:[UIImage invertImageNamed:@"15-tags"], kCellImage, NSLocalizedString(@"Clear All",@""), kCellText, nil]]; 61 | self.cellContents = contents; 62 | 63 | // add table menu 64 | UITableView *tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 0, kMenuWidth, self.view.height) style:UITableViewStylePlain]; 65 | self.menuTable = tableView; 66 | 67 | self.menuTable.backgroundColor = [UIColor clearColor]; 68 | self.menuTable.delegate = self; 69 | self.menuTable.dataSource = self; 70 | [self.view addSubview:self.menuTable]; 71 | [self.menuTable reloadData]; 72 | } 73 | 74 | /////////////////////////////////////////////////////////////////////////////////////////////////// 75 | #pragma mark - UITableViewDataSource 76 | 77 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { 78 | return 1; 79 | } 80 | 81 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 82 | return [cellContents_ count]; 83 | } 84 | 85 | // Customize the appearance of table view cells. 86 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 87 | static NSString *CellIdentifier = @"ExampleMenuCell"; 88 | 89 | MenuTableViewCell *cell = (MenuTableViewCell*)[tableView dequeueReusableCellWithIdentifier:CellIdentifier]; 90 | if (cell == nil) { 91 | cell = [[MenuTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]; 92 | } 93 | 94 | cell.textLabel.text = [[cellContents_ objectAtIndex:indexPath.row] objectForKey:kCellText]; 95 | cell.imageView.image = [[cellContents_ objectAtIndex:indexPath.row] objectForKey:kCellImage]; 96 | 97 | //if (indexPath.row == 5) 98 | // cell.enabled = NO; 99 | 100 | return cell; 101 | } 102 | 103 | 104 | /////////////////////////////////////////////////////////////////////////////////////////////////// 105 | #pragma mark - UITableViewDelegate 106 | 107 | - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section { 108 | return 0; 109 | } 110 | 111 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { 112 | PSStackedViewController *stackController = XAppDelegate.stackController; 113 | UIViewController*viewController = nil; 114 | 115 | 116 | if (indexPath.row < 3) { 117 | // Pop everything off the stack to start a with a fresh app feature 118 | // DISABLED FOR DEBUGGING 119 | //[stackController popToRootViewControllerAnimated:YES]; 120 | } 121 | 122 | if (indexPath.row == 0) { 123 | viewController = [[ExampleViewController1 alloc] initWithNibName:@"ExampleViewController1" bundle:nil]; 124 | ((ExampleViewController1 *)viewController).indexNumber = [stackController.viewControllers count]; 125 | }else if(indexPath.row == 1) { 126 | viewController = [[ExampleViewController2 alloc] initWithStyle:UITableViewStylePlain]; 127 | ((ExampleViewController2 *)viewController).indexNumber = [stackController.viewControllers count]; 128 | }else if(indexPath.row == 2) { // Twitter style 129 | viewController = [[ExampleViewController1 alloc] initWithNibName:@"ExampleViewController1" bundle:nil]; 130 | ((ExampleViewController1 *)viewController).indexNumber = [stackController.viewControllers count]; 131 | viewController.view.width = roundf((self.view.width - stackController.leftInset)/2); 132 | } 133 | else if(indexPath.row == 3) { 134 | [stackController collapseStack:1 animated:YES]; 135 | }else if(indexPath.row == 4) { // right 136 | [stackController expandStack:1 animated:YES]; 137 | }else if(indexPath.row == 5) { 138 | while ([stackController.viewControllers count]) { 139 | [stackController popViewControllerAnimated:YES]; 140 | } 141 | } 142 | 143 | if (viewController) { 144 | [XAppDelegate.stackController pushViewController:viewController fromViewController:nil animated:YES]; 145 | } 146 | } 147 | 148 | @end 149 | -------------------------------------------------------------------------------- /PSStackedView/PSSVContainerView.m: -------------------------------------------------------------------------------- 1 | // 2 | // PSContainerView.m 3 | // PSStackedView 4 | // 5 | // Created by Peter Steinberger on 7/17/11. 6 | // Copyright 2011 Peter Steinberger. All rights reserved. 7 | // 8 | 9 | #import "PSSVContainerView.h" 10 | #import "PSStackedViewGlobal.h" 11 | #import "UIView+PSSizes.h" 12 | 13 | #define kPSSVCornerRadius 6.f 14 | #define kPSSVShadowWidth 60.f 15 | #define kPSSVShadowAlpha 0.5f 16 | 17 | @interface PSSVContainerView () 18 | @property(nonatomic, assign) CGFloat originalWidth; 19 | @property(nonatomic, strong) CAGradientLayer *leftShadowLayer; 20 | @property(nonatomic, strong) CAGradientLayer *innerShadowLayer; 21 | @property(nonatomic, strong) CAGradientLayer *rightShadowLayer; 22 | @property(nonatomic, strong) UIView *transparentView; 23 | @end 24 | 25 | @implementation PSSVContainerView 26 | 27 | @synthesize shadow = shadow_; 28 | @synthesize originalWidth = originalWidth_; 29 | @synthesize controller = controller_; 30 | @synthesize leftShadowLayer = leftShadowLayer_; 31 | @synthesize innerShadowLayer = innerShadowLayer_; 32 | @synthesize rightShadowLayer = rightShadowLayer_; 33 | @synthesize transparentView = transparentView_; 34 | 35 | /////////////////////////////////////////////////////////////////////////////////////////////////// 36 | #pragma mark - 37 | #pragma mark private 38 | 39 | // creates vertical shadow 40 | - (CAGradientLayer *)shadowAsInverse:(BOOL)inverse { 41 | CAGradientLayer *newShadow = [[CAGradientLayer alloc] init]; 42 | newShadow.startPoint = CGPointMake(0, 0.5); 43 | newShadow.endPoint = CGPointMake(1.0, 0.5); 44 | CGColorRef darkColor = (CGColorRef)CFRetain([UIColor colorWithWhite:0.0f alpha:kPSSVShadowAlpha].CGColor); 45 | CGColorRef lightColor = (CGColorRef)CFRetain([UIColor clearColor].CGColor); 46 | newShadow.colors = [NSArray arrayWithObjects: 47 | (__bridge id)(inverse ? lightColor : darkColor), 48 | (__bridge id)(inverse ? darkColor : lightColor), 49 | nil]; 50 | 51 | CFRelease(darkColor); 52 | CFRelease(lightColor); 53 | return newShadow; 54 | } 55 | 56 | // return available shadows as set, for easy enumeration 57 | - (NSSet *)shadowSet { 58 | NSMutableSet *set = [NSMutableSet set]; 59 | if (self.leftShadowLayer) { 60 | [set addObject:self.leftShadowLayer]; 61 | } 62 | if (self.innerShadowLayer) { 63 | [set addObject:self.innerShadowLayer]; 64 | } 65 | if (self.rightShadowLayer) { 66 | [set addObject:self.rightShadowLayer]; 67 | } 68 | return [set copy]; 69 | } 70 | 71 | /////////////////////////////////////////////////////////////////////////////////////////////////// 72 | #pragma mark - NSObject 73 | 74 | + (PSSVContainerView *)containerViewWithController:(UIViewController *)controller; { 75 | PSSVContainerView *view = [[PSSVContainerView alloc] initWithFrame:controller.view.frame]; 76 | view.controller = controller; 77 | return view; 78 | } 79 | 80 | - (void)dealloc { 81 | [self removeMask]; 82 | self.shadow = PSSVSideNone; // TODO needed? 83 | } 84 | 85 | /////////////////////////////////////////////////////////////////////////////////////////////////// 86 | #pragma mark - UIView 87 | 88 | - (void)setFrame:(CGRect)frame { 89 | [super setFrame:frame]; 90 | 91 | // adapt layer heights 92 | for (CALayer *layer in [self shadowSet]) { 93 | CGRect aFrame = layer.frame; 94 | aFrame.size.height = frame.size.height; 95 | layer.frame = aFrame; 96 | } 97 | } 98 | 99 | /////////////////////////////////////////////////////////////////////////////////////////////////// 100 | #pragma mark - Public 101 | 102 | - (CGFloat)limitToMaxWidth:(CGFloat)maxWidth; { 103 | BOOL widthChanged = NO; 104 | 105 | if (maxWidth && self.width > maxWidth) { 106 | self.width = maxWidth; 107 | widthChanged = YES; 108 | }else if(self.originalWidth && self.width < self.originalWidth) { 109 | self.width = MIN(maxWidth, self.originalWidth); 110 | widthChanged = YES; 111 | } 112 | self.controller.view.width = self.width; 113 | 114 | // update shadow layers for new width 115 | if (widthChanged) { 116 | [self updateContainer]; 117 | } 118 | 119 | return self.width; 120 | } 121 | 122 | - (void)setController:(UIViewController *)aController { 123 | if (controller_ != aController) { 124 | if (controller_) { 125 | [controller_.view removeFromSuperview]; 126 | } 127 | controller_ = aController; 128 | 129 | // properly embed view 130 | self.originalWidth = self.controller.view.width; 131 | controller_.view.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; 132 | controller_.view.frame = CGRectMake(0, 0, controller_.view.width, controller_.view.height); 133 | [self addSubview:controller_.view]; 134 | [self bringSubviewToFront:transparentView_]; 135 | } 136 | } 137 | 138 | - (void)addMaskToCorners:(UIRectCorner)corners; { 139 | // Re-calculate the size of the mask to account for adding/removing rows. 140 | CGRect frame = self.controller.view.bounds; 141 | if([self.controller.view isKindOfClass:[UIScrollView class]] && ((UIScrollView *)self.controller.view).contentSize.height > self.controller.view.frame.size.height) { 142 | frame.size = ((UIScrollView *)self.controller.view).contentSize; 143 | } else { 144 | frame.size = self.controller.view.frame.size; 145 | } 146 | 147 | // Create the path (with only the top-left corner rounded) 148 | UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:frame 149 | byRoundingCorners:corners 150 | cornerRadii:CGSizeMake(kPSSVCornerRadius, kPSSVCornerRadius)]; 151 | 152 | // Create the shape layer and set its path 153 | CAShapeLayer *maskLayer = [CAShapeLayer layer]; 154 | maskLayer.frame = frame; 155 | maskLayer.path = maskPath.CGPath; 156 | 157 | // Set the newly created shape layer as the mask for the image view's layer 158 | self.controller.view.layer.mask = maskLayer; 159 | } 160 | 161 | - (void)removeMask; { 162 | self.controller.view.layer.mask = nil; 163 | } 164 | 165 | - (void)updateContainer { 166 | // re-set shadow property 167 | self.shadow = shadow_; 168 | } 169 | 170 | - (void)setShadow:(PSSVSide)shadow { 171 | shadow_ = shadow; 172 | 173 | if (shadow & PSSVSideLeft) { 174 | if (!self.leftShadowLayer) { 175 | CAGradientLayer *leftShadow = [self shadowAsInverse:YES]; 176 | self.leftShadowLayer = leftShadow; 177 | } 178 | self.leftShadowLayer.frame = CGRectMake(-kPSSVShadowWidth, 0, kPSSVShadowWidth+kPSSVCornerRadius, self.controller.view.height);; 179 | if ([self.layer.sublayers indexOfObjectIdenticalTo:self.leftShadowLayer] != 0) { 180 | [self.layer insertSublayer:self.leftShadowLayer atIndex:0]; 181 | } 182 | }else { 183 | [self.leftShadowLayer removeFromSuperlayer]; 184 | } 185 | 186 | if (shadow & PSSVSideRight) { 187 | if (!self.rightShadowLayer) { 188 | CAGradientLayer *rightShadow = [self shadowAsInverse:NO]; 189 | self.rightShadowLayer = rightShadow; 190 | } 191 | self.rightShadowLayer.frame = CGRectMake(self.width-kPSSVCornerRadius, 0, kPSSVShadowWidth, self.controller.view.height); 192 | if ([self.layer.sublayers indexOfObjectIdenticalTo:self.rightShadowLayer] != 0) { 193 | [self.layer insertSublayer:self.rightShadowLayer atIndex:0]; 194 | } 195 | }else { 196 | [self.rightShadowLayer removeFromSuperlayer]; 197 | } 198 | 199 | if (shadow) { 200 | if (!self.innerShadowLayer) { 201 | CAGradientLayer *innerShadow = [[CAGradientLayer alloc] init]; 202 | innerShadow.colors = [NSArray arrayWithObjects:(id)[UIColor colorWithWhite:0.0f alpha:kPSSVShadowAlpha].CGColor, (id)[UIColor colorWithWhite:0.0f alpha:kPSSVShadowAlpha].CGColor, nil]; 203 | self.innerShadowLayer = innerShadow; 204 | } 205 | self.innerShadowLayer.frame = CGRectMake(kPSSVCornerRadius, 0, self.width-kPSSVCornerRadius*2, self.controller.view.height); 206 | if ([self.layer.sublayers indexOfObjectIdenticalTo:self.innerShadowLayer] != 0) { 207 | [self.layer insertSublayer:self.innerShadowLayer atIndex:0]; 208 | } 209 | }else { 210 | [self.innerShadowLayer removeFromSuperlayer]; 211 | } 212 | } 213 | 214 | - (void)setDarkRatio:(CGFloat)darkRatio { 215 | BOOL isTransparent = darkRatio > 0.01f; 216 | 217 | if (isTransparent && !transparentView_) { 218 | transparentView_ = [[UIView alloc] initWithFrame:CGRectMake(0.f, 0.f, self.width, self.height)]; 219 | transparentView_.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 220 | transparentView_.backgroundColor = [UIColor blackColor]; 221 | transparentView_.alpha = 0.f; 222 | transparentView_.userInteractionEnabled = NO; 223 | [self addSubview:transparentView_]; 224 | } 225 | 226 | transparentView_.alpha = darkRatio; 227 | } 228 | 229 | - (CGFloat)darkRatio { 230 | return transparentView_.alpha; 231 | } 232 | 233 | @end 234 | -------------------------------------------------------------------------------- /Example/StackedViewKitExample/ExampleViewController1.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1280 5 | 11C74 6 | 1938 7 | 1138.23 8 | 567.00 9 | 10 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 11 | 933 12 | 13 | 14 | YES 15 | IBUISegmentedControl 16 | IBUIView 17 | IBUILabel 18 | IBProxyObject 19 | 20 | 21 | YES 22 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 23 | 24 | 25 | PluginDependencyRecalculationVersion 26 | 27 | 28 | 29 | YES 30 | 31 | IBFilesOwner 32 | IBCocoaTouchFramework 33 | 34 | 35 | IBFirstResponder 36 | IBCocoaTouchFramework 37 | 38 | 39 | 40 | 314 41 | 42 | YES 43 | 44 | 45 | 288 46 | {{66, 81}, {188, 21}} 47 | 48 | 49 | 50 | _NS:311 51 | NO 52 | YES 53 | 7 54 | NO 55 | IBCocoaTouchFramework 56 | Example View Controller 57 | 58 | 1 59 | MCAwIDAAA 60 | 61 | 62 | 1 63 | 10 64 | 65 | Helvetica 66 | Helvetica 67 | 0 68 | 17 69 | 70 | 71 | Helvetica 72 | 17 73 | 16 74 | 75 | 76 | 77 | 78 | 288 79 | {{82, 223}, {156, 105}} 80 | 81 | 82 | 83 | _NS:311 84 | NO 85 | YES 86 | 7 87 | NO 88 | IBCocoaTouchFramework 89 | 1 90 | 91 | 92 | 1 93 | 20 94 | 1 95 | 96 | 1 97 | 75 98 | 99 | 100 | Helvetica 101 | 75 102 | 16 103 | 104 | 105 | 106 | 107 | 288 108 | {{57, 122}, {207, 44}} 109 | 110 | 111 | 112 | _NS:262 113 | NO 114 | IBCocoaTouchFramework 115 | 2 116 | 0 117 | 118 | YES 119 | First 120 | Second 121 | 122 | 123 | YES 124 | 125 | 126 | 127 | 128 | YES 129 | 130 | 131 | 132 | 133 | YES 134 | {0, 0} 135 | {0, 0} 136 | 137 | 138 | YES 139 | 140 | 141 | 142 | 143 | 144 | {{0, 20}, {320, 460}} 145 | 146 | 147 | 148 | 149 | 3 150 | MQA 151 | 152 | 2 153 | 154 | 155 | 156 | IBCocoaTouchFramework 157 | 158 | 159 | 160 | 161 | YES 162 | 163 | 164 | view 165 | 166 | 167 | 168 | 3 169 | 170 | 171 | 172 | indexNumberLabel 173 | 174 | 175 | 176 | 7 177 | 178 | 179 | 180 | 181 | YES 182 | 183 | 0 184 | 185 | YES 186 | 187 | 188 | 189 | 190 | 191 | 1 192 | 193 | 194 | YES 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | -1 203 | 204 | 205 | File's Owner 206 | 207 | 208 | -2 209 | 210 | 211 | 212 | 213 | 4 214 | 215 | 216 | 217 | 218 | 5 219 | 220 | 221 | 222 | 223 | 6 224 | 225 | 226 | 227 | 228 | 229 | 230 | YES 231 | 232 | YES 233 | -1.CustomClassName 234 | -1.IBPluginDependency 235 | -2.CustomClassName 236 | -2.IBPluginDependency 237 | 1.IBPluginDependency 238 | 4.IBPluginDependency 239 | 5.IBPluginDependency 240 | 6.IBPluginDependency 241 | 242 | 243 | YES 244 | ExampleViewController1 245 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 246 | UIResponder 247 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 248 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 249 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 250 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 251 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 252 | 253 | 254 | 255 | YES 256 | 257 | 258 | 259 | 260 | 261 | YES 262 | 263 | 264 | 265 | 266 | 7 267 | 268 | 269 | 270 | YES 271 | 272 | ExampleViewController1 273 | UIViewController 274 | 275 | indexNumberLabel 276 | UILabel 277 | 278 | 279 | indexNumberLabel 280 | 281 | indexNumberLabel 282 | UILabel 283 | 284 | 285 | 286 | IBProjectSource 287 | ./Classes/ExampleViewController1.h 288 | 289 | 290 | 291 | 292 | 0 293 | IBCocoaTouchFramework 294 | 295 | com.apple.InterfaceBuilder.CocoaTouchPlugin.InterfaceBuilder3 296 | 297 | 298 | YES 299 | 3 300 | 933 301 | 302 | 303 | -------------------------------------------------------------------------------- /Example/PSStackedViewExample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 3798AD7B13E0B299004C1E33 /* error.png in Resources */ = {isa = PBXBuildFile; fileRef = 3798AD7413E0B299004C1E33 /* error.png */; }; 11 | 3798AD7D13E0B299004C1E33 /* error@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3798AD7613E0B299004C1E33 /* error@2x.png */; }; 12 | 3798AD7E13E0B299004C1E33 /* NewGlow.png in Resources */ = {isa = PBXBuildFile; fileRef = 3798AD7813E0B299004C1E33 /* NewGlow.png */; }; 13 | 3798AD8013E0B299004C1E33 /* NewGlow@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3798AD7A13E0B299004C1E33 /* NewGlow@2x.png */; }; 14 | 3798ADF313E125F1004C1E33 /* UIImage+OverlayColor.m in Sources */ = {isa = PBXBuildFile; fileRef = 3798ADF213E125F1004C1E33 /* UIImage+OverlayColor.m */; }; 15 | 78392A2E13D47F20000CAE6E /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 78392A2D13D47F20000CAE6E /* README.md */; }; 16 | 787C229213CEE5F400105064 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 787C229113CEE5F400105064 /* UIKit.framework */; }; 17 | 787C229413CEE5F400105064 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 787C229313CEE5F400105064 /* Foundation.framework */; }; 18 | 787C229613CEE5F400105064 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 787C229513CEE5F400105064 /* CoreGraphics.framework */; }; 19 | 787C229C13CEE5F400105064 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 787C229A13CEE5F400105064 /* InfoPlist.strings */; }; 20 | 787C229E13CEE5F400105064 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 787C229D13CEE5F400105064 /* main.m */; }; 21 | 787C22A213CEE5F400105064 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 787C22A113CEE5F400105064 /* AppDelegate.m */; }; 22 | 78815F1D13CF3C110011B343 /* ExampleViewController1.xib in Resources */ = {isa = PBXBuildFile; fileRef = 78815F1C13CF3C110011B343 /* ExampleViewController1.xib */; }; 23 | 7884393313CEF4B900D18A56 /* ExampleViewController1.m in Sources */ = {isa = PBXBuildFile; fileRef = 7884393113CEF4B900D18A56 /* ExampleViewController1.m */; }; 24 | 7884393813CEF4C800D18A56 /* ExampleViewController2.m in Sources */ = {isa = PBXBuildFile; fileRef = 7884393613CEF4C800D18A56 /* ExampleViewController2.m */; }; 25 | 7884394013CEF82D00D18A56 /* background.png in Resources */ = {isa = PBXBuildFile; fileRef = 7884393F13CEF82D00D18A56 /* background.png */; }; 26 | 789A1CDA13D44C150005364E /* ExampleMenuRootController.m in Sources */ = {isa = PBXBuildFile; fileRef = 789A1CD913D44C150005364E /* ExampleMenuRootController.m */; }; 27 | 78BA6A0F13CEFECF00DDA16E /* MenuTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 78BA6A0E13CEFECF00DDA16E /* MenuTableViewCell.m */; }; 28 | 78BA6A1213CF032B00DDA16E /* glow.png in Resources */ = {isa = PBXBuildFile; fileRef = 78BA6A1113CF032B00DDA16E /* glow.png */; }; 29 | 78BA6A1613CF05C200DDA16E /* 08-chat.png in Resources */ = {isa = PBXBuildFile; fileRef = 78BA6A1313CF05C200DDA16E /* 08-chat.png */; }; 30 | 78BA6A1713CF05C200DDA16E /* 11-clock.png in Resources */ = {isa = PBXBuildFile; fileRef = 78BA6A1413CF05C200DDA16E /* 11-clock.png */; }; 31 | 78BA6A1813CF05C200DDA16E /* 15-tags.png in Resources */ = {isa = PBXBuildFile; fileRef = 78BA6A1513CF05C200DDA16E /* 15-tags.png */; }; 32 | 78D4BE2A142F422C0036BD4B /* PSStackedViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 78D4BE20142F422C0036BD4B /* PSStackedViewController.m */; }; 33 | 78D4BE2B142F422C0036BD4B /* PSStackedViewGlobal.m in Sources */ = {isa = PBXBuildFile; fileRef = 78D4BE23142F422C0036BD4B /* PSStackedViewGlobal.m */; }; 34 | 78D4BE2C142F422C0036BD4B /* PSSVContainerView.m in Sources */ = {isa = PBXBuildFile; fileRef = 78D4BE25142F422C0036BD4B /* PSSVContainerView.m */; }; 35 | 78D4BE2D142F422C0036BD4B /* UIView+PSSizes.m in Sources */ = {isa = PBXBuildFile; fileRef = 78D4BE27142F422C0036BD4B /* UIView+PSSizes.m */; }; 36 | 78D4BE2E142F422C0036BD4B /* UIViewController+PSStackedView.m in Sources */ = {isa = PBXBuildFile; fileRef = 78D4BE29142F422C0036BD4B /* UIViewController+PSStackedView.m */; }; 37 | 78DE83D713D3271700D2BADD /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 78DE83D613D3271700D2BADD /* QuartzCore.framework */; }; 38 | /* End PBXBuildFile section */ 39 | 40 | /* Begin PBXFileReference section */ 41 | 3798AD7413E0B299004C1E33 /* error.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = error.png; sourceTree = ""; }; 42 | 3798AD7613E0B299004C1E33 /* error@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "error@2x.png"; sourceTree = ""; }; 43 | 3798AD7813E0B299004C1E33 /* NewGlow.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = NewGlow.png; sourceTree = ""; }; 44 | 3798AD7A13E0B299004C1E33 /* NewGlow@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "NewGlow@2x.png"; sourceTree = ""; }; 45 | 3798ADF113E125F1004C1E33 /* UIImage+OverlayColor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIImage+OverlayColor.h"; sourceTree = ""; }; 46 | 3798ADF213E125F1004C1E33 /* UIImage+OverlayColor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIImage+OverlayColor.m"; sourceTree = ""; }; 47 | 78392A2D13D47F20000CAE6E /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = README.md; path = ../README.md; sourceTree = ""; }; 48 | 787C228D13CEE5F400105064 /* StackedViewKit.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = StackedViewKit.app; sourceTree = BUILT_PRODUCTS_DIR; }; 49 | 787C229113CEE5F400105064 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; 50 | 787C229313CEE5F400105064 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 51 | 787C229513CEE5F400105064 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; 52 | 787C229913CEE5F400105064 /* PSStackedViewExample-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "PSStackedViewExample-Info.plist"; sourceTree = ""; }; 53 | 787C229B13CEE5F400105064 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 54 | 787C229D13CEE5F400105064 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 55 | 787C229F13CEE5F400105064 /* PSStackedViewExample-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "PSStackedViewExample-Prefix.pch"; sourceTree = ""; }; 56 | 787C22A013CEE5F400105064 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 57 | 787C22A113CEE5F400105064 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 58 | 78815F1C13CF3C110011B343 /* ExampleViewController1.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ExampleViewController1.xib; sourceTree = ""; }; 59 | 7884393013CEF4B900D18A56 /* ExampleViewController1.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ExampleViewController1.h; sourceTree = ""; }; 60 | 7884393113CEF4B900D18A56 /* ExampleViewController1.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ExampleViewController1.m; sourceTree = ""; }; 61 | 7884393513CEF4C800D18A56 /* ExampleViewController2.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ExampleViewController2.h; sourceTree = ""; }; 62 | 7884393613CEF4C800D18A56 /* ExampleViewController2.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ExampleViewController2.m; sourceTree = ""; }; 63 | 7884393F13CEF82D00D18A56 /* background.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = background.png; sourceTree = ""; }; 64 | 789A1CD813D44C150005364E /* ExampleMenuRootController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ExampleMenuRootController.h; sourceTree = ""; }; 65 | 789A1CD913D44C150005364E /* ExampleMenuRootController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ExampleMenuRootController.m; sourceTree = ""; }; 66 | 78BA6A0D13CEFECF00DDA16E /* MenuTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MenuTableViewCell.h; sourceTree = ""; }; 67 | 78BA6A0E13CEFECF00DDA16E /* MenuTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MenuTableViewCell.m; sourceTree = ""; }; 68 | 78BA6A1113CF032B00DDA16E /* glow.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = glow.png; sourceTree = ""; }; 69 | 78BA6A1313CF05C200DDA16E /* 08-chat.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "08-chat.png"; sourceTree = ""; }; 70 | 78BA6A1413CF05C200DDA16E /* 11-clock.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "11-clock.png"; sourceTree = ""; }; 71 | 78BA6A1513CF05C200DDA16E /* 15-tags.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "15-tags.png"; sourceTree = ""; }; 72 | 78D4BE1E142F422C0036BD4B /* PSStackedView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PSStackedView.h; path = ../../PSStackedView/PSStackedView.h; sourceTree = ""; }; 73 | 78D4BE1F142F422C0036BD4B /* PSStackedViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PSStackedViewController.h; path = ../../PSStackedView/PSStackedViewController.h; sourceTree = ""; }; 74 | 78D4BE20142F422C0036BD4B /* PSStackedViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = PSStackedViewController.m; path = ../../PSStackedView/PSStackedViewController.m; sourceTree = ""; }; 75 | 78D4BE21142F422C0036BD4B /* PSStackedViewDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PSStackedViewDelegate.h; path = ../../PSStackedView/PSStackedViewDelegate.h; sourceTree = ""; }; 76 | 78D4BE22142F422C0036BD4B /* PSStackedViewGlobal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PSStackedViewGlobal.h; path = ../../PSStackedView/PSStackedViewGlobal.h; sourceTree = ""; }; 77 | 78D4BE23142F422C0036BD4B /* PSStackedViewGlobal.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = PSStackedViewGlobal.m; path = ../../PSStackedView/PSStackedViewGlobal.m; sourceTree = ""; }; 78 | 78D4BE24142F422C0036BD4B /* PSSVContainerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PSSVContainerView.h; path = ../../PSStackedView/PSSVContainerView.h; sourceTree = ""; }; 79 | 78D4BE25142F422C0036BD4B /* PSSVContainerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = PSSVContainerView.m; path = ../../PSStackedView/PSSVContainerView.m; sourceTree = ""; }; 80 | 78D4BE26142F422C0036BD4B /* UIView+PSSizes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "UIView+PSSizes.h"; path = "../../PSStackedView/UIView+PSSizes.h"; sourceTree = ""; }; 81 | 78D4BE27142F422C0036BD4B /* UIView+PSSizes.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "UIView+PSSizes.m"; path = "../../PSStackedView/UIView+PSSizes.m"; sourceTree = ""; }; 82 | 78D4BE28142F422C0036BD4B /* UIViewController+PSStackedView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "UIViewController+PSStackedView.h"; path = "../../PSStackedView/UIViewController+PSStackedView.h"; sourceTree = ""; }; 83 | 78D4BE29142F422C0036BD4B /* UIViewController+PSStackedView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "UIViewController+PSStackedView.m"; path = "../../PSStackedView/UIViewController+PSStackedView.m"; sourceTree = ""; }; 84 | 78DE83D613D3271700D2BADD /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; 85 | /* End PBXFileReference section */ 86 | 87 | /* Begin PBXFrameworksBuildPhase section */ 88 | 787C228A13CEE5F400105064 /* Frameworks */ = { 89 | isa = PBXFrameworksBuildPhase; 90 | buildActionMask = 2147483647; 91 | files = ( 92 | 78DE83D713D3271700D2BADD /* QuartzCore.framework in Frameworks */, 93 | 787C229213CEE5F400105064 /* UIKit.framework in Frameworks */, 94 | 787C229413CEE5F400105064 /* Foundation.framework in Frameworks */, 95 | 787C229613CEE5F400105064 /* CoreGraphics.framework in Frameworks */, 96 | ); 97 | runOnlyForDeploymentPostprocessing = 0; 98 | }; 99 | /* End PBXFrameworksBuildPhase section */ 100 | 101 | /* Begin PBXGroup section */ 102 | 3798AD7313E0B299004C1E33 /* error */ = { 103 | isa = PBXGroup; 104 | children = ( 105 | 3798AD7413E0B299004C1E33 /* error.png */, 106 | 3798AD7613E0B299004C1E33 /* error@2x.png */, 107 | ); 108 | path = error; 109 | sourceTree = ""; 110 | }; 111 | 3798AD7713E0B299004C1E33 /* NewGlow */ = { 112 | isa = PBXGroup; 113 | children = ( 114 | 3798AD7813E0B299004C1E33 /* NewGlow.png */, 115 | 3798AD7A13E0B299004C1E33 /* NewGlow@2x.png */, 116 | ); 117 | path = NewGlow; 118 | sourceTree = ""; 119 | }; 120 | 787C228213CEE5F400105064 = { 121 | isa = PBXGroup; 122 | children = ( 123 | 78392A2D13D47F20000CAE6E /* README.md */, 124 | 787C22DB13CEF1B200105064 /* PSStackedView */, 125 | 787C229713CEE5F400105064 /* PSStackedViewExample */, 126 | 787C229013CEE5F400105064 /* Frameworks */, 127 | 787C228E13CEE5F400105064 /* Products */, 128 | ); 129 | sourceTree = ""; 130 | }; 131 | 787C228E13CEE5F400105064 /* Products */ = { 132 | isa = PBXGroup; 133 | children = ( 134 | 787C228D13CEE5F400105064 /* StackedViewKit.app */, 135 | ); 136 | name = Products; 137 | sourceTree = ""; 138 | }; 139 | 787C229013CEE5F400105064 /* Frameworks */ = { 140 | isa = PBXGroup; 141 | children = ( 142 | 78DE83D613D3271700D2BADD /* QuartzCore.framework */, 143 | 787C229113CEE5F400105064 /* UIKit.framework */, 144 | 787C229313CEE5F400105064 /* Foundation.framework */, 145 | 787C229513CEE5F400105064 /* CoreGraphics.framework */, 146 | ); 147 | name = Frameworks; 148 | sourceTree = ""; 149 | }; 150 | 787C229713CEE5F400105064 /* PSStackedViewExample */ = { 151 | isa = PBXGroup; 152 | children = ( 153 | 787C229813CEE5F400105064 /* Supporting Files */, 154 | 7884393E13CEF82D00D18A56 /* Images */, 155 | 787C22A013CEE5F400105064 /* AppDelegate.h */, 156 | 787C22A113CEE5F400105064 /* AppDelegate.m */, 157 | 7884393013CEF4B900D18A56 /* ExampleViewController1.h */, 158 | 7884393113CEF4B900D18A56 /* ExampleViewController1.m */, 159 | 78815F1C13CF3C110011B343 /* ExampleViewController1.xib */, 160 | 7884393513CEF4C800D18A56 /* ExampleViewController2.h */, 161 | 7884393613CEF4C800D18A56 /* ExampleViewController2.m */, 162 | 789A1CD813D44C150005364E /* ExampleMenuRootController.h */, 163 | 789A1CD913D44C150005364E /* ExampleMenuRootController.m */, 164 | 78BA6A0D13CEFECF00DDA16E /* MenuTableViewCell.h */, 165 | 78BA6A0E13CEFECF00DDA16E /* MenuTableViewCell.m */, 166 | 3798ADF113E125F1004C1E33 /* UIImage+OverlayColor.h */, 167 | 3798ADF213E125F1004C1E33 /* UIImage+OverlayColor.m */, 168 | ); 169 | name = PSStackedViewExample; 170 | path = StackedViewKitExample; 171 | sourceTree = ""; 172 | }; 173 | 787C229813CEE5F400105064 /* Supporting Files */ = { 174 | isa = PBXGroup; 175 | children = ( 176 | 787C229913CEE5F400105064 /* PSStackedViewExample-Info.plist */, 177 | 787C229A13CEE5F400105064 /* InfoPlist.strings */, 178 | 787C229D13CEE5F400105064 /* main.m */, 179 | 787C229F13CEE5F400105064 /* PSStackedViewExample-Prefix.pch */, 180 | ); 181 | name = "Supporting Files"; 182 | sourceTree = ""; 183 | }; 184 | 787C22DB13CEF1B200105064 /* PSStackedView */ = { 185 | isa = PBXGroup; 186 | children = ( 187 | 78D4BE1E142F422C0036BD4B /* PSStackedView.h */, 188 | 78D4BE1F142F422C0036BD4B /* PSStackedViewController.h */, 189 | 78D4BE20142F422C0036BD4B /* PSStackedViewController.m */, 190 | 78D4BE21142F422C0036BD4B /* PSStackedViewDelegate.h */, 191 | 78D4BE22142F422C0036BD4B /* PSStackedViewGlobal.h */, 192 | 78D4BE23142F422C0036BD4B /* PSStackedViewGlobal.m */, 193 | 78D4BE24142F422C0036BD4B /* PSSVContainerView.h */, 194 | 78D4BE25142F422C0036BD4B /* PSSVContainerView.m */, 195 | 78D4BE26142F422C0036BD4B /* UIView+PSSizes.h */, 196 | 78D4BE27142F422C0036BD4B /* UIView+PSSizes.m */, 197 | 78D4BE28142F422C0036BD4B /* UIViewController+PSStackedView.h */, 198 | 78D4BE29142F422C0036BD4B /* UIViewController+PSStackedView.m */, 199 | ); 200 | name = PSStackedView; 201 | path = StackedViewKitExample; 202 | sourceTree = ""; 203 | }; 204 | 7884393E13CEF82D00D18A56 /* Images */ = { 205 | isa = PBXGroup; 206 | children = ( 207 | 3798AD7313E0B299004C1E33 /* error */, 208 | 3798AD7713E0B299004C1E33 /* NewGlow */, 209 | 78BA6A1313CF05C200DDA16E /* 08-chat.png */, 210 | 78BA6A1413CF05C200DDA16E /* 11-clock.png */, 211 | 78BA6A1513CF05C200DDA16E /* 15-tags.png */, 212 | 78BA6A1113CF032B00DDA16E /* glow.png */, 213 | 7884393F13CEF82D00D18A56 /* background.png */, 214 | ); 215 | path = Images; 216 | sourceTree = ""; 217 | }; 218 | /* End PBXGroup section */ 219 | 220 | /* Begin PBXNativeTarget section */ 221 | 787C228C13CEE5F400105064 /* PSStackedViewExample */ = { 222 | isa = PBXNativeTarget; 223 | buildConfigurationList = 787C22A813CEE5F400105064 /* Build configuration list for PBXNativeTarget "PSStackedViewExample" */; 224 | buildPhases = ( 225 | 787C228913CEE5F400105064 /* Sources */, 226 | 787C228A13CEE5F400105064 /* Frameworks */, 227 | 787C228B13CEE5F400105064 /* Resources */, 228 | ); 229 | buildRules = ( 230 | ); 231 | dependencies = ( 232 | ); 233 | name = PSStackedViewExample; 234 | productName = StackedViewKitExample; 235 | productReference = 787C228D13CEE5F400105064 /* StackedViewKit.app */; 236 | productType = "com.apple.product-type.application"; 237 | }; 238 | /* End PBXNativeTarget section */ 239 | 240 | /* Begin PBXProject section */ 241 | 787C228413CEE5F400105064 /* Project object */ = { 242 | isa = PBXProject; 243 | attributes = { 244 | LastUpgradeCheck = 0420; 245 | ORGANIZATIONNAME = "Peter Steinberger"; 246 | }; 247 | buildConfigurationList = 787C228713CEE5F400105064 /* Build configuration list for PBXProject "PSStackedViewExample" */; 248 | compatibilityVersion = "Xcode 3.2"; 249 | developmentRegion = English; 250 | hasScannedForEncodings = 0; 251 | knownRegions = ( 252 | en, 253 | ); 254 | mainGroup = 787C228213CEE5F400105064; 255 | productRefGroup = 787C228E13CEE5F400105064 /* Products */; 256 | projectDirPath = ""; 257 | projectRoot = ""; 258 | targets = ( 259 | 787C228C13CEE5F400105064 /* PSStackedViewExample */, 260 | ); 261 | }; 262 | /* End PBXProject section */ 263 | 264 | /* Begin PBXResourcesBuildPhase section */ 265 | 787C228B13CEE5F400105064 /* Resources */ = { 266 | isa = PBXResourcesBuildPhase; 267 | buildActionMask = 2147483647; 268 | files = ( 269 | 787C229C13CEE5F400105064 /* InfoPlist.strings in Resources */, 270 | 7884394013CEF82D00D18A56 /* background.png in Resources */, 271 | 78BA6A1213CF032B00DDA16E /* glow.png in Resources */, 272 | 78BA6A1613CF05C200DDA16E /* 08-chat.png in Resources */, 273 | 78BA6A1713CF05C200DDA16E /* 11-clock.png in Resources */, 274 | 78BA6A1813CF05C200DDA16E /* 15-tags.png in Resources */, 275 | 78815F1D13CF3C110011B343 /* ExampleViewController1.xib in Resources */, 276 | 78392A2E13D47F20000CAE6E /* README.md in Resources */, 277 | 3798AD7B13E0B299004C1E33 /* error.png in Resources */, 278 | 3798AD7D13E0B299004C1E33 /* error@2x.png in Resources */, 279 | 3798AD7E13E0B299004C1E33 /* NewGlow.png in Resources */, 280 | 3798AD8013E0B299004C1E33 /* NewGlow@2x.png in Resources */, 281 | ); 282 | runOnlyForDeploymentPostprocessing = 0; 283 | }; 284 | /* End PBXResourcesBuildPhase section */ 285 | 286 | /* Begin PBXSourcesBuildPhase section */ 287 | 787C228913CEE5F400105064 /* Sources */ = { 288 | isa = PBXSourcesBuildPhase; 289 | buildActionMask = 2147483647; 290 | files = ( 291 | 787C229E13CEE5F400105064 /* main.m in Sources */, 292 | 787C22A213CEE5F400105064 /* AppDelegate.m in Sources */, 293 | 7884393313CEF4B900D18A56 /* ExampleViewController1.m in Sources */, 294 | 7884393813CEF4C800D18A56 /* ExampleViewController2.m in Sources */, 295 | 78BA6A0F13CEFECF00DDA16E /* MenuTableViewCell.m in Sources */, 296 | 789A1CDA13D44C150005364E /* ExampleMenuRootController.m in Sources */, 297 | 3798ADF313E125F1004C1E33 /* UIImage+OverlayColor.m in Sources */, 298 | 78D4BE2A142F422C0036BD4B /* PSStackedViewController.m in Sources */, 299 | 78D4BE2B142F422C0036BD4B /* PSStackedViewGlobal.m in Sources */, 300 | 78D4BE2C142F422C0036BD4B /* PSSVContainerView.m in Sources */, 301 | 78D4BE2D142F422C0036BD4B /* UIView+PSSizes.m in Sources */, 302 | 78D4BE2E142F422C0036BD4B /* UIViewController+PSStackedView.m in Sources */, 303 | ); 304 | runOnlyForDeploymentPostprocessing = 0; 305 | }; 306 | /* End PBXSourcesBuildPhase section */ 307 | 308 | /* Begin PBXVariantGroup section */ 309 | 787C229A13CEE5F400105064 /* InfoPlist.strings */ = { 310 | isa = PBXVariantGroup; 311 | children = ( 312 | 787C229B13CEE5F400105064 /* en */, 313 | ); 314 | name = InfoPlist.strings; 315 | sourceTree = ""; 316 | }; 317 | /* End PBXVariantGroup section */ 318 | 319 | /* Begin XCBuildConfiguration section */ 320 | 787C22A613CEE5F400105064 /* Debug */ = { 321 | isa = XCBuildConfiguration; 322 | buildSettings = { 323 | ALWAYS_SEARCH_USER_PATHS = NO; 324 | ARCHS = "$(ARCHS_UNIVERSAL_IPHONE_OS)"; 325 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 326 | COPY_PHASE_STRIP = NO; 327 | GCC_C_LANGUAGE_STANDARD = gnu99; 328 | GCC_DYNAMIC_NO_PIC = NO; 329 | GCC_OPTIMIZATION_LEVEL = 0; 330 | GCC_PREPROCESSOR_DEFINITIONS = ( 331 | "DEBUG=1", 332 | "$(inherited)", 333 | ); 334 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 335 | GCC_VERSION = com.apple.compilers.llvm.clang.1_0; 336 | GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; 337 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 338 | GCC_WARN_UNUSED_VARIABLE = YES; 339 | IPHONEOS_DEPLOYMENT_TARGET = 4.3; 340 | SDKROOT = iphoneos; 341 | TARGETED_DEVICE_FAMILY = "1,2"; 342 | }; 343 | name = Debug; 344 | }; 345 | 787C22A713CEE5F400105064 /* Release */ = { 346 | isa = XCBuildConfiguration; 347 | buildSettings = { 348 | ALWAYS_SEARCH_USER_PATHS = NO; 349 | ARCHS = "$(ARCHS_UNIVERSAL_IPHONE_OS)"; 350 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 351 | COPY_PHASE_STRIP = YES; 352 | GCC_C_LANGUAGE_STANDARD = gnu99; 353 | GCC_VERSION = com.apple.compilers.llvm.clang.1_0; 354 | GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; 355 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 356 | GCC_WARN_UNUSED_VARIABLE = YES; 357 | IPHONEOS_DEPLOYMENT_TARGET = 4.3; 358 | OTHER_CFLAGS = "-DNS_BLOCK_ASSERTIONS=1"; 359 | SDKROOT = iphoneos; 360 | TARGETED_DEVICE_FAMILY = "1,2"; 361 | VALIDATE_PRODUCT = YES; 362 | }; 363 | name = Release; 364 | }; 365 | 787C22A913CEE5F400105064 /* Debug */ = { 366 | isa = XCBuildConfiguration; 367 | buildSettings = { 368 | CLANG_ENABLE_OBJC_ARC = YES; 369 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 370 | GCC_PREFIX_HEADER = "StackedViewKitExample/PSStackedViewExample-Prefix.pch"; 371 | INFOPLIST_FILE = "StackedViewKitExample/PSStackedViewExample-Info.plist"; 372 | PRODUCT_NAME = StackedViewKit; 373 | WRAPPER_EXTENSION = app; 374 | }; 375 | name = Debug; 376 | }; 377 | 787C22AA13CEE5F400105064 /* Release */ = { 378 | isa = XCBuildConfiguration; 379 | buildSettings = { 380 | CLANG_ENABLE_OBJC_ARC = YES; 381 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 382 | GCC_PREFIX_HEADER = "StackedViewKitExample/PSStackedViewExample-Prefix.pch"; 383 | INFOPLIST_FILE = "StackedViewKitExample/PSStackedViewExample-Info.plist"; 384 | PRODUCT_NAME = StackedViewKit; 385 | WRAPPER_EXTENSION = app; 386 | }; 387 | name = Release; 388 | }; 389 | /* End XCBuildConfiguration section */ 390 | 391 | /* Begin XCConfigurationList section */ 392 | 787C228713CEE5F400105064 /* Build configuration list for PBXProject "PSStackedViewExample" */ = { 393 | isa = XCConfigurationList; 394 | buildConfigurations = ( 395 | 787C22A613CEE5F400105064 /* Debug */, 396 | 787C22A713CEE5F400105064 /* Release */, 397 | ); 398 | defaultConfigurationIsVisible = 0; 399 | defaultConfigurationName = Release; 400 | }; 401 | 787C22A813CEE5F400105064 /* Build configuration list for PBXNativeTarget "PSStackedViewExample" */ = { 402 | isa = XCConfigurationList; 403 | buildConfigurations = ( 404 | 787C22A913CEE5F400105064 /* Debug */, 405 | 787C22AA13CEE5F400105064 /* Release */, 406 | ); 407 | defaultConfigurationIsVisible = 0; 408 | defaultConfigurationName = Release; 409 | }; 410 | /* End XCConfigurationList section */ 411 | }; 412 | rootObject = 787C228413CEE5F400105064 /* Project object */; 413 | } 414 | -------------------------------------------------------------------------------- /PSStackedView/PSStackedViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // SVStackRootController.m 3 | // PSStackedView 4 | // 5 | // Created by Peter Steinberger on 7/14/11. 6 | // Copyright 2011 Peter Steinberger. All rights reserved. 7 | // 8 | 9 | #import "PSStackedView.h" 10 | #import "UIViewController+PSStackedView.h" 11 | #import 12 | #import 13 | 14 | #define kPSSVStackAnimationSpeedModifier 1.f // DEBUG! 15 | #define kPSSVStackAnimationDuration kPSSVStackAnimationSpeedModifier * 0.25f 16 | #define kPSSVStackAnimationBounceDuration kPSSVStackAnimationSpeedModifier * 0.20f 17 | #define kPSSVStackAnimationPushDuration kPSSVStackAnimationSpeedModifier * 0.25f 18 | #define kPSSVStackAnimationPopDuration kPSSVStackAnimationSpeedModifier * 0.25f 19 | #define kPSSVMaxSnapOverOffset 20 20 | #define kPSSVAssociatedBaseViewControllerKey @"kPSSVAssociatedBaseViewController" 21 | 22 | // reduces alpha over overlapped view controllers. 1.f would totally black-out on complete overlay 23 | #define kAlphaReductRatio 2.f 24 | #define EPSILON .001f // float calculations 25 | 26 | // prevents me getting crazy 27 | typedef void(^PSSVSimpleBlock)(void); 28 | 29 | @interface PSStackedViewController() { 30 | NSMutableArray *viewControllers_; 31 | // internal drag state handling and other messy details 32 | PSSVSnapOption lastDragOption_; 33 | BOOL snapBackFromLeft_; 34 | NSInteger lastDragOffset_; 35 | BOOL lastDragDividedOne_; 36 | NSInteger lastVisibleIndexBeforeRotation_; 37 | struct { 38 | unsigned int delegateWillInsertViewController:1; 39 | unsigned int delegateDidInsertViewController:1; 40 | unsigned int delegateWillRemoveViewController:1; 41 | unsigned int delegateDidRemoveViewController:1; 42 | }delegateFlags_; 43 | } 44 | @property(nonatomic, strong) UIViewController *rootViewController; 45 | @property(nonatomic, strong) NSArray *viewControllers; 46 | @property(nonatomic, assign) NSInteger firstVisibleIndex; 47 | @property(nonatomic, assign) CGFloat floatIndex; 48 | - (UIViewController *)overlappedViewController; 49 | @end 50 | 51 | @implementation PSStackedViewController 52 | 53 | @synthesize leftInset = leftInset_; 54 | @synthesize largeLeftInset = largeLeftInset_; 55 | @synthesize viewControllers = viewControllers_; 56 | @synthesize floatIndex = floatIndex_; 57 | @synthesize rootViewController = rootViewController_; 58 | @synthesize panRecognizer = panRecognizer_; 59 | @synthesize delegate = delegate_; 60 | @synthesize reduceAnimations = reduceAnimations_; 61 | @dynamic firstVisibleIndex; 62 | 63 | #ifdef ALLOW_SWIZZLING_NAVIGATIONCONTROLLER 64 | @synthesize navigationBar; 65 | #endif 66 | 67 | /////////////////////////////////////////////////////////////////////////////////////////////////// 68 | #pragma mark - NSObject 69 | 70 | - (id)initWithRootViewController:(UIViewController *)rootViewController; { 71 | if ((self = [super init])) { 72 | rootViewController_ = rootViewController; 73 | viewControllers_ = [[NSMutableArray alloc] init]; 74 | 75 | // set some reasonble defaults 76 | leftInset_ = 60; 77 | largeLeftInset_ = 200; 78 | 79 | // add a gesture recognizer to detect dragging to the guest controllers 80 | UIPanGestureRecognizer *panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanFrom:)]; 81 | [panRecognizer setMaximumNumberOfTouches:1]; 82 | [panRecognizer setDelaysTouchesBegan:NO]; 83 | [panRecognizer setDelaysTouchesEnded:YES]; 84 | [panRecognizer setCancelsTouchesInView:YES]; 85 | panRecognizer.delegate = self; 86 | [self.view addGestureRecognizer:panRecognizer]; 87 | self.panRecognizer = panRecognizer; 88 | 89 | 90 | 91 | #ifdef ALLOW_SWIZZLING_NAVIGATIONCONTROLLER 92 | PSSVLog("Swizzling UIViewController.navigationController"); 93 | Method origMethod = class_getInstanceMethod([UIViewController class], @selector(navigationController)); 94 | Method overrideMethod = class_getInstanceMethod([UIViewController class], @selector(navigationControllerSwizzled)); 95 | method_exchangeImplementations(origMethod, overrideMethod); 96 | #endif 97 | } 98 | return self; 99 | } 100 | 101 | - (void)dealloc { 102 | delegate_ = nil; 103 | panRecognizer_.delegate = nil; 104 | // remove all view controllers the hard way (w/o calling delegate) 105 | while ([self.viewControllers count]) { 106 | [self popViewControllerAnimated:NO]; 107 | } 108 | 109 | } 110 | 111 | /////////////////////////////////////////////////////////////////////////////////////////////////// 112 | #pragma mark - Delegate 113 | 114 | - (void)setDelegate:(id)delegate { 115 | if (delegate != delegate_) { 116 | delegate_ = delegate; 117 | 118 | delegateFlags_.delegateWillInsertViewController = [delegate respondsToSelector:@selector(stackedView:willInsertViewController:)]; 119 | delegateFlags_.delegateDidInsertViewController = [delegate respondsToSelector:@selector(stackedView:didInsertViewController:)]; 120 | delegateFlags_.delegateWillRemoveViewController = [delegate respondsToSelector:@selector(stackedView:willRemoveViewController:)]; 121 | delegateFlags_.delegateDidRemoveViewController = [delegate respondsToSelector:@selector(stackedView:didRemoveViewController:)]; 122 | } 123 | } 124 | 125 | - (void)delegateWillInsertViewController:(UIViewController *)viewController { 126 | if (delegateFlags_.delegateWillInsertViewController) { 127 | [self.delegate stackedView:self willInsertViewController:viewController]; 128 | } 129 | } 130 | 131 | - (void)delegateDidInsertViewController:(UIViewController *)viewController { 132 | if (delegateFlags_.delegateDidInsertViewController) { 133 | [self.delegate stackedView:self didInsertViewController:viewController]; 134 | } 135 | } 136 | 137 | - (void)delegateWillRemoveViewController:(UIViewController *)viewController { 138 | if (delegateFlags_.delegateWillRemoveViewController) { 139 | [self.delegate stackedView:self willRemoveViewController:viewController]; 140 | } 141 | } 142 | 143 | - (void)delegateDidRemoveViewController:(UIViewController *)viewController { 144 | if (delegateFlags_.delegateDidRemoveViewController) { 145 | [self.delegate stackedView:self didRemoveViewController:viewController]; 146 | } 147 | } 148 | 149 | /////////////////////////////////////////////////////////////////////////////////////////////////// 150 | #pragma mark - Private Helpers 151 | 152 | - (NSInteger)firstVisibleIndex { 153 | NSInteger firstVisibleIndex = floorf(self.floatIndex); 154 | return firstVisibleIndex; 155 | } 156 | 157 | - (CGRect)viewRect { 158 | // self.view.frame not used, it's wrong in viewWillAppear 159 | CGRect viewRect = [[UIScreen mainScreen] applicationFrame]; 160 | return viewRect; 161 | } 162 | 163 | // return screen width 164 | - (CGFloat)screenWidth { 165 | CGRect viewRect = [self viewRect]; 166 | CGFloat screenWidth = PSIsLandscape() ? viewRect.size.height : viewRect.size.width; 167 | return screenWidth; 168 | } 169 | 170 | - (CGFloat)screenHeight { 171 | CGRect viewRect = [self viewRect]; 172 | NSUInteger screenHeight = PSIsLandscape() ? viewRect.size.width : viewRect.size.height; 173 | return screenHeight; 174 | } 175 | 176 | - (CGFloat)maxControllerWidth { 177 | CGFloat maxWidth = [self screenWidth] - self.leftInset; 178 | return maxWidth; 179 | } 180 | 181 | // total stack width if completely expanded 182 | - (NSUInteger)totalStackWidth { 183 | NSUInteger totalStackWidth = 0; 184 | for (UIViewController *controller in self.viewControllers) { 185 | totalStackWidth += controller.containerView.width; 186 | } 187 | return totalStackWidth; 188 | } 189 | 190 | // menu is only collapsable if stack is large enough 191 | - (BOOL)isMenuCollapsable { 192 | BOOL isMenuCollapsable = [self totalStackWidth] + self.largeLeftInset > [self screenWidth]; 193 | return isMenuCollapsable; 194 | } 195 | 196 | // return current left border (how it *should* be) 197 | - (NSUInteger)currentLeftInset { 198 | return self.floatIndex >= 0.5 ? self.leftInset : self.largeLeftInset; 199 | } 200 | 201 | // minimal left border is depending on amount of VCs 202 | - (NSUInteger)minimalLeftInset { 203 | return [self isMenuCollapsable] ? self.leftInset : self.largeLeftInset; 204 | } 205 | 206 | // check if a view controller is visible or not 207 | - (BOOL)isViewControllerVisible:(UIViewController *)viewController completely:(BOOL)completely { 208 | NSParameterAssert(viewController); 209 | NSUInteger screenWidth = [self screenWidth]; 210 | 211 | BOOL isVCVisible = ((viewController.containerView.left < screenWidth && !completely) || 212 | (completely && viewController.containerView.right <= screenWidth)); 213 | return isVCVisible; 214 | } 215 | 216 | // returns view controller that is displayed before viewController 217 | - (UIViewController *)previousViewController:(UIViewController *)viewController { 218 | if(!viewController) // don't assert on mere menu events 219 | return nil; 220 | 221 | NSUInteger vcIndex = [self indexOfViewController:viewController]; 222 | UIViewController *prevVC = nil; 223 | if (vcIndex > 0) { 224 | prevVC = [self.viewControllers objectAtIndex:vcIndex-1]; 225 | } 226 | 227 | return prevVC; 228 | } 229 | 230 | // returns view controller that is displayed after viewController 231 | - (UIViewController *)nextViewController:(UIViewController *)viewController { 232 | NSParameterAssert(viewController); 233 | 234 | NSUInteger vcIndex = [self indexOfViewController:viewController]; 235 | UIViewController *nextVC = nil; 236 | if (vcIndex + 1 < [self.viewControllers count]) { 237 | nextVC = [self.viewControllers objectAtIndex:vcIndex+1]; 238 | } 239 | 240 | return nextVC; 241 | } 242 | 243 | // returns last visible view controller. this *can* be the last view controller in the stack, 244 | // but also one of the previous ones if the user navigates back in the stack 245 | - (UIViewController *)lastVisibleViewControllerCompletelyVisible:(BOOL)completely { 246 | __block UIViewController *lastVisibleViewController = nil; 247 | 248 | [self.viewControllers enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 249 | UIViewController *currentViewController = (UIViewController *)obj; 250 | if ([self isViewControllerVisible:currentViewController completely:completely]) { 251 | lastVisibleViewController = currentViewController; 252 | *stop = YES; 253 | } 254 | }]; 255 | 256 | return lastVisibleViewController; 257 | } 258 | 259 | // returns true if firstVisibleIndex is the last available index. 260 | - (BOOL)isLastIndex { 261 | BOOL isLastIndex = self.firstVisibleIndex == [self.viewControllers count] - 1; 262 | return isLastIndex; 263 | } 264 | 265 | enum { 266 | PSSVRoundNearest, 267 | PSSVRoundUp, 268 | PSSVRoundDown 269 | }typedef PSSVRoundOption; 270 | 271 | - (BOOL)isFloatIndexBetween:(CGFloat)floatIndex { 272 | CGFloat intIndex, restIndex; 273 | restIndex = modff(floatIndex, &intIndex); 274 | BOOL isBetween = fabsf(restIndex - 0.5f) < EPSILON; 275 | return isBetween; 276 | } 277 | 278 | // check if index is valid. Valid indexes are >= 0.0 and only full or .5 parts are allowed. 279 | // there are lots of other, more complex rules, so calculate! 280 | - (BOOL)isValidFloatIndex:(CGFloat)floatIndex { 281 | BOOL isValid = floatIndex == 0.f; // 0.f is always allowed 282 | if (!isValid) { 283 | CGFloat contentWidth = [self totalStackWidth]; 284 | if (floatIndex == 0.5f) { 285 | // docking to menu is only allowed if content > available size. 286 | isValid = contentWidth > [self screenWidth] - self.largeLeftInset; 287 | }else { 288 | NSUInteger stackCount = [self.viewControllers count]; 289 | CGFloat intIndex, restIndex; 290 | restIndex = modff(floatIndex, &intIndex); // split e.g. 1.5 in 1.0 and 0.5 291 | isValid = stackCount > intIndex && contentWidth > ([self screenWidth] - self.leftInset); 292 | if (isValid && fabsf(restIndex - 0.5f) < EPSILON) { // comparing floats -> if so, we have a .5 here 293 | if (ceilf(floatIndex) < stackCount) { // at the end? 294 | CGFloat widthLeft = [[self.viewControllers objectAtIndex:floorf(floatIndex)] containerView].width; 295 | CGFloat widthRight = [[self.viewControllers objectAtIndex:ceilf(floatIndex)] containerView].width; 296 | isValid = (widthLeft + widthRight) > ([self screenWidth] - self.leftInset); 297 | }else { 298 | isValid = NO; 299 | } 300 | } 301 | } 302 | } 303 | return isValid; 304 | } 305 | 306 | - (CGFloat)nearestValidFloatIndex:(CGFloat)floatIndex round:(PSSVRoundOption)roundOption { 307 | CGFloat roundedFloat; 308 | CGFloat intIndex, restIndex; 309 | restIndex = modff(floatIndex, &intIndex); 310 | 311 | if (restIndex < 0.5f) { 312 | if (roundOption == PSSVRoundNearest) { 313 | restIndex = (restIndex < 0.25f) ? 0.f : 0.5f; 314 | }else { 315 | restIndex = (roundOption == PSSVRoundUp) ? 0.5f : 0.f; 316 | } 317 | }else { 318 | if (roundOption == PSSVRoundNearest) { 319 | restIndex = (restIndex < 0.75f) ? 0.5f : 1.f; 320 | }else { 321 | restIndex = (roundOption == PSSVRoundUp) ? 1.f : 0.5f; 322 | } 323 | } 324 | roundedFloat = intIndex + restIndex; 325 | 326 | // now check if this is valid 327 | BOOL isValid = [self isValidFloatIndex:roundedFloat]; 328 | 329 | // if not valid, and custom rounding produced a .5, test again with full rounding 330 | if (!isValid && restIndex == 0.5f) { 331 | CGFloat naturalRoundedIndex; 332 | if (roundOption == PSSVRoundNearest) { 333 | naturalRoundedIndex = roundf(floatIndex); 334 | }else if(roundOption == PSSVRoundUp) { 335 | naturalRoundedIndex = ceilf(floatIndex); 336 | }else { 337 | naturalRoundedIndex = floorf(floatIndex); 338 | } 339 | // if that works out, return it! 340 | if ([self isValidFloatIndex:naturalRoundedIndex]) { 341 | isValid = YES; 342 | roundedFloat = naturalRoundedIndex; 343 | } 344 | } 345 | 346 | // still not valid? start the for loops, find nearest valid index 347 | if (!isValid) { 348 | CGFloat validLowIndex = 0.f, validHighIndex = 0.f; 349 | 350 | // upper bound 351 | CGFloat viewControllerCount = [self.viewControllers count]; 352 | for (CGFloat tester = roundedFloat + 0.5f; tester < viewControllerCount; tester += 0.5f) { 353 | if ([self isValidFloatIndex:tester]) { 354 | validHighIndex = tester; 355 | break; 356 | } 357 | } 358 | // lower bound 359 | for (CGFloat tester = roundedFloat - 0.5f; tester >= 0.f; tester -= 0.5f) { 360 | if ([self isValidFloatIndex:tester]) { 361 | validLowIndex = tester; 362 | break; 363 | } 364 | } 365 | 366 | if (fabsf(validLowIndex - roundedFloat) < fabsf(validHighIndex - roundedFloat)) { 367 | roundedFloat = validLowIndex; 368 | }else { 369 | roundedFloat = validHighIndex; 370 | } 371 | } 372 | 373 | return roundedFloat; 374 | } 375 | 376 | - (CGFloat)nearestValidFloatIndex:(CGFloat)floatIndex { 377 | return [self nearestValidFloatIndex:floatIndex round:PSSVRoundNearest]; 378 | } 379 | 380 | - (CGFloat)nextFloatIndex:(CGFloat)floatIndex { 381 | CGFloat nextFloat = 0.f; 382 | CGFloat roundedFloat = [self nearestValidFloatIndex:floatIndex]; 383 | CGFloat viewControllerCount = [self.viewControllers count]; 384 | for (CGFloat tester = roundedFloat + 0.5f; tester < viewControllerCount; tester += 0.5f) { 385 | if ([self isValidFloatIndex:tester]) { 386 | nextFloat = tester; 387 | break; 388 | } 389 | } 390 | return nextFloat; 391 | } 392 | 393 | - (CGFloat)prevFloatIndex:(CGFloat)floatIndex { 394 | CGFloat prevFloat = 0.f; 395 | CGFloat roundedFloat = [self nearestValidFloatIndex:floatIndex]; 396 | for (CGFloat tester = roundedFloat - 0.5f; tester >= 0.f; tester -= 0.5f) { 397 | if ([self isValidFloatIndex:tester]) { 398 | prevFloat = tester; 399 | break; 400 | } 401 | } 402 | return prevFloat; 403 | } 404 | 405 | /// calculates all rects for current visibleIndex orientation 406 | - (NSArray *)rectsForControllers { 407 | NSMutableArray *frames = [NSMutableArray array]; 408 | 409 | // TODO: currently calculates *all* objects, should cache! 410 | CGFloat floatIndex = [self nearestValidFloatIndex:self.floatIndex]; 411 | [self.viewControllers enumerateObjectsWithOptions:0 usingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 412 | UIViewController *currentVC = (UIViewController *)obj; 413 | CGFloat leftPos = [self currentLeftInset]; 414 | CGRect leftRect = idx > 0 ? [[frames objectAtIndex:idx-1] CGRectValue] : CGRectZero; 415 | 416 | if (idx == floorf(floatIndex)) { 417 | BOOL dockRight = ![self isFloatIndexBetween:floatIndex] && floatIndex >= 1.f; 418 | 419 | // should we pan it to the right? 420 | if (dockRight) { 421 | leftPos = [self screenWidth] - currentVC.containerView.width; 422 | } 423 | }else if (idx > floatIndex) { 424 | // connect vc to left vc's right! 425 | leftPos = leftRect.origin.x + leftRect.size.width; 426 | } 427 | 428 | CGRect currentRect = CGRectMake(leftPos, currentVC.containerView.top, currentVC.containerView.width, currentVC.containerView.height); 429 | [frames addObject:[NSValue valueWithCGRect:currentRect]]; 430 | }]; 431 | 432 | return frames; 433 | } 434 | 435 | /// calculates the specific rect 436 | - (CGRect)rectForControllerAtIndex:(NSUInteger)index { 437 | NSArray *frames = [self rectsForControllers]; 438 | return [[frames objectAtIndex:index] CGRectValue]; 439 | } 440 | 441 | 442 | /// moves a rect around, recalculates following rects 443 | - (NSArray *)modifiedRects:(NSArray *)frames newLeft:(CGFloat)newLeft index:(NSUInteger)index { 444 | NSMutableArray *modifiedFrames = [NSMutableArray arrayWithArray:frames]; 445 | 446 | CGRect prevFrame; 447 | for (int i = index; i < [modifiedFrames count]; i++) { 448 | CGRect vcFrame = [[modifiedFrames objectAtIndex:i] CGRectValue]; 449 | if (i == index) { 450 | vcFrame.origin.x = newLeft; 451 | }else { 452 | vcFrame.origin.x = prevFrame.origin.x + prevFrame.size.width; 453 | } 454 | [modifiedFrames replaceObjectAtIndex:i withObject:[NSValue valueWithCGRect:vcFrame]]; 455 | prevFrame = vcFrame; 456 | } 457 | 458 | return modifiedFrames; 459 | } 460 | 461 | // at some point, dragging does not make any more sense 462 | - (BOOL)snapPointAvailableAfterOffset:(NSInteger)offset { 463 | BOOL snapPointAvailableAfterOffset = YES; 464 | NSUInteger screenWidth = [self screenWidth]; 465 | NSUInteger totalWidth = [self totalStackWidth]; 466 | NSUInteger minCommonWidth = MIN(screenWidth, totalWidth); 467 | // NSArray *frames = [self rectsForControllers]; 468 | 469 | // are we at the end? 470 | UIViewController *topViewController = [self topViewController]; 471 | if (topViewController == [self lastVisibleViewControllerCompletelyVisible:YES]) { 472 | if (minCommonWidth + [self minimalLeftInset] <= topViewController.containerView.right) { 473 | snapPointAvailableAfterOffset = NO; 474 | } 475 | } 476 | 477 | // slow down first controller when dragged to the right 478 | if ([self canCollapseStack] == 0) { 479 | snapPointAvailableAfterOffset = NO; 480 | } 481 | 482 | if ([self firstViewController].containerView.left > self.largeLeftInset) { 483 | snapPointAvailableAfterOffset = NO; 484 | } 485 | 486 | return snapPointAvailableAfterOffset; 487 | } 488 | 489 | - (BOOL)displayViewControllerOnRightMost:(UIViewController *)vc animated:(BOOL)animated { 490 | NSUInteger index = [self indexOfViewController:vc]; 491 | if (index != NSNotFound) { 492 | [self displayViewControllerIndexOnRightMost:index animated:animated]; 493 | return YES; 494 | } 495 | return NO; 496 | } 497 | 498 | // ensures index is on rightmost position 499 | - (void)displayViewControllerIndexOnRightMost:(NSInteger)index animated:(BOOL)animated; { 500 | // add epsilon to round indexes like 1.0 to 2.0, also -1.0 to -2.0 501 | CGFloat floatIndexOffset = index - self.floatIndex; 502 | NSInteger indexOffset = ceilf(floatIndexOffset + (floatIndexOffset > 0 ? EPSILON : -EPSILON)); 503 | if (indexOffset > 0) { 504 | [self collapseStack:indexOffset animated:animated]; 505 | }else if(indexOffset < 0) { 506 | [self expandStack:indexOffset animated:animated]; 507 | } 508 | 509 | // hide menu, if first VC is larger than available screen space with floatIndex = 0.0 510 | else if (index == 0 && [self.viewControllers count] && [[self.viewControllers objectAtIndex:0] containerView].width >= ([self screenWidth] - self.leftInset)) { 511 | self.floatIndex = 0.5f; 512 | [self alignStackAnimated:YES]; 513 | } 514 | } 515 | 516 | // iterates controllers and sets width (also, enlarges if requested width is larger than current width) 517 | - (void)updateViewControllerSizes { 518 | CGFloat maxControllerView = [self maxControllerWidth]; 519 | for (UIViewController *controller in self.viewControllers) { 520 | [controller.containerView limitToMaxWidth:maxControllerView]; 521 | } 522 | } 523 | 524 | - (CGFloat)overlapRatio { 525 | CGFloat overlapRatio = 0.f; 526 | 527 | UIViewController *overlappedVC = [self overlappedViewController]; 528 | if (overlappedVC) { 529 | UIViewController *rightVC = [self nextViewController:overlappedVC]; 530 | PSSVLog(@"overlapping %@ with %@", NSStringFromCGRect(overlappedVC.containerView.frame), NSStringFromCGRect(rightVC.containerView.frame)); 531 | overlapRatio = fabsf(overlappedVC.containerView.right - rightVC.containerView.left)/overlappedVC.containerView.width; 532 | } 533 | return overlapRatio; 534 | } 535 | 536 | // updates view containers 537 | - (void)updateViewControllerMasksAndShadow { 538 | // only one! 539 | if ([self.viewControllers count] == 1) { 540 | // [[self firstViewController].containerView addMaskToCorners:UIRectCornerAllCorners]; 541 | self.firstViewController.containerView.shadow = PSSVSideLeft | PSSVSideRight; 542 | }else { 543 | // rounded corners on first and last controller 544 | [self.viewControllers enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 545 | UIViewController *vc = (UIViewController *)obj; 546 | if (idx == 0) { 547 | //[vc.containerView addMaskToCorners:UIRectCornerBottomLeft | UIRectCornerTopLeft]; 548 | }else if(idx == [self.viewControllers count]-1) { 549 | // [vc.containerView addMaskToCorners:UIRectCornerBottomRight | UIRectCornerTopRight]; 550 | vc.containerView.shadow = PSSVSideLeft | PSSVSideRight; 551 | }else { 552 | // [vc.containerView removeMask]; 553 | vc.containerView.shadow = PSSVSideLeft | PSSVSideRight; 554 | } 555 | }]; 556 | } 557 | 558 | // update alpha mask 559 | CGFloat overlapRatio = [self overlapRatio]; 560 | UIViewController *overlappedVC = [self overlappedViewController]; 561 | overlappedVC.containerView.darkRatio = MIN(overlapRatio, 1.f)/kAlphaReductRatio; 562 | 563 | // reset alpha ratio everywhere else 564 | for (UIViewController *vc in self.viewControllers) { 565 | if (vc != overlappedVC) { 566 | vc.containerView.darkRatio = 0.0f; 567 | } 568 | } 569 | } 570 | 571 | - (NSArray *)visibleViewControllersSetFullyVisible:(BOOL)fullyVisible; { 572 | NSMutableArray *array = [NSMutableArray array]; 573 | [self.viewControllers enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 574 | if ([self isViewControllerVisible:obj completely:fullyVisible]) { 575 | [array addObject:obj]; 576 | } 577 | }]; 578 | 579 | return [array copy]; 580 | } 581 | 582 | 583 | // check if there is any overlapping going on between VCs 584 | - (BOOL)isViewController:(UIViewController *)leftViewController overlappingWith:(UIViewController *)rightViewController { 585 | NSParameterAssert(leftViewController); 586 | NSParameterAssert(rightViewController); 587 | 588 | // figure out which controller is the top one 589 | if ([self indexOfViewController:rightViewController] < [self indexOfViewController:leftViewController]) { 590 | PSSVLog(@"overlapping check flipped! fixing that..."); 591 | UIViewController *tmp = rightViewController; 592 | rightViewController = leftViewController; 593 | leftViewController = tmp; 594 | } 595 | 596 | BOOL overlapping = leftViewController.containerView.right > rightViewController.containerView.left; 597 | if (overlapping) { 598 | PSSVLog(@"overlap detected: %@ (%@) with %@ (%@)", leftViewController, NSStringFromCGRect(leftViewController.containerView.frame), rightViewController, NSStringFromCGRect(rightViewController.containerView.frame)); 599 | } 600 | return overlapping; 601 | } 602 | 603 | // find the rightmost overlapping controller 604 | - (UIViewController *)overlappedViewController { 605 | __block UIViewController *overlappedViewController = nil; 606 | 607 | [self.viewControllers enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 608 | UIViewController *currentViewController = (UIViewController *)obj; 609 | UIViewController *leftViewController = [self previousViewController:currentViewController]; 610 | 611 | BOOL overlapping = NO; 612 | if (leftViewController && currentViewController) { 613 | overlapping = [self isViewController:leftViewController overlappingWith:currentViewController]; 614 | } 615 | 616 | if (overlapping) { 617 | overlappedViewController = leftViewController; 618 | *stop = YES; 619 | } 620 | }]; 621 | 622 | return overlappedViewController; 623 | } 624 | 625 | /////////////////////////////////////////////////////////////////////////////////////////////////// 626 | #pragma mark - Touch Handling 627 | 628 | - (void)stopStackAnimation { 629 | // remove all current animations 630 | //[self.view.layer removeAllAnimations]; 631 | [self.viewControllers enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 632 | UIViewController *vc = (UIViewController *)obj; 633 | [vc.containerView.layer removeAllAnimations]; 634 | }]; 635 | } 636 | 637 | // moves the stack to a specific offset. 638 | - (void)moveStackWithOffset:(NSInteger)offset animated:(BOOL)animated userDragging:(BOOL)userDragging { 639 | PSSVLog(@"moving stack on %d pixels (animated:%d, decellerating:%d)", offset, animated, userDragging); 640 | 641 | [self stopStackAnimation]; 642 | [UIView animateWithDuration:animated ? kPSSVStackAnimationDuration : 0.f delay:0.f options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction animations:^{ 643 | 644 | // enumerate controllers from rig ht to left 645 | // scroll each controller until we begin to overlap! 646 | __block BOOL isTopViewController = YES; 647 | [self.viewControllers enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 648 | UIViewController *currentViewController = (UIViewController *)obj; 649 | UIViewController *leftViewController = [self previousViewController:currentViewController]; 650 | UIViewController *rightViewController = [self nextViewController:currentViewController]; 651 | NSInteger minimalLeftInset = [self minimalLeftInset]; 652 | 653 | // we just move the top view controller 654 | NSInteger currentVCLeftPosition = currentViewController.containerView.left; 655 | if (isTopViewController) { 656 | currentVCLeftPosition += offset; 657 | }else { 658 | // make sure we're connected to the next controller! 659 | currentVCLeftPosition = rightViewController.containerView.left - currentViewController.containerView.width; 660 | } 661 | 662 | // prevent scrolling < minimal width (except for the top view controller - allow stupidness!) 663 | if (currentVCLeftPosition < minimalLeftInset && (!userDragging || (userDragging && !isTopViewController))) { 664 | currentVCLeftPosition = minimalLeftInset; 665 | } 666 | 667 | // a previous view controller is not allowed to overlap the next view controller. 668 | if (leftViewController && leftViewController.containerView.right > currentVCLeftPosition) { 669 | NSInteger leftVCLeftPosition = currentVCLeftPosition - leftViewController.containerView.width; 670 | if (leftVCLeftPosition < minimalLeftInset) { 671 | leftVCLeftPosition = minimalLeftInset; 672 | } 673 | leftViewController.containerView.left = leftVCLeftPosition; 674 | } 675 | 676 | currentViewController.containerView.left = currentVCLeftPosition; 677 | 678 | isTopViewController = NO; // there can only be one. 679 | }]; 680 | 681 | [self updateViewControllerMasksAndShadow]; 682 | 683 | 684 | // special case, if we have overlapping controllers! 685 | // in this case underlying controllers are visible, but they are overlapped by another controller 686 | UIViewController *lastViewController = [self lastVisibleViewControllerCompletelyVisible:YES]; 687 | // there may be no controller completely visible - use partly visible then 688 | if (!lastViewController) { 689 | NSArray *visibleViewControllers = self.visibleViewControllers; 690 | lastViewController = [visibleViewControllers count] ? [visibleViewControllers objectAtIndex:0] : nil; 691 | } 692 | 693 | // calculate float index 694 | NSUInteger newFirstVisibleIndex = lastViewController ? [self indexOfViewController:lastViewController] : 0; 695 | CGFloat floatIndex = [self nearestValidFloatIndex:newFirstVisibleIndex]; // absolut value 696 | 697 | CGFloat overlapRatio = 0.f; 698 | UIViewController *overlappedVC = [self overlappedViewController]; 699 | if (overlappedVC) { 700 | UIViewController *rightVC = [self nextViewController:overlappedVC]; 701 | PSSVLog(@"overlapping %@ with %@", NSStringFromCGRect(overlappedVC.containerView.frame), NSStringFromCGRect(rightVC.containerView.frame)); 702 | overlapRatio = fabsf(overlappedVC.containerView.right - rightVC.containerView.left)/(overlappedVC.containerView.right - ([self screenWidth] - rightVC.containerView.width)); 703 | } 704 | 705 | // only update ratio if < 1 (else we move sth else) 706 | if (overlapRatio <= 1.f && overlapRatio > 0.f) { 707 | floatIndex += 0.5f + overlapRatio*0.5f; // fully overlapped = the .5 ratio! 708 | }else { 709 | // overlap ratio 710 | UIViewController *lastVC = [self.visibleViewControllers lastObject]; 711 | UIViewController *prevVC = [self previousViewController:lastVC]; 712 | if (lastVC && prevVC && lastVC.containerView.right > [self screenWidth]) { 713 | overlapRatio = fabsf(([self screenWidth] - lastVC.containerView.left)/([self screenWidth] - (self.leftInset + prevVC.containerView.width)))*.5f; 714 | floatIndex += overlapRatio; 715 | } 716 | } 717 | 718 | // special case for menu 719 | if (floatIndex == 0.f) { 720 | CGFloat menuCollapsedRatio = (self.largeLeftInset - self.firstViewController.containerView.left)/(self.largeLeftInset - self.leftInset); 721 | menuCollapsedRatio = MAX(0.0f, MIN(0.5f, menuCollapsedRatio/2)); 722 | floatIndex += menuCollapsedRatio; 723 | } 724 | 725 | floatIndex_ = floatIndex; 726 | } completion:nil]; 727 | } 728 | 729 | - (void)handlePanFrom:(UIPanGestureRecognizer *)recognizer { 730 | CGPoint translatedPoint = [recognizer translationInView:self.view]; 731 | UIGestureRecognizerState state = recognizer.state; 732 | 733 | // reset last offset if gesture just started 734 | if (state == UIGestureRecognizerStateBegan) { 735 | lastDragOffset_ = 0; 736 | } 737 | 738 | NSInteger offset = translatedPoint.x - lastDragOffset_; 739 | 740 | // if the move does not make sense (no snapping region), only use 1/2 offset 741 | BOOL snapPointAvailable = [self snapPointAvailableAfterOffset:offset]; 742 | if (!snapPointAvailable) { 743 | PSSVLog(@"offset dividing/2 in effect"); 744 | 745 | // we only want to move full pixels - but if we drag slowly, 1 get divided to zero. 746 | // so only omit every second event 747 | if (abs(offset) == 1) { 748 | if(!lastDragDividedOne_) { 749 | lastDragDividedOne_ = YES; 750 | offset = 0; 751 | }else { 752 | lastDragDividedOne_ = NO; 753 | } 754 | }else { 755 | offset = roundf(offset/2.f); 756 | } 757 | } 758 | [self moveStackWithOffset:offset animated:NO userDragging:YES]; 759 | 760 | // set up designated drag destination 761 | if (state == UIGestureRecognizerStateBegan) { 762 | if (offset > 0) { 763 | lastDragOption_ = SVSnapOptionRight; 764 | }else { 765 | lastDragOption_ = SVSnapOptionLeft; 766 | } 767 | }else { 768 | // if there's a continuous drag in one direction, keep designation - else use nearest to snap. 769 | if ((lastDragOption_ == SVSnapOptionLeft && offset > 0) || (lastDragOption_ == SVSnapOptionRight && offset < 0)) { 770 | lastDragOption_ = SVSnapOptionNearest; 771 | } 772 | } 773 | 774 | // save last point to calculate new offset 775 | if (state == UIGestureRecognizerStateBegan || state == UIGestureRecognizerStateChanged) { 776 | lastDragOffset_ = translatedPoint.x; 777 | } 778 | 779 | // perform snapping after gesture ended 780 | BOOL gestureEnded = state == UIGestureRecognizerStateEnded; 781 | if (gestureEnded) { 782 | 783 | if (lastDragOption_ == SVSnapOptionRight) { 784 | self.floatIndex = [self nearestValidFloatIndex:self.floatIndex round:PSSVRoundDown]; 785 | }else if(lastDragOption_ == SVSnapOptionLeft) { 786 | self.floatIndex = [self nearestValidFloatIndex:self.floatIndex round:PSSVRoundUp]; 787 | }else { 788 | self.floatIndex = [self nearestValidFloatIndex:self.floatIndex round:PSSVRoundNearest]; 789 | } 790 | 791 | [self alignStackAnimated:YES]; 792 | } 793 | } 794 | 795 | /////////////////////////////////////////////////////////////////////////////////////////////////// 796 | #pragma mark - SVStackRootController (Public) 797 | 798 | - (NSInteger)indexOfViewController:(UIViewController *)viewController { 799 | __block NSUInteger index = [self.viewControllers indexOfObject:viewController]; 800 | if (index == NSNotFound) { 801 | index = [self.viewControllers indexOfObject:viewController.navigationController]; 802 | if (index == NSNotFound) { 803 | [self.viewControllers enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 804 | if ([obj isKindOfClass:[UINavigationController class]] && ((UINavigationController *)obj).topViewController == viewController) { 805 | index = idx; 806 | *stop = YES; 807 | } 808 | }]; 809 | } 810 | } 811 | return index; 812 | } 813 | 814 | - (UIViewController *)topViewController { 815 | return [self.viewControllers lastObject]; 816 | } 817 | 818 | - (UIViewController *)firstViewController { 819 | return [self.viewControllers count] ? [self.viewControllers objectAtIndex:0] : nil; 820 | } 821 | 822 | - (NSArray *)visibleViewControllers { 823 | return [self visibleViewControllersSetFullyVisible:NO]; 824 | } 825 | 826 | - (NSArray *)fullyVisibleViewControllers { 827 | return [self visibleViewControllersSetFullyVisible:YES]; 828 | } 829 | 830 | - (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated; { 831 | [self pushViewController:viewController fromViewController:self.topViewController animated:animated]; 832 | } 833 | 834 | - (void)pushViewController:(UIViewController *)viewController fromViewController:(UIViewController *)baseViewController animated:(BOOL)animated; { 835 | // figure out where to push, and if we need to get rid of some viewControllers 836 | if (baseViewController) { 837 | [self.viewControllers enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 838 | UIViewController *baseVC = objc_getAssociatedObject(obj, kPSSVAssociatedBaseViewControllerKey); 839 | if (baseVC == baseViewController) { 840 | PSSVLog(@"BaseViewController found on index: %d", idx); 841 | UIViewController *parentVC = [self previousViewController:obj]; 842 | if (parentVC) { 843 | [self popToViewController:parentVC animated:animated]; 844 | }else { 845 | [self popToRootViewControllerAnimated:animated]; 846 | } 847 | *stop = YES; 848 | } 849 | }]; 850 | 851 | objc_setAssociatedObject(viewController, kPSSVAssociatedBaseViewControllerKey, baseViewController, OBJC_ASSOCIATION_ASSIGN); // associate weak 852 | } 853 | 854 | PSSVLog(@"pushing with index %d on stack: %@ (animated: %d)", [self.viewControllers count], viewController, animated); 855 | viewController.view.height = [self screenHeight]; 856 | 857 | // get predefined stack width; query topViewController if we have a UINavigationController 858 | CGFloat stackWidth = viewController.stackWidth; 859 | if (stackWidth == 0.f && [viewController isKindOfClass:[UINavigationController class]]) { 860 | UIViewController *topVC = ((UINavigationController *)viewController).topViewController; 861 | stackWidth = topVC.stackWidth; 862 | } 863 | if (stackWidth > 0.f) { 864 | viewController.view.width = stackWidth; 865 | } 866 | 867 | // Starting out in portrait, right side up, we see a 20 pixel gap (for status bar???) 868 | viewController.view.top = 0.f; 869 | 870 | [self delegateWillInsertViewController:viewController]; 871 | 872 | // controller view is embedded into a container 873 | PSSVContainerView *container = [PSSVContainerView containerViewWithController:viewController]; 874 | NSUInteger leftGap = [self totalStackWidth] + [self minimalLeftInset]; 875 | container.left = leftGap; 876 | container.width = viewController.view.width; 877 | container.autoresizingMask = UIViewAutoresizingFlexibleHeight; // width is not flexible! 878 | [container limitToMaxWidth:[self maxControllerWidth]]; 879 | PSSVLog(@"container frame: %@", NSStringFromCGRect(container.frame)); 880 | 881 | // relay willAppear and add to subview 882 | [viewController viewWillAppear:animated]; 883 | 884 | if (animated) { 885 | container.alpha = 0.f; 886 | container.transform = CGAffineTransformMakeScale(1.2, 1.2); // large but fade in 887 | } 888 | 889 | [self.view addSubview:container]; 890 | 891 | if (animated) { 892 | [UIView animateWithDuration:kPSSVStackAnimationPushDuration delay:0.f options:UIViewAnimationOptionAllowUserInteraction animations:^{ 893 | container.alpha = 1.f; 894 | container.transform = CGAffineTransformIdentity; 895 | } completion:nil]; 896 | } 897 | 898 | // properly sizes the scroll view contents (for table view scrolling) 899 | [container layoutIfNeeded]; 900 | //container.width = viewController.view.width; // sync width (after it may has changed in layoutIfNeeded) 901 | 902 | [viewController viewDidAppear:animated]; 903 | [viewControllers_ addObject:viewController]; 904 | 905 | // register stack controller 906 | objc_setAssociatedObject(viewController, kPSSVAssociatedStackViewControllerKey, self, OBJC_ASSOCIATION_ASSIGN); 907 | 908 | [self updateViewControllerMasksAndShadow]; 909 | [self displayViewControllerIndexOnRightMost:[self.viewControllers count]-1 animated:animated]; 910 | [self delegateDidInsertViewController:viewController]; 911 | } 912 | 913 | - (BOOL)popViewController:(UIViewController *)controller animated:(BOOL)animated { 914 | if (controller != self.topViewController) { 915 | return NO; 916 | }else { 917 | return [self popViewControllerAnimated:animated] == controller; 918 | } 919 | } 920 | 921 | - (UIViewController *)popViewControllerAnimated:(BOOL)animated; { 922 | PSSVLog(@"popping controller: %@ (#%d total, animated:%d)", [self topViewController], [self.viewControllers count], animated); 923 | 924 | UIViewController *lastController = [self topViewController]; 925 | if (lastController) { 926 | [self delegateWillRemoveViewController:lastController]; 927 | 928 | // remove from view stack! 929 | PSSVContainerView *container = lastController.containerView; 930 | [lastController viewWillDisappear:animated]; 931 | 932 | PSSVSimpleBlock finishBlock = ^{ 933 | [container removeFromSuperview]; 934 | [lastController viewDidDisappear:animated]; 935 | [self delegateDidRemoveViewController:lastController]; 936 | }; 937 | 938 | if (animated) { // kPSSVStackAnimationDuration 939 | [UIView animateWithDuration:kPSSVStackAnimationPopDuration delay:0.f options:UIViewAnimationOptionBeginFromCurrentState animations:^(void) { 940 | lastController.containerView.alpha = 0.f; 941 | lastController.containerView.transform = CGAffineTransformMakeScale(0.8, 0.8); // make smaller while fading out 942 | } completion:^(BOOL finished) { 943 | // even with duration = 0, this doesn't fire instantly but on a future runloop with NSFireDelayedPerform, thus ugly double-check 944 | if (finished) { 945 | finishBlock(); 946 | } 947 | }]; 948 | } 949 | else { 950 | finishBlock(); 951 | } 952 | 953 | [viewControllers_ removeLastObject]; 954 | 955 | // save current stack controller as an associated object. 956 | objc_setAssociatedObject(lastController, kPSSVAssociatedStackViewControllerKey, nil, OBJC_ASSOCIATION_ASSIGN); 957 | 958 | // realign view controllers 959 | [self updateViewControllerMasksAndShadow]; 960 | [self alignStackAnimated:animated]; 961 | } 962 | 963 | return lastController; 964 | } 965 | 966 | - (NSArray *)popToRootViewControllerAnimated:(BOOL)animated; { 967 | NSMutableArray *array = [NSMutableArray array]; 968 | while ([self.viewControllers count] > 0) { 969 | UIViewController *vc = [self popViewControllerAnimated:animated]; 970 | [array addObject:vc]; 971 | } 972 | return array; 973 | } 974 | 975 | // get view controllers that are in stack _after_ current view controller 976 | - (NSArray *)viewControllersAfterViewController:(UIViewController *)viewController { 977 | NSParameterAssert(viewController); 978 | NSUInteger index = [self indexOfViewController:viewController]; 979 | if (NSNotFound == index) { 980 | return nil; 981 | } 982 | 983 | NSArray *array = nil; 984 | // don't remove view controller we've been called with 985 | if ([self.viewControllers count] > index + 1) { 986 | array = [self.viewControllers subarrayWithRange:NSMakeRange(index + 1, [self.viewControllers count] - index - 1)]; 987 | } 988 | 989 | return array; 990 | } 991 | 992 | - (NSArray *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated; { 993 | NSParameterAssert(viewController); 994 | 995 | NSUInteger index = [self indexOfViewController:viewController]; 996 | if (NSNotFound == index) { 997 | return nil; 998 | } 999 | PSSVLog(@"popping to index %d, from %d", index, [self.viewControllers count]); 1000 | 1001 | NSArray *controllersToRemove = [self viewControllersAfterViewController:viewController]; 1002 | [controllersToRemove enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 1003 | [self popViewControllerAnimated:animated]; 1004 | }]; 1005 | 1006 | return controllersToRemove; 1007 | } 1008 | 1009 | - (NSArray *)controllersForClass:(Class)theClass { 1010 | NSArray *controllers = [self.viewControllers filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) { 1011 | return [evaluatedObject isKindOfClass:theClass] || ([evaluatedObject isKindOfClass:[UINavigationController class]] && [((UINavigationController *)evaluatedObject).topViewController isKindOfClass:theClass]); 1012 | }]]; 1013 | return controllers; 1014 | } 1015 | 1016 | // last visible index is calculated dynamically, depending on width of VCs 1017 | - (NSInteger)lastVisibleIndex { 1018 | NSInteger lastVisibleIndex = self.firstVisibleIndex; 1019 | 1020 | NSUInteger currentLeftInset = [self currentLeftInset]; 1021 | NSInteger screenSpaceLeft = [self screenWidth] - currentLeftInset; 1022 | while (screenSpaceLeft > 0 && lastVisibleIndex < [self.viewControllers count]) { 1023 | UIViewController *vc = [self.viewControllers objectAtIndex:lastVisibleIndex]; 1024 | screenSpaceLeft -= vc.containerView.width; 1025 | 1026 | if (screenSpaceLeft >= 0) { 1027 | lastVisibleIndex++; 1028 | } 1029 | } 1030 | 1031 | if (lastVisibleIndex > 0) { 1032 | lastVisibleIndex--; // compensate for last failure 1033 | } 1034 | 1035 | return lastVisibleIndex; 1036 | } 1037 | 1038 | // returns +/- amount if grid is not aligned correctly 1039 | // + if view is too far on the right, - if too far on the left 1040 | - (CGFloat)gridOffsetByPixels { 1041 | CGFloat gridOffset = 0; 1042 | 1043 | CGFloat firstVCLeft = self.firstViewController.containerView.left; 1044 | 1045 | // easiest case, controller is > then wide menu 1046 | if (firstVCLeft > [self currentLeftInset] || firstVCLeft < [self currentLeftInset]) { 1047 | gridOffset = firstVCLeft - [self currentLeftInset]; 1048 | }else { 1049 | NSUInteger targetIndex = self.firstVisibleIndex; // default, abs(gridOffset) < 1 1050 | 1051 | UIViewController *overlappedVC = [self overlappedViewController]; 1052 | if (overlappedVC) { 1053 | UIViewController *rightVC = [self nextViewController:overlappedVC]; 1054 | targetIndex = [self indexOfViewController:rightVC]; 1055 | PSSVLog(@"overlapping %@ with %@", NSStringFromCGRect(overlappedVC.containerView.frame), NSStringFromCGRect(rightVC.containerView.frame)); 1056 | } 1057 | 1058 | UIViewController *targetVCController = [self.viewControllers objectAtIndex:targetIndex]; 1059 | CGRect targetVCFrame = [self rectForControllerAtIndex:targetIndex]; 1060 | gridOffset = targetVCController.containerView.left - targetVCFrame.origin.x; 1061 | } 1062 | 1063 | PSSVLog(@"gridOffset: %f", gridOffset); 1064 | return gridOffset; 1065 | } 1066 | 1067 | /// detect if last drag offset is large enough that we should make a snap animation 1068 | - (BOOL)shouldSnapAnimate { 1069 | BOOL shouldSnapAnimate = abs(lastDragOffset_) > 10; 1070 | return shouldSnapAnimate; 1071 | } 1072 | 1073 | // bouncing is a three-way operation 1074 | enum { 1075 | PSSVBounceNone, 1076 | PSSVBounceMoveToInitial, 1077 | PSSVBounceBleedOver, 1078 | PSSVBounceBack, 1079 | }typedef PSSVBounceOption; 1080 | 1081 | - (void)alignStackAnimated:(BOOL)animated duration:(CGFloat)duration bounceType:(PSSVBounceOption)bounce; { 1082 | animated = animated && !self.isReducingAnimations; // don't animate if set 1083 | self.floatIndex = [self nearestValidFloatIndex:self.floatIndex]; // round to nearest correct index 1084 | UIViewAnimationCurve animationCurve = UIViewAnimationCurveEaseInOut; 1085 | if (animated) { 1086 | if (bounce == PSSVBounceMoveToInitial) { 1087 | if ([self shouldSnapAnimate]) { 1088 | animationCurve = UIViewAnimationCurveLinear; 1089 | } 1090 | CGFloat gridOffset = [self gridOffsetByPixels]; 1091 | snapBackFromLeft_ = gridOffset < 0; 1092 | 1093 | // some magic numbers to better reflect movement time 1094 | duration = abs(gridOffset)/200.f * duration * 0.4f + duration * 0.6f; 1095 | }else if(bounce == PSSVBounceBleedOver) { 1096 | animationCurve = UIViewAnimationCurveEaseOut; 1097 | } 1098 | } 1099 | 1100 | PSSVSimpleBlock alignmentBlock = ^{ 1101 | 1102 | PSSVLog(@"Begin aliging VCs. Last drag offset:%d direction:%d bounce:%d.", lastDragOffset_, lastDragOption_, bounce); 1103 | 1104 | // calculate offset used only when we're bleeding over 1105 | NSInteger snapOverOffset = 0; // > 0 = <--- ; we scrolled from right to left. 1106 | NSUInteger firstVisibleIndex = [self firstVisibleIndex]; 1107 | NSUInteger lastFullyVCIndex = [self indexOfViewController:[self lastVisibleViewControllerCompletelyVisible:YES]]; 1108 | BOOL bounceAtVeryEnd = NO; 1109 | 1110 | if ([self shouldSnapAnimate] && bounce == PSSVBounceBleedOver) { 1111 | snapOverOffset = abs(lastDragOffset_ / 5.f); 1112 | if (snapOverOffset > kPSSVMaxSnapOverOffset) { 1113 | snapOverOffset = kPSSVMaxSnapOverOffset; 1114 | } 1115 | 1116 | // positive/negative snap offset depending on snap back direction 1117 | snapOverOffset *= snapBackFromLeft_ ? 1 : -1; 1118 | 1119 | // if we're dragging menu all the way out, bounce back in 1120 | PSSVLog(@"%@", NSStringFromCGRect(self.firstViewController.containerView.frame)); 1121 | CGFloat firstVCLeft = self.firstViewController.containerView.left; 1122 | if (firstVisibleIndex == 0 && !snapBackFromLeft_ && firstVCLeft >= self.largeLeftInset) { 1123 | bounceAtVeryEnd = YES; 1124 | }else if(lastFullyVCIndex == [self.viewControllers count]-1 && lastFullyVCIndex > 0) { 1125 | bounceAtVeryEnd = YES; 1126 | } 1127 | 1128 | PSSVLog(@"bouncing with offset: %d, firstIndex:%d, snapToLeft:%d veryEnd:%d", snapOverOffset, firstVisibleIndex, snapOverOffset<0, bounceAtVeryEnd); 1129 | } 1130 | 1131 | // iterate over all view controllers and snap them to their correct positions 1132 | __block NSArray *frames = [self rectsForControllers]; 1133 | [self.viewControllers enumerateObjectsWithOptions:0 usingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 1134 | UIViewController *currentVC = (UIViewController *)obj; 1135 | 1136 | CGRect currentFrame = [[frames objectAtIndex:idx] CGRectValue]; 1137 | currentVC.containerView.left = currentFrame.origin.x; 1138 | 1139 | // menu drag to right case or swiping last vc towards menu 1140 | if (bounceAtVeryEnd) { 1141 | if (idx == firstVisibleIndex) { 1142 | frames = [self modifiedRects:frames newLeft:currentVC.containerView.left + snapOverOffset index:idx]; 1143 | } 1144 | } 1145 | // snap the leftmost view controller 1146 | else if ((snapOverOffset > 0 && idx == firstVisibleIndex) || (snapOverOffset < 0 && (idx == firstVisibleIndex+1)) 1147 | || [self.viewControllers count] == 1) { 1148 | frames = [self modifiedRects:frames newLeft:currentVC.containerView.left + snapOverOffset index:idx]; 1149 | } 1150 | 1151 | // set again (maybe changed) 1152 | currentFrame = [[frames objectAtIndex:idx] CGRectValue]; 1153 | currentVC.containerView.left = currentFrame.origin.x; 1154 | }]; 1155 | 1156 | [self updateViewControllerMasksAndShadow]; 1157 | 1158 | }; 1159 | 1160 | if (animated) { 1161 | [UIView animateWithDuration:duration delay:0.f 1162 | options:UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionBeginFromCurrentState | animationCurve 1163 | animations:alignmentBlock completion:^(BOOL finished) { 1164 | /* Scroll physics are applied here. Drag speed is saved in lastDragOffset. (direction with +/-, speed) 1165 | * If we are above a certain speed, we "shoot over the target", then snap back. 1166 | * This is of course dependent on the direction we scrolled. 1167 | * 1168 | * Right swiping (collapsing) makes the next vc overlapping the current vc a few pixels. 1169 | * Left swiping (expanding) takes the parent controller a few pixels with, then snapping back. 1170 | * 1171 | * We have 3 animations total 1172 | * 1) scroll to correct position 1173 | * 2) bleed over 1174 | * 3) snap back to correct position 1175 | */ 1176 | if (finished && [self shouldSnapAnimate]) { 1177 | CGFloat animationDuration = kPSSVStackAnimationBounceDuration/2.f; 1178 | switch (bounce) { 1179 | case PSSVBounceMoveToInitial: { 1180 | // bleed over now! 1181 | [self alignStackAnimated:YES duration:animationDuration bounceType:PSSVBounceBleedOver]; 1182 | }break; 1183 | case PSSVBounceBleedOver: { 1184 | // now bounce back to origin 1185 | [self alignStackAnimated:YES duration:animationDuration bounceType:PSSVBounceBack]; 1186 | }break; 1187 | 1188 | // we're done here 1189 | case PSSVBounceNone: 1190 | case PSSVBounceBack: 1191 | default: { 1192 | lastDragOffset_ = 0; // clear last drag offset for the animation 1193 | //[self removeAnimationBlockerView]; 1194 | }break; 1195 | } 1196 | } 1197 | 1198 | } 1199 | ]; 1200 | } 1201 | else { 1202 | alignmentBlock(); 1203 | } 1204 | 1205 | } 1206 | 1207 | - (void)alignStackAnimated:(BOOL)animated; { 1208 | [self alignStackAnimated:animated duration:kPSSVStackAnimationDuration bounceType:PSSVBounceMoveToInitial]; 1209 | } 1210 | 1211 | - (NSUInteger)canCollapseStack; { 1212 | NSUInteger steps = [self.viewControllers count] - self.firstVisibleIndex - 1; 1213 | 1214 | if (self.lastVisibleIndex == [self.viewControllers count]-1) { 1215 | //PSSVLog(@"complete stack is displayed - aborting."); 1216 | steps = 0; 1217 | }else if (self.firstVisibleIndex + steps > [self.viewControllers count]-1) { 1218 | steps = [self.viewControllers count] - self.firstVisibleIndex - 1; 1219 | //PSSVLog(@"too much steps, adjusting to %d", steps); 1220 | } 1221 | 1222 | return steps; 1223 | } 1224 | 1225 | 1226 | - (NSUInteger)collapseStack:(NSInteger)steps animated:(BOOL)animated; { // (<--- increases firstVisibleIndex) 1227 | PSSVLog(@"collapsing stack with %d steps [%d-%d]", steps, self.firstVisibleIndex, self.lastVisibleIndex); 1228 | 1229 | CGFloat newFloatIndex = self.floatIndex; 1230 | while (steps > 0) { 1231 | newFloatIndex = [self nextFloatIndex:newFloatIndex]; 1232 | steps--; 1233 | } 1234 | 1235 | if (newFloatIndex > 0.f) { 1236 | self.floatIndex = MAX(newFloatIndex, self.floatIndex); 1237 | } 1238 | 1239 | [self alignStackAnimated:animated]; 1240 | return steps; 1241 | } 1242 | 1243 | 1244 | - (NSUInteger)canExpandStack; { 1245 | NSUInteger steps = self.firstVisibleIndex; 1246 | 1247 | // sanity check 1248 | if (steps >= [self.viewControllers count]-1) { 1249 | PSSVLog(@"Warning: firstVisibleIndex is higher than viewController count!"); 1250 | steps = [self.viewControllers count]-1; 1251 | } 1252 | 1253 | return steps; 1254 | } 1255 | 1256 | - (NSUInteger)expandStack:(NSInteger)steps animated:(BOOL)animated; { // (---> decreases firstVisibleIndex) 1257 | steps = abs(steps); // normalize 1258 | PSSVLog(@"expanding stack with %d steps [%d-%d]", steps, self.firstVisibleIndex, self.lastVisibleIndex); 1259 | 1260 | CGFloat newFloatIndex = self.floatIndex; 1261 | while (steps > 0) { 1262 | newFloatIndex = [self prevFloatIndex:newFloatIndex]; 1263 | steps--; 1264 | } 1265 | 1266 | self.floatIndex = MIN(newFloatIndex, self.floatIndex); 1267 | 1268 | [self alignStackAnimated:animated]; 1269 | return steps; 1270 | } 1271 | 1272 | - (void)setLeftInset:(NSUInteger)leftInset { 1273 | [self setLeftInset:leftInset animated:NO]; 1274 | } 1275 | 1276 | - (void)setLeftInset:(NSUInteger)leftInset animated:(BOOL)animated; { 1277 | leftInset_ = leftInset; 1278 | [self alignStackAnimated:animated]; 1279 | } 1280 | 1281 | - (void)setLargeLeftInset:(NSUInteger)leftInset { 1282 | [self setLargeLeftInset:leftInset animated:NO]; 1283 | } 1284 | 1285 | - (void)setLargeLeftInset:(NSUInteger)leftInset animated:(BOOL)animated; { 1286 | largeLeftInset_ = leftInset; 1287 | [self alignStackAnimated:animated]; 1288 | } 1289 | 1290 | /////////////////////////////////////////////////////////////////////////////////////////////////// 1291 | #pragma mark - UIView 1292 | 1293 | - (void)viewDidLoad { 1294 | [super viewDidLoad]; 1295 | 1296 | // embedding rootViewController 1297 | if (self.rootViewController) { 1298 | [self.view addSubview:self.rootViewController.view]; 1299 | self.rootViewController.view.frame = self.view.bounds; 1300 | self.rootViewController.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 1301 | } 1302 | 1303 | for (UIViewController *controller in self.viewControllers) { 1304 | // forces view loading, calls viewDidLoad via system 1305 | UIView *controllerView = controller.view; 1306 | #pragma unused(controllerView) 1307 | // [controller viewDidLoad]; 1308 | } 1309 | } 1310 | 1311 | - (void)viewWillAppear:(BOOL)animated { 1312 | [super viewWillAppear:animated]; 1313 | 1314 | [self.rootViewController viewWillAppear:animated]; 1315 | for (UIViewController *controller in self.viewControllers) { 1316 | [controller viewWillAppear:animated]; 1317 | } 1318 | 1319 | // enlarge/shrinken stack 1320 | [self updateViewControllerSizes]; 1321 | [self updateViewControllerMasksAndShadow]; 1322 | [self alignStackAnimated:NO]; 1323 | } 1324 | 1325 | - (void)viewDidAppear:(BOOL)animated { 1326 | [super viewDidAppear:animated]; 1327 | 1328 | [self.rootViewController viewDidAppear:animated]; 1329 | for (UIViewController *controller in self.viewControllers) { 1330 | [controller viewDidAppear:animated]; 1331 | } 1332 | } 1333 | 1334 | - (void)viewWillDisappear:(BOOL)animated { 1335 | [super viewWillDisappear:animated]; 1336 | 1337 | [self.rootViewController viewWillDisappear:animated]; 1338 | for (UIViewController *controller in self.viewControllers) { 1339 | [controller viewWillDisappear:animated]; 1340 | } 1341 | } 1342 | 1343 | - (void)viewDidDisappear:(BOOL)animated { 1344 | [super viewDidDisappear:animated]; 1345 | 1346 | [self.rootViewController viewDidDisappear:animated]; 1347 | for (UIViewController *controller in self.viewControllers) { 1348 | [controller viewDidDisappear:animated]; 1349 | } 1350 | } 1351 | 1352 | - (void)viewDidUnload { 1353 | [self.rootViewController.view removeFromSuperview]; 1354 | self.rootViewController.view = nil; 1355 | [self.rootViewController viewDidUnload]; 1356 | 1357 | for (UIViewController *controller in self.viewControllers) { 1358 | [controller.view removeFromSuperview]; 1359 | controller.view = nil; 1360 | [controller viewDidUnload]; 1361 | } 1362 | 1363 | [super viewDidUnload]; 1364 | } 1365 | 1366 | - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation { 1367 | if (PSIsIpad()) { 1368 | return YES; 1369 | }else { 1370 | return toInterfaceOrientation != UIInterfaceOrientationPortraitUpsideDown; 1371 | } 1372 | } 1373 | 1374 | // event relay 1375 | - (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration; { 1376 | //lastVisibleIndexBeforeRotation_ = self.lastVisibleIndex; 1377 | 1378 | [rootViewController_ willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration]; 1379 | 1380 | for (UIViewController *controller in self.viewControllers) { 1381 | [controller willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration]; 1382 | } 1383 | } 1384 | 1385 | // event relay 1386 | - (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation; { 1387 | [rootViewController_ didRotateFromInterfaceOrientation:fromInterfaceOrientation]; 1388 | 1389 | if (self.isReducingAnimations) { 1390 | [self updateViewControllerSizes]; 1391 | [self updateViewControllerMasksAndShadow]; 1392 | } 1393 | 1394 | for (UIViewController *controller in self.viewControllers) { 1395 | [controller didRotateFromInterfaceOrientation:fromInterfaceOrientation]; 1396 | } 1397 | 1398 | // ensure we're correctly aligned (may be messed up in willAnimate, if panRecognizer is still active) 1399 | [self alignStackAnimated:!self.isReducingAnimations]; 1400 | } 1401 | 1402 | // event relay 1403 | - (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration; { 1404 | [rootViewController_ willAnimateRotationToInterfaceOrientation:toInterfaceOrientation duration:duration]; 1405 | 1406 | if (!self.isReducingAnimations) { 1407 | [self updateViewControllerSizes]; 1408 | [self updateViewControllerMasksAndShadow]; 1409 | } 1410 | 1411 | // finally relay rotation events 1412 | for (UIViewController *controller in self.viewControllers) { 1413 | [controller willAnimateRotationToInterfaceOrientation:toInterfaceOrientation duration:duration]; 1414 | } 1415 | 1416 | // enlarge/shrinken stack 1417 | [self alignStackAnimated:!self.isReducingAnimations]; 1418 | } 1419 | 1420 | /////////////////////////////////////////////////////////////////////////////////////////////////// 1421 | #pragma mark - UIGestureRecognizerDelegate 1422 | 1423 | - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch { 1424 | if ([touch.view isKindOfClass:[UIControl class]]) { 1425 | // prevent recognizing touches on the slider 1426 | return NO; 1427 | } 1428 | return YES; 1429 | } 1430 | 1431 | @end 1432 | --------------------------------------------------------------------------------