├── Example ├── StackedViewKitExample │ ├── en.lproj │ │ └── InfoPlist.strings │ ├── Images │ │ ├── glow.png │ │ ├── 08-chat.png │ │ ├── 15-tags.png │ │ ├── 11-clock.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 ├── PSStackedView ├── PSStackedViewGlobal.m ├── PSStackedView.h ├── UIView+PSSizes.h ├── UIViewController+PSStackedView.h ├── PSSVContainerView.h ├── PSStackedViewDelegate.h ├── UIViewController+PSStackedView.m ├── UIView+PSSizes.m ├── PSStackedViewGlobal.h ├── PSStackedViewController.h ├── PSSVContainerView.m └── PSStackedViewController.m ├── .gitignore ├── 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/gamma/PSStackedView/master/Example/StackedViewKitExample/Images/glow.png -------------------------------------------------------------------------------- /Example/StackedViewKitExample/Images/08-chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamma/PSStackedView/master/Example/StackedViewKitExample/Images/08-chat.png -------------------------------------------------------------------------------- /Example/StackedViewKitExample/Images/15-tags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamma/PSStackedView/master/Example/StackedViewKitExample/Images/15-tags.png -------------------------------------------------------------------------------- /Example/StackedViewKitExample/Images/11-clock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamma/PSStackedView/master/Example/StackedViewKitExample/Images/11-clock.png -------------------------------------------------------------------------------- /Example/StackedViewKitExample/Images/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamma/PSStackedView/master/Example/StackedViewKitExample/Images/background.png -------------------------------------------------------------------------------- /Example/StackedViewKitExample/Images/error/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamma/PSStackedView/master/Example/StackedViewKitExample/Images/error/error.png -------------------------------------------------------------------------------- /Example/StackedViewKitExample/Images/error/error.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamma/PSStackedView/master/Example/StackedViewKitExample/Images/error/error.psd -------------------------------------------------------------------------------- /Example/StackedViewKitExample/Images/error/error@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamma/PSStackedView/master/Example/StackedViewKitExample/Images/error/error@2x.png -------------------------------------------------------------------------------- /Example/StackedViewKitExample/Images/NewGlow/NewGlow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamma/PSStackedView/master/Example/StackedViewKitExample/Images/NewGlow/NewGlow.png -------------------------------------------------------------------------------- /Example/StackedViewKitExample/Images/NewGlow/NewGlow.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamma/PSStackedView/master/Example/StackedViewKitExample/Images/NewGlow/NewGlow.psd -------------------------------------------------------------------------------- /Example/StackedViewKitExample/Images/NewGlow/NewGlow@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamma/PSStackedView/master/Example/StackedViewKitExample/Images/NewGlow/NewGlow@2x.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | .svn 20 | 21 | PSStackedView/PSStackedViewController.m 22 | 23 | .gitignore 24 | 25 | .gitignore 26 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | /// viewController is still visible 30 | - (void)stackedView:(PSStackedViewController *)stackedView isStillVisibleViewController:(UIViewController *)viewController fullyVisible:(BOOL)fullyVisible; 31 | 32 | /// viewController is now visible 33 | - (void)stackedView:(PSStackedViewController *)stackedView isNowVisibleViewController:(UIViewController *)viewController; 34 | 35 | /// viewController is now hidden 36 | - (void)stackedView:(PSStackedViewController *)stackedView isNowHiddenViewController:(UIViewController *)viewController; 37 | 38 | @end 39 | -------------------------------------------------------------------------------- /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.navigationControllerSwizzled; 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 | -------------------------------------------------------------------------------- /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/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 | id __unsafe_unretained delegate_; 23 | UIViewController *rootViewController_; 24 | UIPanGestureRecognizer *panRecognizer_; 25 | 26 | // properites 27 | NSUInteger leftInset_; 28 | NSUInteger largeLeftInset_; 29 | BOOL reduceAnimations_; 30 | 31 | // stack state 32 | CGFloat floatIndex_; 33 | NSMutableArray *viewControllers_; 34 | 35 | // internal drag state handling and other messy details 36 | PSSVSnapOption lastDragOption_; 37 | BOOL snapBackFromLeft_; 38 | NSInteger lastDragOffset_; 39 | BOOL lastDragDividedOne_; 40 | NSInteger lastVisibleIndexBeforeRotation_; 41 | 42 | struct { 43 | unsigned int delegateWillInsertViewController:1; 44 | unsigned int delegateDidInsertViewController:1; 45 | unsigned int delegateWillRemoveViewController:1; 46 | unsigned int delegateDidRemoveViewController:1; 47 | unsigned int isStillVisibleViewController:1; 48 | unsigned int isNowVisibleViewController:1; 49 | unsigned int isNowHiddenViewController:1; 50 | }delegateFlags_; 51 | } 52 | 53 | /// the root controller gets the whole background view 54 | - (id)initWithRootViewController:(UIViewController *)rootViewController; 55 | 56 | /// Uses a horizontal slide transition. Has no effect if the view controller is already in the stack. 57 | /// baseViewController is used to remove subviews if a previous controller invokes a new view. can be nil. 58 | - (void)pushViewController:(UIViewController *)viewController fromViewController:(UIViewController *)baseViewController animated:(BOOL)animated; 59 | 60 | /// pushes the view controller, sets the last current vc as parent controller 61 | - (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated; 62 | 63 | /// remove top view controller from stack, return it 64 | - (UIViewController *)popViewControllerAnimated:(BOOL)animated; 65 | 66 | /// remove specific view controller. returns false if controller is not on top of stack 67 | - (BOOL)popViewController:(UIViewController *)controller animated:(BOOL)animated; 68 | 69 | /// remove view controllers until 'viewController' is found 70 | - (NSArray *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated; 71 | 72 | /// removes all view controller 73 | - (NSArray *)popToRootViewControllerAnimated:(BOOL)animated; 74 | 75 | /// return all controllers of certain class 76 | - (NSArray *)controllersForClass:(Class)theClass; 77 | 78 | /// can we collapse (= hide) view controllers? Only collapses until screen width is used 79 | - (NSUInteger)canCollapseStack; 80 | 81 | /// can the stack be further expanded (are some views stacked?) 82 | - (NSUInteger)canExpandStack; 83 | 84 | /// moves view controller stack to the left, potentially hiding older VCs (increases firstVisibleIndex) 85 | - (NSUInteger)collapseStack:(NSInteger)steps animated:(BOOL)animated; 86 | 87 | /// move view controller stack to the right, showing older VCs (decreases firstVisibleIndex) 88 | - (NSUInteger)expandStack:(NSInteger)steps animated:(BOOL)animated; 89 | 90 | /// align stack to nearest grid 91 | - (void)alignStackAnimated:(BOOL)animated; 92 | 93 | /// expands/collapses stack until entered index is topmost right 94 | - (void)displayViewControllerIndexOnRightMost:(NSInteger)index animated:(BOOL)animated; 95 | 96 | /// expands/collapses stack until entered controller is topmost right 97 | - (BOOL)displayViewControllerOnRightMost:(UIViewController *)vc animated:(BOOL)animated; 98 | 99 | /// return view controllers that follow a certain view controller. Helper function. 100 | - (NSArray *)viewControllersAfterViewController:(UIViewController *)viewController; 101 | 102 | /// index of current view controller. Supports search within UINavigationControllers. 103 | - (NSInteger)indexOfViewController:(UIViewController *)viewController; 104 | 105 | /// event delegate 106 | @property(nonatomic, unsafe_unretained) id delegate; 107 | 108 | /// root view controller, always displayed behind stack 109 | @property(nonatomic, strong, readonly) UIViewController *rootViewController; 110 | 111 | /// The top(last) view controller on the stack. 112 | @property(nonatomic, readonly, strong) UIViewController *topViewController; 113 | 114 | /// first view controller 115 | @property(nonatomic, readonly, strong) UIViewController *firstViewController; 116 | 117 | /// represents current state via floating point. shows edge attaches, menu docking, etc 118 | @property(nonatomic, readonly, assign) CGFloat floatIndex; 119 | 120 | /// view controllers visible. NOT KVO compliant, is calculated on demand. 121 | @property(nonatomic, readonly, strong) NSArray *visibleViewControllers; 122 | 123 | @property(nonatomic, readonly, strong) NSArray *fullyVisibleViewControllers; 124 | 125 | /// index of first currently visible view controller [calculated] 126 | @property(nonatomic, assign, readonly) NSInteger firstVisibleIndex; 127 | 128 | /// index of last currently visible view controller [calculated] 129 | @property(nonatomic, assign, readonly) NSInteger lastVisibleIndex; 130 | 131 | /// array of all current view controllers, sorted 132 | @property(nonatomic, strong, readonly) NSArray *viewControllers; 133 | 134 | /// pangesture recognizer used 135 | @property(nonatomic, strong) UIPanGestureRecognizer *panRecognizer; 136 | 137 | /// enable if you show another object in fullscreen, but stacked view still thinks it's displayed 138 | /// reduces animations to a minimum to get smoother reactions on frontmost view. 139 | @property(nonatomic, assign, getter=isReducingAnimations) BOOL reduceAnimations; 140 | 141 | /// left inset thats always visible. Defaults to 60. 142 | @property(nonatomic, assign) NSUInteger leftInset; 143 | /// animate setting of the left inset that is always visible 144 | - (void)setLeftInset:(NSUInteger)leftInset animated:(BOOL)animated; 145 | 146 | /// large left inset. is visible to show you the full menu width. Defaults to 200. 147 | @property(nonatomic, assign) NSUInteger largeLeftInset; 148 | /// animate setting of large left inset 149 | - (void)setLargeLeftInset:(NSUInteger)largeLeftInset animated:(BOOL)animated; 150 | 151 | // compatibility with UINavigationBar -- returns nil 152 | #ifdef ALLOW_SWIZZLING_NAVIGATIONCONTROLLER 153 | @property(nonatomic, assign) UINavigationBar *navigationBar; 154 | #endif 155 | 156 | @end 157 | -------------------------------------------------------------------------------- /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 | #ifndef kPSSVCornerRadius 14 | #define kPSSVCornerRadius 6.f 15 | #endif 16 | #ifndef kPSSVShadowWidth 17 | #define kPSSVShadowWidth 30.f 18 | #endif 19 | #ifndef kPSSVShadowAlpha 20 | #define kPSSVShadowAlpha 0.25f 21 | #endif 22 | 23 | @interface PSSVContainerView () 24 | @property(nonatomic, assign) CGFloat originalWidth; 25 | @property(nonatomic, strong) CAGradientLayer *leftShadowLayer; 26 | @property(nonatomic, strong) CAGradientLayer *innerShadowLayer; 27 | @property(nonatomic, strong) CAGradientLayer *rightShadowLayer; 28 | @property(nonatomic, strong) UIView *transparentView; 29 | @end 30 | 31 | @implementation PSSVContainerView 32 | 33 | @synthesize shadow = shadow_; 34 | @synthesize originalWidth = originalWidth_; 35 | @synthesize controller = controller_; 36 | @synthesize leftShadowLayer = leftShadowLayer_; 37 | @synthesize innerShadowLayer = innerShadowLayer_; 38 | @synthesize rightShadowLayer = rightShadowLayer_; 39 | @synthesize transparentView = transparentView_; 40 | 41 | /////////////////////////////////////////////////////////////////////////////////////////////////// 42 | #pragma mark - 43 | #pragma mark private 44 | 45 | // creates vertical shadow 46 | - (CAGradientLayer *)shadowAsInverse:(BOOL)inverse { 47 | CAGradientLayer *newShadow = [[CAGradientLayer alloc] init]; 48 | newShadow.startPoint = CGPointMake(0, 0.5); 49 | newShadow.endPoint = CGPointMake(1.0, 0.5); 50 | CGColorRef darkColor = (CGColorRef)CFRetain([UIColor colorWithWhite:0.0f alpha:kPSSVShadowAlpha].CGColor); 51 | CGColorRef lightColor = (CGColorRef)CFRetain([UIColor clearColor].CGColor); 52 | newShadow.colors = [NSArray arrayWithObjects: 53 | (__bridge id)(inverse ? lightColor : darkColor), 54 | (__bridge id)(inverse ? darkColor : lightColor), 55 | nil]; 56 | 57 | CFRelease(darkColor); 58 | CFRelease(lightColor); 59 | return newShadow; 60 | } 61 | 62 | // return available shadows as set, for easy enumeration 63 | - (NSSet *)shadowSet { 64 | NSMutableSet *set = [NSMutableSet set]; 65 | if (self.leftShadowLayer) { 66 | [set addObject:self.leftShadowLayer]; 67 | } 68 | if (self.innerShadowLayer) { 69 | [set addObject:self.innerShadowLayer]; 70 | } 71 | if (self.rightShadowLayer) { 72 | [set addObject:self.rightShadowLayer]; 73 | } 74 | return [set copy]; 75 | } 76 | 77 | /////////////////////////////////////////////////////////////////////////////////////////////////// 78 | #pragma mark - NSObject 79 | 80 | + (PSSVContainerView *)containerViewWithController:(UIViewController *)controller; { 81 | PSSVContainerView *view = [[PSSVContainerView alloc] initWithFrame:controller.view.frame]; 82 | view.controller = controller; 83 | return view; 84 | } 85 | 86 | - (void)dealloc { 87 | [self removeMask]; 88 | self.shadow = PSSVSideNone; // TODO needed? 89 | } 90 | 91 | /////////////////////////////////////////////////////////////////////////////////////////////////// 92 | #pragma mark - UIView 93 | 94 | - (void)setFrame:(CGRect)frame { 95 | [super setFrame:frame]; 96 | 97 | // adapt layer heights 98 | for (CALayer *layer in [self shadowSet]) { 99 | CGRect aFrame = layer.frame; 100 | aFrame.size.height = frame.size.height; 101 | layer.frame = aFrame; 102 | } 103 | } 104 | 105 | /////////////////////////////////////////////////////////////////////////////////////////////////// 106 | #pragma mark - Public 107 | 108 | - (CGFloat)limitToMaxWidth:(CGFloat)maxWidth; { 109 | BOOL widthChanged = NO; 110 | 111 | if (maxWidth && self.width > maxWidth) { 112 | self.width = maxWidth; 113 | widthChanged = YES; 114 | }else if(self.originalWidth && self.width < self.originalWidth) { 115 | self.width = MIN(maxWidth, self.originalWidth); 116 | widthChanged = YES; 117 | } 118 | self.controller.view.width = self.width; 119 | 120 | // update shadow layers for new width 121 | if (widthChanged) { 122 | [self updateContainer]; 123 | } 124 | 125 | return self.width; 126 | } 127 | 128 | - (void)setController:(UIViewController *)aController { 129 | if (controller_ != aController) { 130 | if (controller_) { 131 | [controller_.view removeFromSuperview]; 132 | } 133 | controller_ = aController; 134 | 135 | // properly embed view 136 | self.originalWidth = self.controller.view.width; 137 | controller_.view.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; 138 | controller_.view.frame = CGRectMake(0, 0, controller_.view.width, controller_.view.height); 139 | [self addSubview:controller_.view]; 140 | [self bringSubviewToFront:transparentView_]; 141 | } 142 | } 143 | 144 | - (void)addMaskToCorners:(UIRectCorner)corners; { 145 | return; 146 | // Re-calculate the size of the mask to account for adding/removing rows. 147 | CGRect frame = self.controller.view.bounds; 148 | if([self.controller.view isKindOfClass:[UIScrollView class]] && ((UIScrollView *)self.controller.view).contentSize.height > self.controller.view.frame.size.height) { 149 | frame.size = ((UIScrollView *)self.controller.view).contentSize; 150 | } else { 151 | frame.size = self.controller.view.frame.size; 152 | } 153 | 154 | // Create the path (with only the top-left corner rounded) 155 | UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:frame 156 | byRoundingCorners:corners 157 | cornerRadii:CGSizeMake(kPSSVCornerRadius, kPSSVCornerRadius)]; 158 | 159 | // Create the shape layer and set its path 160 | CAShapeLayer *maskLayer = [CAShapeLayer layer]; 161 | maskLayer.frame = frame; 162 | maskLayer.path = maskPath.CGPath; 163 | 164 | // Set the newly created shape layer as the mask for the image view's layer 165 | self.controller.view.layer.mask = maskLayer; 166 | } 167 | 168 | - (void)removeMask; { 169 | self.controller.view.layer.mask = nil; 170 | } 171 | 172 | - (void)updateContainer { 173 | // re-set shadow property 174 | self.shadow = shadow_; 175 | } 176 | 177 | - (void)setShadow:(PSSVSide)shadow { 178 | shadow_ = shadow; 179 | 180 | if (shadow & PSSVSideLeft) { 181 | if (!self.leftShadowLayer) { 182 | CAGradientLayer *leftShadow = [self shadowAsInverse:YES]; 183 | self.leftShadowLayer = leftShadow; 184 | } 185 | self.leftShadowLayer.frame = CGRectMake(-kPSSVShadowWidth, 0, kPSSVShadowWidth+kPSSVCornerRadius, self.controller.view.height);; 186 | if ([self.layer.sublayers indexOfObjectIdenticalTo:self.leftShadowLayer] != 0) { 187 | [self.layer insertSublayer:self.leftShadowLayer atIndex:0]; 188 | } 189 | }else { 190 | [self.leftShadowLayer removeFromSuperlayer]; 191 | } 192 | 193 | if (shadow & PSSVSideRight) { 194 | if (!self.rightShadowLayer) { 195 | CAGradientLayer *rightShadow = [self shadowAsInverse:NO]; 196 | self.rightShadowLayer = rightShadow; 197 | } 198 | self.rightShadowLayer.frame = CGRectMake(self.width-kPSSVCornerRadius, 0, kPSSVShadowWidth, self.controller.view.height); 199 | if ([self.layer.sublayers indexOfObjectIdenticalTo:self.rightShadowLayer] != 0) { 200 | [self.layer insertSublayer:self.rightShadowLayer atIndex:0]; 201 | } 202 | }else { 203 | [self.rightShadowLayer removeFromSuperlayer]; 204 | } 205 | 206 | if (shadow) { 207 | if (!self.innerShadowLayer) { 208 | CAGradientLayer *innerShadow = [[CAGradientLayer alloc] init]; 209 | innerShadow.colors = [NSArray arrayWithObjects:(id)[UIColor colorWithWhite:0.0f alpha:kPSSVShadowAlpha].CGColor, (id)[UIColor colorWithWhite:0.0f alpha:kPSSVShadowAlpha].CGColor, nil]; 210 | self.innerShadowLayer = innerShadow; 211 | } 212 | self.innerShadowLayer.frame = CGRectMake(kPSSVCornerRadius, 0, self.width-kPSSVCornerRadius*2, self.controller.view.height); 213 | if ([self.layer.sublayers indexOfObjectIdenticalTo:self.innerShadowLayer] != 0) { 214 | [self.layer insertSublayer:self.innerShadowLayer atIndex:0]; 215 | } 216 | }else { 217 | [self.innerShadowLayer removeFromSuperlayer]; 218 | } 219 | } 220 | 221 | - (void)setDarkRatio:(CGFloat)darkRatio { 222 | BOOL isTransparent = darkRatio > 0.01f; 223 | 224 | if (isTransparent && !transparentView_) { 225 | transparentView_ = [[UIView alloc] initWithFrame:CGRectMake(0.f, 0.f, self.width, self.height)]; 226 | transparentView_.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 227 | transparentView_.backgroundColor = [UIColor blackColor]; 228 | transparentView_.alpha = 0.f; 229 | transparentView_.userInteractionEnabled = NO; 230 | [self addSubview:transparentView_]; 231 | } 232 | 233 | transparentView_.alpha = darkRatio; 234 | } 235 | 236 | - (CGFloat)darkRatio { 237 | return transparentView_.alpha; 238 | } 239 | 240 | @end 241 | -------------------------------------------------------------------------------- /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.25f 17 | #define kPSSVStackAnimationPushDuration kPSSVStackAnimationSpeedModifier * 0.25f 18 | #define kPSSVStackAnimationPopDuration kPSSVStackAnimationSpeedModifier * 0.25f 19 | #define kPSSVMaxSnapOverOffset 7 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 | @property(nonatomic, strong) UIViewController *rootViewController; 31 | @property(nonatomic, strong) NSMutableArray *viewControllers; 32 | @property(nonatomic, assign) NSInteger firstVisibleIndex; 33 | @property(nonatomic, assign) CGFloat floatIndex; 34 | - (UIViewController *)overlappedViewController; 35 | - (void)handleVisibleChangeFromInitionalViewControllers:(NSArray *)initialControllers toFinalViewControllers:(NSArray *)finalViewControllers; 36 | @end 37 | 38 | @implementation PSStackedViewController 39 | 40 | @synthesize leftInset = leftInset_; 41 | @synthesize largeLeftInset = largeLeftInset_; 42 | @synthesize viewControllers = viewControllers_; 43 | @synthesize floatIndex = floatIndex_; 44 | @synthesize rootViewController = rootViewController_; 45 | @synthesize panRecognizer = panRecognizer_; 46 | @synthesize delegate = delegate_; 47 | @synthesize reduceAnimations = reduceAnimations_; 48 | @dynamic firstVisibleIndex; 49 | 50 | #ifdef ALLOW_SWIZZLING_NAVIGATIONCONTROLLER 51 | @synthesize navigationBar; 52 | #endif 53 | 54 | /////////////////////////////////////////////////////////////////////////////////////////////////// 55 | #pragma mark - NSObject 56 | 57 | - (id)initWithRootViewController:(UIViewController *)rootViewController; { 58 | if ((self = [super init])) { 59 | rootViewController_ = rootViewController; 60 | viewControllers_ = [[NSMutableArray alloc] init]; 61 | 62 | // set some reasonble defaults 63 | leftInset_ = 60; 64 | largeLeftInset_ = 200; 65 | 66 | // add a gesture recognizer to detect dragging to the guest controllers 67 | UIPanGestureRecognizer *panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanFrom:)]; 68 | [panRecognizer setMaximumNumberOfTouches:1]; 69 | [panRecognizer setDelaysTouchesBegan:NO]; 70 | [panRecognizer setDelaysTouchesEnded:YES]; 71 | [panRecognizer setCancelsTouchesInView:YES]; 72 | panRecognizer.delegate = self; 73 | [self.view addGestureRecognizer:panRecognizer]; 74 | self.panRecognizer = panRecognizer; 75 | 76 | 77 | 78 | #ifdef ALLOW_SWIZZLING_NAVIGATIONCONTROLLER 79 | PSSVLog("Swizzling UIViewController.navigationController"); 80 | Method origMethod = class_getInstanceMethod([UIViewController class], @selector(navigationController)); 81 | Method overrideMethod = class_getInstanceMethod([UIViewController class], @selector(navigationControllerSwizzled)); 82 | method_exchangeImplementations(origMethod, overrideMethod); 83 | #endif 84 | } 85 | return self; 86 | } 87 | 88 | #ifdef ALLOW_SWIZZLING_NAVIGATIONCONTROLLER 89 | - (void)setNavigationBarHidden:(BOOL)hidden animated:(BOOL)animated 90 | { 91 | 92 | } 93 | #endif 94 | 95 | - (void)dealloc { 96 | delegate_ = nil; 97 | panRecognizer_.delegate = nil; 98 | // remove all view controllers the hard way (w/o calling delegate) 99 | while ([self.viewControllers count]) { 100 | [self popViewControllerAnimated:NO]; 101 | } 102 | 103 | } 104 | 105 | /////////////////////////////////////////////////////////////////////////////////////////////////// 106 | #pragma mark - Delegate 107 | 108 | - (void)setDelegate:(id)delegate { 109 | if (delegate != delegate_) { 110 | delegate_ = delegate; 111 | 112 | delegateFlags_.delegateWillInsertViewController = [delegate respondsToSelector:@selector(stackedView:willInsertViewController:)]; 113 | delegateFlags_.delegateDidInsertViewController = [delegate respondsToSelector:@selector(stackedView:didInsertViewController:)]; 114 | delegateFlags_.delegateWillRemoveViewController = [delegate respondsToSelector:@selector(stackedView:willRemoveViewController:)]; 115 | delegateFlags_.delegateDidRemoveViewController = [delegate respondsToSelector:@selector(stackedView:didRemoveViewController:)]; 116 | delegateFlags_.isStillVisibleViewController = [delegate respondsToSelector:@selector(stackedView:isStillVisibleViewController:fullyVisible:)]; 117 | delegateFlags_.isNowVisibleViewController = [delegate respondsToSelector:@selector(stackedView:isNowVisibleViewController:)]; 118 | delegateFlags_.isNowHiddenViewController = [delegate respondsToSelector:@selector(stackedView:isNowHiddenViewController:)]; 119 | } 120 | } 121 | 122 | - (void)delegateWillInsertViewController:(UIViewController *)viewController { 123 | if (delegateFlags_.delegateWillInsertViewController) { 124 | [self.delegate stackedView:self willInsertViewController:viewController]; 125 | } 126 | } 127 | 128 | - (void)delegateDidInsertViewController:(UIViewController *)viewController { 129 | if (delegateFlags_.delegateDidInsertViewController) { 130 | [self.delegate stackedView:self didInsertViewController:viewController]; 131 | } 132 | } 133 | 134 | - (void)delegateWillRemoveViewController:(UIViewController *)viewController { 135 | if (delegateFlags_.delegateWillRemoveViewController) { 136 | [self.delegate stackedView:self willRemoveViewController:viewController]; 137 | } 138 | } 139 | 140 | - (void)delegateDidRemoveViewController:(UIViewController *)viewController { 141 | if (delegateFlags_.delegateDidRemoveViewController) { 142 | [self.delegate stackedView:self didRemoveViewController:viewController]; 143 | } 144 | } 145 | 146 | - (void)delegateIsStillVisibleViewController:(UIViewController *)viewController fullyVisible:(BOOL)fullyVisible { 147 | if (delegateFlags_.isStillVisibleViewController) { 148 | [self.delegate stackedView:self isStillVisibleViewController:viewController fullyVisible:fullyVisible]; 149 | } 150 | } 151 | 152 | - (void)delegateIsNowVisibleViewController:(UIViewController *)viewController { 153 | if (delegateFlags_.isNowVisibleViewController) { 154 | [self.delegate stackedView:self isNowVisibleViewController:viewController]; 155 | } 156 | } 157 | 158 | - (void)delegateIsNowHiddenViewController:(UIViewController *)viewController { 159 | if (delegateFlags_.isNowHiddenViewController) { 160 | [self.delegate stackedView:self isNowHiddenViewController:viewController]; 161 | } 162 | } 163 | 164 | /////////////////////////////////////////////////////////////////////////////////////////////////// 165 | #pragma mark - Private Helpers 166 | 167 | - (NSInteger)firstVisibleIndex { 168 | NSInteger firstVisibleIndex = floorf(self.floatIndex); 169 | return firstVisibleIndex; 170 | } 171 | 172 | - (CGRect)viewRect { 173 | // self.view.frame not used, it's wrong in viewWillAppear 174 | CGRect viewRect = [[UIScreen mainScreen] applicationFrame]; 175 | return viewRect; 176 | } 177 | 178 | // return screen width 179 | - (CGFloat)screenWidth { 180 | CGRect viewRect = [self viewRect]; 181 | CGFloat screenWidth = PSIsLandscape() ? viewRect.size.height : viewRect.size.width; 182 | return screenWidth; 183 | } 184 | 185 | - (CGFloat)screenHeight { 186 | CGRect viewRect = [self viewRect]; 187 | NSUInteger screenHeight = PSIsLandscape() ? viewRect.size.width : viewRect.size.height; 188 | return screenHeight; 189 | } 190 | 191 | - (CGFloat)maxControllerWidth { 192 | CGFloat maxWidth = [self screenWidth] - self.leftInset; 193 | return maxWidth; 194 | } 195 | 196 | // total stack width if completely expanded 197 | - (NSUInteger)totalStackWidth { 198 | NSUInteger totalStackWidth = 0; 199 | for (UIViewController *controller in self.viewControllers) { 200 | totalStackWidth += controller.containerView.width; 201 | } 202 | return totalStackWidth; 203 | } 204 | 205 | // menu is only collapsable if stack is large enough 206 | - (BOOL)isMenuCollapsable { 207 | BOOL isMenuCollapsable = [self totalStackWidth] + self.largeLeftInset > [self screenWidth]; 208 | return isMenuCollapsable; 209 | } 210 | 211 | // return current left border (how it *should* be) 212 | - (NSUInteger)currentLeftInset { 213 | return self.floatIndex >= 0.5 ? self.leftInset : self.largeLeftInset; 214 | } 215 | 216 | // minimal left border is depending on amount of VCs 217 | - (NSUInteger)minimalLeftInset { 218 | return [self isMenuCollapsable] ? self.leftInset : self.largeLeftInset; 219 | } 220 | 221 | // check if a view controller is visible or not 222 | - (BOOL)isViewControllerVisible:(UIViewController *)viewController completely:(BOOL)completely { 223 | NSParameterAssert(viewController); 224 | NSUInteger screenWidth = [self screenWidth]; 225 | 226 | BOOL isVCVisible = ((viewController.containerView.left < screenWidth && !completely) || 227 | (completely && viewController.containerView.right <= screenWidth)); 228 | return isVCVisible; 229 | } 230 | 231 | // returns view controller that is displayed before viewController 232 | - (UIViewController *)previousViewController:(UIViewController *)viewController { 233 | if(!viewController) // don't assert on mere menu events 234 | return nil; 235 | 236 | NSUInteger vcIndex = [self indexOfViewController:viewController]; 237 | UIViewController *prevVC = nil; 238 | if (vcIndex > 0) { 239 | prevVC = [self.viewControllers objectAtIndex:vcIndex-1]; 240 | } 241 | 242 | return prevVC; 243 | } 244 | 245 | // returns view controller that is displayed after viewController 246 | - (UIViewController *)nextViewController:(UIViewController *)viewController { 247 | NSParameterAssert(viewController); 248 | 249 | NSUInteger vcIndex = [self indexOfViewController:viewController]; 250 | UIViewController *nextVC = nil; 251 | if (vcIndex + 1 < [self.viewControllers count]) { 252 | nextVC = [self.viewControllers objectAtIndex:vcIndex+1]; 253 | } 254 | 255 | return nextVC; 256 | } 257 | 258 | // returns last visible view controller. this *can* be the last view controller in the stack, 259 | // but also one of the previous ones if the user navigates back in the stack 260 | - (UIViewController *)lastVisibleViewControllerCompletelyVisible:(BOOL)completely { 261 | __block UIViewController *lastVisibleViewController = nil; 262 | 263 | [self.viewControllers enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 264 | UIViewController *currentViewController = (UIViewController *)obj; 265 | if ([self isViewControllerVisible:currentViewController completely:completely]) { 266 | lastVisibleViewController = currentViewController; 267 | *stop = YES; 268 | } 269 | }]; 270 | 271 | return lastVisibleViewController; 272 | } 273 | 274 | // returns true if firstVisibleIndex is the last available index. 275 | - (BOOL)isLastIndex { 276 | BOOL isLastIndex = self.firstVisibleIndex == [self.viewControllers count] - 1; 277 | return isLastIndex; 278 | } 279 | 280 | enum { 281 | PSSVRoundNearest, 282 | PSSVRoundUp, 283 | PSSVRoundDown 284 | }typedef PSSVRoundOption; 285 | 286 | - (BOOL)isFloatIndexBetween:(CGFloat)floatIndex { 287 | CGFloat intIndex, restIndex; 288 | restIndex = modff(floatIndex, &intIndex); 289 | BOOL isBetween = fabsf(restIndex - 0.5f) < EPSILON; 290 | return isBetween; 291 | } 292 | 293 | // check if index is valid. Valid indexes are >= 0.0 and only full or .5 parts are allowed. 294 | // there are lots of other, more complex rules, so calculate! 295 | - (BOOL)isValidFloatIndex:(CGFloat)floatIndex { 296 | BOOL isValid = floatIndex == 0.f; // 0.f is always allowed 297 | if (!isValid) { 298 | CGFloat contentWidth = [self totalStackWidth]; 299 | if (floatIndex == 0.5f) { 300 | // docking to menu is only allowed if content > available size. 301 | isValid = contentWidth > [self screenWidth] - self.largeLeftInset; 302 | }else { 303 | NSUInteger stackCount = [self.viewControllers count]; 304 | CGFloat intIndex, restIndex; 305 | restIndex = modff(floatIndex, &intIndex); // split e.g. 1.5 in 1.0 and 0.5 306 | isValid = stackCount > intIndex && contentWidth > ([self screenWidth] - self.leftInset); 307 | if (isValid && fabsf(restIndex - 0.5f) < EPSILON) { // comparing floats -> if so, we have a .5 here 308 | if (ceilf(floatIndex) < stackCount) { // at the end? 309 | CGFloat widthLeft = [[self.viewControllers objectAtIndex:floorf(floatIndex)] containerView].width; 310 | CGFloat widthRight = [[self.viewControllers objectAtIndex:ceilf(floatIndex)] containerView].width; 311 | isValid = (widthLeft + widthRight) > ([self screenWidth] - self.leftInset); 312 | }else { 313 | isValid = NO; 314 | } 315 | } 316 | } 317 | } 318 | return isValid; 319 | } 320 | 321 | - (CGFloat)nearestValidFloatIndex:(CGFloat)floatIndex round:(PSSVRoundOption)roundOption { 322 | CGFloat roundedFloat; 323 | CGFloat intIndex, restIndex; 324 | restIndex = modff(floatIndex, &intIndex); 325 | 326 | if (restIndex < 0.5f) { 327 | if (roundOption == PSSVRoundNearest) { 328 | restIndex = (restIndex < 0.25f) ? 0.f : 0.5f; 329 | }else { 330 | restIndex = (roundOption == PSSVRoundUp) ? 0.5f : 0.f; 331 | } 332 | }else { 333 | if (roundOption == PSSVRoundNearest) { 334 | restIndex = (restIndex < 0.75f) ? 0.5f : 1.f; 335 | }else { 336 | restIndex = (roundOption == PSSVRoundUp) ? 1.f : 0.5f; 337 | } 338 | } 339 | roundedFloat = intIndex + restIndex; 340 | 341 | // now check if this is valid 342 | BOOL isValid = [self isValidFloatIndex:roundedFloat]; 343 | 344 | // if not valid, and custom rounding produced a .5, test again with full rounding 345 | if (!isValid && restIndex == 0.5f) { 346 | CGFloat naturalRoundedIndex; 347 | if (roundOption == PSSVRoundNearest) { 348 | naturalRoundedIndex = roundf(floatIndex); 349 | }else if(roundOption == PSSVRoundUp) { 350 | naturalRoundedIndex = ceilf(floatIndex); 351 | }else { 352 | naturalRoundedIndex = floorf(floatIndex); 353 | } 354 | // if that works out, return it! 355 | if ([self isValidFloatIndex:naturalRoundedIndex]) { 356 | isValid = YES; 357 | roundedFloat = naturalRoundedIndex; 358 | } 359 | } 360 | 361 | // still not valid? start the for loops, find nearest valid index 362 | if (!isValid) { 363 | CGFloat validLowIndex = 0.f, validHighIndex = 0.f; 364 | 365 | // upper bound 366 | CGFloat viewControllerCount = [self.viewControllers count]; 367 | for (CGFloat tester = roundedFloat + 0.5f; tester < viewControllerCount; tester += 0.5f) { 368 | if ([self isValidFloatIndex:tester]) { 369 | validHighIndex = tester; 370 | break; 371 | } 372 | } 373 | // lower bound 374 | for (CGFloat tester = roundedFloat - 0.5f; tester >= 0.f; tester -= 0.5f) { 375 | if ([self isValidFloatIndex:tester]) { 376 | validLowIndex = tester; 377 | break; 378 | } 379 | } 380 | 381 | if (fabsf(validLowIndex - roundedFloat) < fabsf(validHighIndex - roundedFloat)) { 382 | roundedFloat = validLowIndex; 383 | }else { 384 | roundedFloat = validHighIndex; 385 | } 386 | } 387 | 388 | return roundedFloat; 389 | } 390 | 391 | - (CGFloat)nearestValidFloatIndex:(CGFloat)floatIndex { 392 | return [self nearestValidFloatIndex:floatIndex round:PSSVRoundNearest]; 393 | } 394 | 395 | - (CGFloat)nextFloatIndex:(CGFloat)floatIndex { 396 | CGFloat nextFloat = 0.f; 397 | CGFloat roundedFloat = [self nearestValidFloatIndex:floatIndex]; 398 | CGFloat viewControllerCount = [self.viewControllers count]; 399 | for (CGFloat tester = roundedFloat + 0.5f; tester < viewControllerCount; tester += 0.5f) { 400 | if ([self isValidFloatIndex:tester]) { 401 | nextFloat = tester; 402 | break; 403 | } 404 | } 405 | return nextFloat; 406 | } 407 | 408 | - (CGFloat)prevFloatIndex:(CGFloat)floatIndex { 409 | CGFloat prevFloat = 0.f; 410 | CGFloat roundedFloat = [self nearestValidFloatIndex:floatIndex]; 411 | for (CGFloat tester = roundedFloat - 0.5f; tester >= 0.f; tester -= 0.5f) { 412 | if ([self isValidFloatIndex:tester]) { 413 | prevFloat = tester; 414 | break; 415 | } 416 | } 417 | return prevFloat; 418 | } 419 | 420 | /// calculates all rects for current visibleIndex orientation 421 | - (NSArray *)rectsForControllers { 422 | NSMutableArray *frames = [NSMutableArray array]; 423 | 424 | // TODO: currently calculates *all* objects, should cache! 425 | CGFloat floatIndex = [self nearestValidFloatIndex:self.floatIndex]; 426 | [self.viewControllers enumerateObjectsWithOptions:0 usingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 427 | UIViewController *currentVC = (UIViewController *)obj; 428 | CGFloat leftPos = [self currentLeftInset]; 429 | CGRect leftRect = idx > 0 ? [[frames objectAtIndex:idx-1] CGRectValue] : CGRectZero; 430 | 431 | if (idx == floorf(floatIndex)) { 432 | BOOL dockRight = ![self isFloatIndexBetween:floatIndex] && floatIndex >= 1.f; 433 | 434 | // should we pan it to the right? 435 | if (dockRight) { 436 | leftPos = [self screenWidth] - currentVC.containerView.width; 437 | } 438 | }else if (idx > floatIndex) { 439 | // connect vc to left vc's right! 440 | leftPos = leftRect.origin.x + leftRect.size.width; 441 | } 442 | 443 | CGRect currentRect = CGRectMake(leftPos, currentVC.containerView.top, currentVC.containerView.width, currentVC.containerView.height); 444 | [frames addObject:[NSValue valueWithCGRect:currentRect]]; 445 | }]; 446 | 447 | return frames; 448 | } 449 | 450 | /// calculates the specific rect 451 | - (CGRect)rectForControllerAtIndex:(NSUInteger)index { 452 | NSArray *frames = [self rectsForControllers]; 453 | return [[frames objectAtIndex:index] CGRectValue]; 454 | } 455 | 456 | 457 | /// moves a rect around, recalculates following rects 458 | - (NSArray *)modifiedRects:(NSArray *)frames newLeft:(CGFloat)newLeft index:(NSUInteger)index { 459 | NSMutableArray *modifiedFrames = [NSMutableArray arrayWithArray:frames]; 460 | 461 | CGRect prevFrame; 462 | for (int i = index; i < [modifiedFrames count]; i++) { 463 | CGRect vcFrame = [[modifiedFrames objectAtIndex:i] CGRectValue]; 464 | if (i == index) { 465 | vcFrame.origin.x = newLeft; 466 | }else { 467 | vcFrame.origin.x = prevFrame.origin.x + prevFrame.size.width; 468 | } 469 | [modifiedFrames replaceObjectAtIndex:i withObject:[NSValue valueWithCGRect:vcFrame]]; 470 | prevFrame = vcFrame; 471 | } 472 | 473 | return modifiedFrames; 474 | } 475 | 476 | // at some point, dragging does not make any more sense 477 | - (BOOL)snapPointAvailableAfterOffset:(NSInteger)offset { 478 | BOOL snapPointAvailableAfterOffset = YES; 479 | NSUInteger screenWidth = [self screenWidth]; 480 | NSUInteger totalWidth = [self totalStackWidth]; 481 | NSUInteger minCommonWidth = MIN(screenWidth, totalWidth); 482 | // NSArray *frames = [self rectsForControllers]; 483 | 484 | // are we at the end? 485 | UIViewController *topViewController = [self topViewController]; 486 | if (topViewController == [self lastVisibleViewControllerCompletelyVisible:YES]) { 487 | if (minCommonWidth + [self minimalLeftInset] <= topViewController.containerView.right) { 488 | snapPointAvailableAfterOffset = NO; 489 | } 490 | } 491 | 492 | // slow down first controller when dragged to the right 493 | if ([self canCollapseStack] == 0) { 494 | snapPointAvailableAfterOffset = NO; 495 | } 496 | 497 | if ([self firstViewController].containerView.left > self.largeLeftInset) { 498 | snapPointAvailableAfterOffset = NO; 499 | } 500 | 501 | return snapPointAvailableAfterOffset; 502 | } 503 | 504 | - (BOOL)displayViewControllerOnRightMost:(UIViewController *)vc animated:(BOOL)animated { 505 | NSUInteger index = [self indexOfViewController:vc]; 506 | if (index != NSNotFound) { 507 | [self displayViewControllerIndexOnRightMost:index animated:animated]; 508 | return YES; 509 | } 510 | return NO; 511 | } 512 | 513 | // ensures index is on rightmost position 514 | - (void)displayViewControllerIndexOnRightMost:(NSInteger)index animated:(BOOL)animated; { 515 | // add epsilon to round indexes like 1.0 to 2.0, also -1.0 to -2.0 516 | CGFloat floatIndexOffset = index - self.floatIndex; 517 | NSInteger indexOffset = ceilf(floatIndexOffset + (floatIndexOffset > 0 ? EPSILON : -EPSILON)); 518 | if (indexOffset > 0) { 519 | [self collapseStack:indexOffset animated:animated]; 520 | }else if(indexOffset < 0) { 521 | [self expandStack:indexOffset animated:animated]; 522 | } 523 | 524 | // hide menu, if first VC is larger than available screen space with floatIndex = 0.0 525 | else if (index == 0 && [self.viewControllers count] && [[self.viewControllers objectAtIndex:0] containerView].width >= ([self screenWidth] - self.leftInset)) { 526 | self.floatIndex = 0.5f; 527 | [self alignStackAnimated:YES]; 528 | } 529 | 530 | // Upgrade visiblity 531 | [self handleVisibleChangeFromInitionalViewControllers:[self visibleViewControllers] toFinalViewControllers:[self visibleViewControllers]]; 532 | } 533 | 534 | // iterates controllers and sets width (also, enlarges if requested width is larger than current width) 535 | - (void)updateViewControllerSizes { 536 | CGFloat maxControllerView = [self maxControllerWidth]; 537 | for (UIViewController *controller in self.viewControllers) { 538 | [controller.containerView limitToMaxWidth:maxControllerView]; 539 | } 540 | } 541 | 542 | - (CGFloat)overlapRatio { 543 | CGFloat overlapRatio = 0.f; 544 | 545 | UIViewController *overlappedVC = [self overlappedViewController]; 546 | if (overlappedVC) { 547 | UIViewController *rightVC = [self nextViewController:overlappedVC]; 548 | overlapRatio = fabsf(overlappedVC.containerView.right - rightVC.containerView.left)/overlappedVC.containerView.width; 549 | PSSVLog(@"overlapping %@ with %@ having ratio %.1f", NSStringFromCGRect(overlappedVC.containerView.frame), NSStringFromCGRect(rightVC.containerView.frame), overlapRatio); 550 | } 551 | return overlapRatio; 552 | } 553 | 554 | // updates view containers 555 | - (void)updateViewControllerMasksAndShadow { 556 | // only one! 557 | if ([self.viewControllers count] == 1) { 558 | [[self firstViewController].containerView addMaskToCorners:UIRectCornerAllCorners]; 559 | self.firstViewController.containerView.shadow = PSSVSideLeft | PSSVSideRight; 560 | }else { 561 | // rounded corners on first and last controller 562 | [self.viewControllers enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 563 | UIViewController *vc = (UIViewController *)obj; 564 | if (idx == 0) { 565 | [vc.containerView addMaskToCorners:UIRectCornerBottomLeft | UIRectCornerTopLeft]; 566 | }else if(idx == [self.viewControllers count]-1) { 567 | [vc.containerView addMaskToCorners:UIRectCornerBottomRight | UIRectCornerTopRight]; 568 | vc.containerView.shadow = PSSVSideLeft | PSSVSideRight; 569 | }else { 570 | [vc.containerView removeMask]; 571 | vc.containerView.shadow = PSSVSideLeft | PSSVSideRight; 572 | } 573 | }]; 574 | } 575 | 576 | // update alpha mask 577 | CGFloat overlapRatio = [self overlapRatio]; 578 | UIViewController *overlappedVC = [self overlappedViewController]; 579 | overlappedVC.containerView.darkRatio = MIN(overlapRatio, 1.f)/kAlphaReductRatio; 580 | 581 | // reset alpha ratio everywhere else 582 | for (UIViewController *vc in self.viewControllers) { 583 | if (vc != overlappedVC) { 584 | vc.containerView.darkRatio = 0.0f; 585 | } 586 | } 587 | } 588 | 589 | - (NSArray *)visibleViewControllersSetFullyVisible:(BOOL)fullyVisible; { 590 | NSMutableArray *array = [NSMutableArray array]; 591 | [self.viewControllers enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 592 | if ([self isViewControllerVisible:obj completely:fullyVisible]) { 593 | [array addObject:obj]; 594 | } 595 | }]; 596 | 597 | return [array copy]; 598 | } 599 | 600 | 601 | // check if there is any overlapping going on between VCs 602 | - (BOOL)isViewController:(UIViewController *)leftViewController overlappingWith:(UIViewController *)rightViewController { 603 | NSParameterAssert(leftViewController); 604 | NSParameterAssert(rightViewController); 605 | 606 | // figure out which controller is the top one 607 | if ([self indexOfViewController:rightViewController] < [self indexOfViewController:leftViewController]) { 608 | PSSVLog(@"overlapping check flipped! fixing that..."); 609 | UIViewController *tmp = rightViewController; 610 | rightViewController = leftViewController; 611 | leftViewController = tmp; 612 | } 613 | 614 | BOOL overlapping = leftViewController.containerView.right > rightViewController.containerView.left; 615 | if (overlapping) { 616 | PSSVLog(@"overlap detected: %@ (%@) with %@ (%@)", leftViewController, NSStringFromCGRect(leftViewController.containerView.frame), rightViewController, NSStringFromCGRect(rightViewController.containerView.frame)); 617 | } 618 | return overlapping; 619 | } 620 | 621 | // find the rightmost overlapping controller 622 | - (UIViewController *)overlappedViewController { 623 | __block UIViewController *overlappedViewController = nil; 624 | 625 | [self.viewControllers enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 626 | UIViewController *currentViewController = (UIViewController *)obj; 627 | UIViewController *leftViewController = [self previousViewController:currentViewController]; 628 | 629 | BOOL overlapping = NO; 630 | if (leftViewController && currentViewController) { 631 | overlapping = [self isViewController:leftViewController overlappingWith:currentViewController]; 632 | } 633 | 634 | if (overlapping) { 635 | overlappedViewController = leftViewController; 636 | *stop = YES; 637 | } 638 | }]; 639 | 640 | return overlappedViewController; 641 | } 642 | 643 | - (void)handleVisibleChangeFromInitionalViewControllers:(NSArray *)initialControllers toFinalViewControllers:(NSArray *)finalViewControllers 644 | { 645 | PSSVLog(@"Check ViewController Amount was: %d / is now: %d", [initialControllers count], [finalViewControllers count] ); 646 | NSMutableArray *mutableFinalViewControllers = [finalViewControllers mutableCopy]; 647 | 648 | // Enumerate all initial controllers and check if one is missing. If so, let the delegate know 649 | [initialControllers enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop){ 650 | if ( ![obj isKindOfClass:[UIViewController class]] ) 651 | { 652 | // Sanity 653 | return; 654 | } 655 | if ( ![mutableFinalViewControllers containsObject:obj] ) 656 | { 657 | [self delegateIsNowHiddenViewController:(UIViewController *)obj]; 658 | } else 659 | { 660 | if ( [self isViewControllerVisible:(UIViewController *)obj completely:NO] ) 661 | { 662 | UIViewController *nextViewController = [self nextViewController:obj]; 663 | BOOL fullyVisible = [self isViewControllerVisible:obj completely:YES] && (nextViewController == nil || (nextViewController != nil && ![self isViewController:obj overlappingWith:nextViewController])); 664 | [self delegateIsStillVisibleViewController:(UIViewController *)obj fullyVisible:fullyVisible]; 665 | } 666 | [mutableFinalViewControllers removeObject:obj]; 667 | } 668 | }]; 669 | 670 | // Enumerate all remaining controllers and check if there are new ones. If so, let the delegate know 671 | [mutableFinalViewControllers enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop){ 672 | if ( ![obj isKindOfClass:[UIViewController class]] ) 673 | { 674 | // Sanity 675 | return; 676 | } 677 | [self delegateIsNowVisibleViewController:(UIViewController *)obj]; 678 | }]; 679 | } 680 | 681 | /////////////////////////////////////////////////////////////////////////////////////////////////// 682 | #pragma mark - Touch Handling 683 | 684 | - (void)stopStackAnimation { 685 | // remove all current animations 686 | //[self.view.layer removeAllAnimations]; 687 | [self.viewControllers enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 688 | UIViewController *vc = (UIViewController *)obj; 689 | [vc.containerView.layer removeAllAnimations]; 690 | }]; 691 | } 692 | 693 | // moves the stack to a specific offset. 694 | - (void)moveStackWithOffset:(NSInteger)offset animated:(BOOL)animated userDragging:(BOOL)userDragging { 695 | PSSVLog(@"moving stack on %d pixels (animated:%d, decellerating:%d)", offset, animated, userDragging); 696 | 697 | [self stopStackAnimation]; 698 | [UIView animateWithDuration:animated ? kPSSVStackAnimationDuration : 0.f delay:0.f options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction animations:^{ 699 | 700 | // enumerate controllers from right to left 701 | // scroll each controller until we begin to overlap! 702 | __block BOOL isTopViewController = YES; 703 | [self.viewControllers enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 704 | UIViewController *currentViewController = (UIViewController *)obj; 705 | UIViewController *leftViewController = [self previousViewController:currentViewController]; 706 | UIViewController *rightViewController = [self nextViewController:currentViewController]; 707 | NSInteger minimalLeftInset = [self minimalLeftInset]; 708 | 709 | // we just move the top view controller 710 | NSInteger currentVCLeftPosition = currentViewController.containerView.left; 711 | if (isTopViewController) { 712 | currentVCLeftPosition += offset; 713 | }else { 714 | // make sure we're connected to the next controller! 715 | currentVCLeftPosition = rightViewController.containerView.left - currentViewController.containerView.width; 716 | } 717 | 718 | // prevent scrolling < minimal width (except for the top view controller - allow stupidness!) 719 | if (currentVCLeftPosition < minimalLeftInset && (!userDragging || (userDragging && !isTopViewController))) { 720 | currentVCLeftPosition = minimalLeftInset; 721 | } 722 | 723 | // a previous view controller is not allowed to overlap the next view controller. 724 | if (leftViewController && leftViewController.containerView.right > currentVCLeftPosition) { 725 | NSInteger leftVCLeftPosition = currentVCLeftPosition - leftViewController.containerView.width; 726 | if (leftVCLeftPosition < minimalLeftInset) { 727 | leftVCLeftPosition = minimalLeftInset; 728 | } 729 | leftViewController.containerView.left = leftVCLeftPosition; 730 | } 731 | 732 | // Absolute minimum - we must not drag over the menu 733 | if ( currentVCLeftPosition < self.leftInset && isTopViewController ) 734 | { 735 | currentVCLeftPosition = self.leftInset; 736 | } 737 | 738 | currentViewController.containerView.left = currentVCLeftPosition; 739 | 740 | isTopViewController = NO; // there can only be one. 741 | }]; 742 | 743 | [self updateViewControllerMasksAndShadow]; 744 | 745 | 746 | // special case, if we have overlapping controllers! 747 | // in this case underlying controllers are visible, but they are overlapped by another controller 748 | UIViewController *lastViewController = [self lastVisibleViewControllerCompletelyVisible:YES]; 749 | // there may be no controller completely visible - use partly visible then 750 | if (!lastViewController) { 751 | NSArray *visibleViewControllers = self.visibleViewControllers; 752 | lastViewController = [visibleViewControllers count] ? [visibleViewControllers objectAtIndex:0] : nil; 753 | } 754 | 755 | // calculate float index 756 | NSUInteger newFirstVisibleIndex = lastViewController ? [self indexOfViewController:lastViewController] : 0; 757 | CGFloat floatIndex = [self nearestValidFloatIndex:newFirstVisibleIndex]; // absolut value 758 | 759 | CGFloat overlapRatio = 0.f; 760 | UIViewController *overlappedVC = [self overlappedViewController]; 761 | if (overlappedVC) { 762 | UIViewController *rightVC = [self nextViewController:overlappedVC]; 763 | PSSVLog(@"overlapping %@ with %@", NSStringFromCGRect(overlappedVC.containerView.frame), NSStringFromCGRect(rightVC.containerView.frame)); 764 | overlapRatio = fabsf(overlappedVC.containerView.right - rightVC.containerView.left)/(overlappedVC.containerView.right - ([self screenWidth] - rightVC.containerView.width)); 765 | } 766 | 767 | // only update ratio if < 1 (else we move sth else) 768 | if (overlapRatio <= 1.f && overlapRatio > 0.f) { 769 | floatIndex += 0.5f + overlapRatio*0.5f; // fully overlapped = the .5 ratio! 770 | }else { 771 | // overlap ratio 772 | UIViewController *lastVC = [self.visibleViewControllers lastObject]; 773 | UIViewController *prevVC = [self previousViewController:lastVC]; 774 | if (lastVC && prevVC && lastVC.containerView.right > [self screenWidth]) { 775 | overlapRatio = fabsf(([self screenWidth] - lastVC.containerView.left)/([self screenWidth] - (self.leftInset + prevVC.containerView.width)))*.5f; 776 | floatIndex += overlapRatio; 777 | } 778 | } 779 | 780 | // special case for menu 781 | if (floatIndex == 0.f) { 782 | CGFloat menuCollapsedRatio = (self.largeLeftInset - self.firstViewController.containerView.left)/(self.largeLeftInset - self.leftInset); 783 | menuCollapsedRatio = MAX(0.0f, MIN(0.5f, menuCollapsedRatio/2)); 784 | floatIndex += menuCollapsedRatio; 785 | } 786 | 787 | floatIndex_ = floatIndex; 788 | } completion:nil]; 789 | } 790 | 791 | - (void)handlePanFrom:(UIPanGestureRecognizer *)recognizer { 792 | CGPoint translatedPoint = [recognizer translationInView:self.view]; 793 | UIGestureRecognizerState state = recognizer.state; 794 | 795 | static NSArray *initialVisibleViewControllers = nil; 796 | 797 | // reset last offset if gesture just started 798 | if (state == UIGestureRecognizerStateBegan) { 799 | lastDragOffset_ = 0; 800 | 801 | initialVisibleViewControllers = [self visibleViewControllers]; 802 | } 803 | 804 | NSInteger offset = translatedPoint.x - lastDragOffset_; 805 | 806 | // if the move does not make sense (no snapping region), only use 1/2 offset 807 | BOOL snapPointAvailable = [self snapPointAvailableAfterOffset:offset]; 808 | if (!snapPointAvailable) { 809 | PSSVLog(@"offset dividing/2 in effect"); 810 | 811 | // we only want to move full pixels - but if we drag slowly, 1 get divided to zero. 812 | // so only omit every second event 813 | if (abs(offset) == 1) { 814 | if(!lastDragDividedOne_) { 815 | lastDragDividedOne_ = YES; 816 | offset = 0; 817 | }else { 818 | lastDragDividedOne_ = NO; 819 | } 820 | }else { 821 | offset = roundf(offset/2.f); 822 | } 823 | } 824 | [self moveStackWithOffset:offset animated:NO userDragging:YES]; 825 | 826 | // set up designated drag destination 827 | if (state == UIGestureRecognizerStateBegan) { 828 | if (offset > 0) { 829 | lastDragOption_ = SVSnapOptionRight; 830 | }else { 831 | lastDragOption_ = SVSnapOptionLeft; 832 | } 833 | }else { 834 | // if there's a continuous drag in one direction, keep designation - else use nearest to snap. 835 | if ((lastDragOption_ == SVSnapOptionLeft && offset > 0) || (lastDragOption_ == SVSnapOptionRight && offset < 0)) { 836 | lastDragOption_ = SVSnapOptionNearest; 837 | } 838 | } 839 | 840 | // save last point to calculate new offset 841 | if (state == UIGestureRecognizerStateBegan || state == UIGestureRecognizerStateChanged) { 842 | lastDragOffset_ = translatedPoint.x; 843 | } 844 | 845 | // perform snapping after gesture ended 846 | BOOL gestureEnded = state == UIGestureRecognizerStateEnded; 847 | if (gestureEnded) { 848 | 849 | if (lastDragOption_ == SVSnapOptionRight) { 850 | self.floatIndex = [self nearestValidFloatIndex:self.floatIndex round:PSSVRoundDown]; 851 | }else if(lastDragOption_ == SVSnapOptionLeft) { 852 | self.floatIndex = [self nearestValidFloatIndex:self.floatIndex round:PSSVRoundUp]; 853 | }else { 854 | self.floatIndex = [self nearestValidFloatIndex:self.floatIndex round:PSSVRoundNearest]; 855 | } 856 | 857 | [self alignStackAnimated:YES]; 858 | [self handleVisibleChangeFromInitionalViewControllers:initialVisibleViewControllers toFinalViewControllers:[self visibleViewControllers]]; 859 | initialVisibleViewControllers = nil; 860 | } 861 | } 862 | 863 | /////////////////////////////////////////////////////////////////////////////////////////////////// 864 | #pragma mark - SVStackRootController (Public) 865 | 866 | - (NSInteger)indexOfViewController:(UIViewController *)viewController { 867 | __block NSUInteger index = [self.viewControllers indexOfObject:viewController]; 868 | if (index == NSNotFound) { 869 | index = [self.viewControllers indexOfObject:viewController.navigationController]; 870 | if (index == NSNotFound) { 871 | [self.viewControllers enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 872 | if ([obj isKindOfClass:[UINavigationController class]] && ((UINavigationController *)obj).topViewController == viewController) { 873 | index = idx; 874 | *stop = YES; 875 | } 876 | }]; 877 | } 878 | } 879 | return index; 880 | } 881 | 882 | - (UIViewController *)topViewController { 883 | return [self.viewControllers lastObject]; 884 | } 885 | 886 | - (UIViewController *)firstViewController { 887 | return [self.viewControllers count] ? [self.viewControllers objectAtIndex:0] : nil; 888 | } 889 | 890 | - (NSArray *)visibleViewControllers { 891 | return [self visibleViewControllersSetFullyVisible:NO]; 892 | } 893 | 894 | - (NSArray *)fullyVisibleViewControllers { 895 | return [self visibleViewControllersSetFullyVisible:YES]; 896 | } 897 | 898 | - (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated; { 899 | [self pushViewController:viewController fromViewController:self.topViewController animated:animated]; 900 | } 901 | 902 | - (void)pushViewController:(UIViewController *)viewController fromViewController:(UIViewController *)baseViewController animated:(BOOL)animated; { 903 | // figure out where to push, and if we need to get rid of some viewControllers 904 | if (baseViewController) { 905 | [self.viewControllers enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 906 | UIViewController *baseVC = objc_getAssociatedObject(obj, kPSSVAssociatedBaseViewControllerKey); 907 | if (baseVC == baseViewController) { 908 | PSSVLog(@"BaseViewController found on index: %d", idx); 909 | UIViewController *parentVC = [self previousViewController:obj]; 910 | if (parentVC) { 911 | [self popToViewController:parentVC animated:animated]; 912 | }else { 913 | [self popToRootViewControllerAnimated:animated]; 914 | } 915 | *stop = YES; 916 | } 917 | }]; 918 | 919 | objc_setAssociatedObject(viewController, kPSSVAssociatedBaseViewControllerKey, baseViewController, OBJC_ASSOCIATION_ASSIGN); // associate weak 920 | } 921 | 922 | PSSVLog(@"pushing with index %d on stack: %@ (animated: %d)", [self.viewControllers count], viewController, animated); 923 | viewController.view.height = [self screenHeight]; 924 | 925 | // get predefined stack width; query topViewController if we have a UINavigationController 926 | CGFloat stackWidth = viewController.stackWidth; 927 | if (stackWidth == 0.f && [viewController isKindOfClass:[UINavigationController class]]) { 928 | UIViewController *topVC = ((UINavigationController *)viewController).topViewController; 929 | stackWidth = topVC.stackWidth; 930 | } 931 | if (stackWidth > 0.f) { 932 | viewController.view.width = stackWidth; 933 | } 934 | 935 | // Starting out in portrait, right side up, we see a 20 pixel gap (for status bar???) 936 | viewController.view.top = 0.f; 937 | 938 | [self delegateWillInsertViewController:viewController]; 939 | 940 | // controller view is embedded into a container 941 | PSSVContainerView *container = [PSSVContainerView containerViewWithController:viewController]; 942 | NSUInteger leftGap = [self totalStackWidth] + [self minimalLeftInset]; 943 | container.left = leftGap; 944 | container.width = viewController.view.width; 945 | container.autoresizingMask = UIViewAutoresizingFlexibleHeight; // width is not flexible! 946 | [container limitToMaxWidth:[self maxControllerWidth]]; 947 | PSSVLog(@"container frame: %@", NSStringFromCGRect(container.frame)); 948 | 949 | // relay willAppear and add to subview 950 | if ( self.view.window ) 951 | { 952 | [viewController viewWillAppear:animated]; 953 | } 954 | 955 | if (animated) { 956 | container.alpha = 0.f; 957 | container.transform = CGAffineTransformMakeScale(1.2, 1.2); // large but fade in 958 | } 959 | 960 | [self.view addSubview:container]; 961 | 962 | if (animated) { 963 | [UIView animateWithDuration:kPSSVStackAnimationPushDuration delay:0.f options:UIViewAnimationOptionAllowUserInteraction animations:^{ 964 | container.alpha = 1.f; 965 | container.transform = CGAffineTransformIdentity; 966 | } completion:nil]; 967 | } 968 | 969 | // properly sizes the scroll view contents (for table view scrolling) 970 | [container layoutIfNeeded]; 971 | //container.width = viewController.view.width; // sync width (after it may has changed in layoutIfNeeded) 972 | 973 | // relay didAppear and add to subview 974 | [viewController viewDidAppear:animated]; 975 | [viewControllers_ addObject:viewController]; 976 | 977 | // register stack controller 978 | objc_setAssociatedObject(viewController, kPSSVAssociatedStackViewControllerKey, self, OBJC_ASSOCIATION_ASSIGN); 979 | 980 | [self updateViewControllerMasksAndShadow]; 981 | [self displayViewControllerIndexOnRightMost:[self.viewControllers count]-1 animated:animated]; 982 | [self delegateDidInsertViewController:viewController]; 983 | } 984 | 985 | - (BOOL)popViewController:(UIViewController *)controller animated:(BOOL)animated { 986 | if (controller != self.topViewController) { 987 | return NO; 988 | }else { 989 | return [self popViewControllerAnimated:animated] == controller; 990 | } 991 | } 992 | 993 | - (UIViewController *)popViewControllerAnimated:(BOOL)animated; { 994 | PSSVLog(@"popping controller: %@ (#%d total, animated:%d)", [self topViewController], [self.viewControllers count], animated); 995 | 996 | UIViewController *lastController = [self topViewController]; 997 | if (lastController) { 998 | [self delegateWillRemoveViewController:lastController]; 999 | 1000 | // remove from view stack! 1001 | PSSVContainerView *container = lastController.containerView; 1002 | [lastController viewWillDisappear:animated]; 1003 | 1004 | PSSVSimpleBlock finishBlock = ^{ 1005 | [container removeFromSuperview]; 1006 | [lastController viewDidDisappear:animated]; 1007 | [self delegateDidRemoveViewController:lastController]; 1008 | }; 1009 | 1010 | if (animated) { // kPSSVStackAnimationDuration 1011 | [UIView animateWithDuration:kPSSVStackAnimationPopDuration delay:0.f options:UIViewAnimationOptionBeginFromCurrentState animations:^(void) { 1012 | lastController.containerView.alpha = 0.f; 1013 | lastController.containerView.transform = CGAffineTransformMakeScale(0.8, 0.8); // make smaller while fading out 1014 | } completion:^(BOOL finished) { 1015 | // even with duration = 0, this doesn't fire instantly but on a future runloop with NSFireDelayedPerform, thus ugly double-check 1016 | if (finished) { 1017 | finishBlock(); 1018 | } 1019 | }]; 1020 | } 1021 | else { 1022 | finishBlock(); 1023 | } 1024 | 1025 | [viewControllers_ removeLastObject]; 1026 | 1027 | // save current stack controller as an associated object. 1028 | objc_setAssociatedObject(lastController, kPSSVAssociatedStackViewControllerKey, nil, OBJC_ASSOCIATION_ASSIGN); 1029 | 1030 | // realign view controllers 1031 | [self updateViewControllerMasksAndShadow]; 1032 | [self alignStackAnimated:animated]; 1033 | } 1034 | 1035 | return lastController; 1036 | } 1037 | 1038 | - (NSArray *)popToRootViewControllerAnimated:(BOOL)animated; { 1039 | NSMutableArray *array = [NSMutableArray array]; 1040 | while ([self.viewControllers count] > 0) { 1041 | UIViewController *vc = [self popViewControllerAnimated:animated]; 1042 | [array addObject:vc]; 1043 | } 1044 | return array; 1045 | } 1046 | 1047 | // get view controllers that are in stack _after_ current view controller 1048 | - (NSArray *)viewControllersAfterViewController:(UIViewController *)viewController { 1049 | NSParameterAssert(viewController); 1050 | NSUInteger index = [self indexOfViewController:viewController]; 1051 | if (NSNotFound == index) { 1052 | return nil; 1053 | } 1054 | 1055 | NSArray *array = nil; 1056 | // don't remove view controller we've been called with 1057 | if ([self.viewControllers count] > index + 1) { 1058 | array = [self.viewControllers subarrayWithRange:NSMakeRange(index + 1, [self.viewControllers count] - index - 1)]; 1059 | } 1060 | 1061 | return array; 1062 | } 1063 | 1064 | - (NSArray *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated; { 1065 | NSParameterAssert(viewController); 1066 | 1067 | NSUInteger index = [self indexOfViewController:viewController]; 1068 | if (NSNotFound == index) { 1069 | return nil; 1070 | } 1071 | PSSVLog(@"popping to index %d, from %d", index, [self.viewControllers count]); 1072 | 1073 | NSArray *controllersToRemove = [self viewControllersAfterViewController:viewController]; 1074 | [controllersToRemove enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 1075 | [self popViewControllerAnimated:animated]; 1076 | }]; 1077 | 1078 | [self displayViewControllerOnRightMost:viewController animated:YES]; 1079 | return controllersToRemove; 1080 | } 1081 | 1082 | - (NSArray *)controllersForClass:(Class)theClass { 1083 | NSArray *controllers = [self.viewControllers filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) { 1084 | return [evaluatedObject isKindOfClass:theClass] || ([evaluatedObject isKindOfClass:[UINavigationController class]] && [((UINavigationController *)evaluatedObject).topViewController isKindOfClass:theClass]); 1085 | }]]; 1086 | return controllers; 1087 | } 1088 | 1089 | // last visible index is calculated dynamically, depending on width of VCs 1090 | - (NSInteger)lastVisibleIndex { 1091 | NSInteger lastVisibleIndex = self.firstVisibleIndex; 1092 | 1093 | NSUInteger currentLeftInset = [self currentLeftInset]; 1094 | NSInteger screenSpaceLeft = [self screenWidth] - currentLeftInset; 1095 | while (screenSpaceLeft > 0 && lastVisibleIndex < [self.viewControllers count]) { 1096 | UIViewController *vc = [self.viewControllers objectAtIndex:lastVisibleIndex]; 1097 | screenSpaceLeft -= vc.containerView.width; 1098 | 1099 | if (screenSpaceLeft >= 0) { 1100 | lastVisibleIndex++; 1101 | } 1102 | } 1103 | 1104 | if (lastVisibleIndex > 0) { 1105 | lastVisibleIndex--; // compensate for last failure 1106 | } 1107 | 1108 | return lastVisibleIndex; 1109 | } 1110 | 1111 | // returns +/- amount if grid is not aligned correctly 1112 | // + if view is too far on the right, - if too far on the left 1113 | - (CGFloat)gridOffsetByPixels { 1114 | CGFloat gridOffset = 0; 1115 | 1116 | CGFloat firstVCLeft = self.firstViewController.containerView.left; 1117 | 1118 | // easiest case, controller is > then wide menu 1119 | if (firstVCLeft > [self currentLeftInset] || firstVCLeft < [self currentLeftInset]) { 1120 | gridOffset = firstVCLeft - [self currentLeftInset]; 1121 | }else { 1122 | NSUInteger targetIndex = self.firstVisibleIndex; // default, abs(gridOffset) < 1 1123 | 1124 | UIViewController *overlappedVC = [self overlappedViewController]; 1125 | if (overlappedVC) { 1126 | UIViewController *rightVC = [self nextViewController:overlappedVC]; 1127 | targetIndex = [self indexOfViewController:rightVC]; 1128 | PSSVLog(@"overlapping %@ with %@", NSStringFromCGRect(overlappedVC.containerView.frame), NSStringFromCGRect(rightVC.containerView.frame)); 1129 | } 1130 | 1131 | UIViewController *targetVCController = [self.viewControllers objectAtIndex:targetIndex]; 1132 | CGRect targetVCFrame = [self rectForControllerAtIndex:targetIndex]; 1133 | gridOffset = targetVCController.containerView.left - targetVCFrame.origin.x; 1134 | } 1135 | 1136 | PSSVLog(@"gridOffset: %f", gridOffset); 1137 | return gridOffset; 1138 | } 1139 | 1140 | /// detect if last drag offset is large enough that we should make a snap animation 1141 | - (BOOL)shouldSnapAnimate { 1142 | BOOL shouldSnapAnimate = abs(lastDragOffset_) > 10; 1143 | return shouldSnapAnimate; 1144 | } 1145 | 1146 | // bouncing is a three-way operation 1147 | enum { 1148 | PSSVBounceNone, 1149 | PSSVBounceMoveToInitial, 1150 | PSSVBounceBleedOver, 1151 | PSSVBounceBack, 1152 | }typedef PSSVBounceOption; 1153 | 1154 | - (void)alignStackAnimated:(BOOL)animated duration:(CGFloat)duration bounceType:(PSSVBounceOption)bounce; { 1155 | animated = animated && !self.isReducingAnimations; // don't animate if set 1156 | self.floatIndex = [self nearestValidFloatIndex:self.floatIndex]; // round to nearest correct index 1157 | UIViewAnimationCurve animationCurve = UIViewAnimationCurveEaseInOut; 1158 | if (animated) { 1159 | if (bounce == PSSVBounceMoveToInitial) { 1160 | if ([self shouldSnapAnimate]) { 1161 | animationCurve = UIViewAnimationCurveLinear; 1162 | } 1163 | CGFloat gridOffset = [self gridOffsetByPixels]; 1164 | snapBackFromLeft_ = gridOffset < 0; 1165 | 1166 | // some magic numbers to better reflect movement time 1167 | duration = abs(gridOffset)/200.f * duration * 0.4f + duration * 0.6f; 1168 | }else if(bounce == PSSVBounceBleedOver) { 1169 | animationCurve = UIViewAnimationCurveEaseOut; 1170 | } 1171 | } 1172 | 1173 | PSSVSimpleBlock alignmentBlock = ^{ 1174 | 1175 | PSSVLog(@"Begin aliging VCs. Last drag offset:%d direction:%d bounce:%d.", lastDragOffset_, lastDragOption_, bounce); 1176 | 1177 | // calculate offset used only when we're bleeding over 1178 | NSInteger snapOverOffset = 0; // > 0 = <--- ; we scrolled from right to left. 1179 | NSUInteger firstVisibleIndex = [self firstVisibleIndex]; 1180 | NSUInteger lastFullyVCIndex = [self indexOfViewController:[self lastVisibleViewControllerCompletelyVisible:YES]]; 1181 | BOOL bounceAtVeryEnd = NO; 1182 | 1183 | if ([self shouldSnapAnimate] && bounce == PSSVBounceBleedOver) { 1184 | snapOverOffset = abs(lastDragOffset_ / 5.f); 1185 | if (snapOverOffset > kPSSVMaxSnapOverOffset) { 1186 | snapOverOffset = kPSSVMaxSnapOverOffset; 1187 | } 1188 | 1189 | // positive/negative snap offset depending on snap back direction 1190 | snapOverOffset *= snapBackFromLeft_ ? 1 : -1; 1191 | 1192 | // if we're dragging menu all the way out, bounce back in 1193 | PSSVLog(@"%@", NSStringFromCGRect(self.firstViewController.containerView.frame)); 1194 | CGFloat firstVCLeft = self.firstViewController.containerView.left; 1195 | if (firstVisibleIndex == 0 && !snapBackFromLeft_ && firstVCLeft >= self.largeLeftInset) { 1196 | bounceAtVeryEnd = YES; 1197 | }else if(lastFullyVCIndex == [self.viewControllers count]-1 && lastFullyVCIndex > 0) { 1198 | bounceAtVeryEnd = YES; 1199 | } 1200 | 1201 | PSSVLog(@"bouncing with offset: %d, firstIndex:%d, snapToLeft:%d veryEnd:%d", snapOverOffset, firstVisibleIndex, snapOverOffset<0, bounceAtVeryEnd); 1202 | } 1203 | 1204 | // iterate over all view controllers and snap them to their correct positions 1205 | __block NSArray *frames = [self rectsForControllers]; 1206 | [self.viewControllers enumerateObjectsWithOptions:0 usingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 1207 | UIViewController *currentVC = (UIViewController *)obj; 1208 | 1209 | CGRect currentFrame = [[frames objectAtIndex:idx] CGRectValue]; 1210 | currentVC.containerView.left = currentFrame.origin.x; 1211 | 1212 | // menu drag to right case or swiping last vc towards menu 1213 | if (bounceAtVeryEnd) { 1214 | if (idx == firstVisibleIndex) { 1215 | frames = [self modifiedRects:frames newLeft:currentVC.containerView.left + snapOverOffset index:idx]; 1216 | } 1217 | } 1218 | // snap the leftmost view controller 1219 | else if ((snapOverOffset > 0 && idx == firstVisibleIndex) || (snapOverOffset < 0 && (idx == firstVisibleIndex)) 1220 | || [self.viewControllers count] == 1) { 1221 | frames = [self modifiedRects:frames newLeft:currentVC.containerView.left + snapOverOffset index:idx]; 1222 | } 1223 | 1224 | // set again (maybe changed) 1225 | currentFrame = [[frames objectAtIndex:idx] CGRectValue]; 1226 | currentVC.containerView.left = currentFrame.origin.x; 1227 | }]; 1228 | 1229 | [self updateViewControllerMasksAndShadow]; 1230 | 1231 | }; 1232 | 1233 | if (animated) { 1234 | [UIView animateWithDuration:duration delay:0.f 1235 | options:UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionBeginFromCurrentState | animationCurve 1236 | animations:alignmentBlock completion:^(BOOL finished) { 1237 | /* Scroll physics are applied here. Drag speed is saved in lastDragOffset. (direction with +/-, speed) 1238 | * If we are above a certain speed, we "shoot over the target", then snap back. 1239 | * This is of course dependent on the direction we scrolled. 1240 | * 1241 | * Right swiping (collapsing) makes the next vc overlapping the current vc a few pixels. 1242 | * Left swiping (expanding) takes the parent controller a few pixels with, then snapping back. 1243 | * 1244 | * We have 3 animations total 1245 | * 1) scroll to correct position 1246 | * 2) bleed over 1247 | * 3) snap back to correct position 1248 | */ 1249 | if (finished && [self shouldSnapAnimate]) { 1250 | CGFloat animationDuration = kPSSVStackAnimationBounceDuration/2.f; 1251 | switch (bounce) { 1252 | case PSSVBounceMoveToInitial: { 1253 | // bleed over now! 1254 | [self alignStackAnimated:YES duration:animationDuration bounceType:PSSVBounceBleedOver]; 1255 | }break; 1256 | case PSSVBounceBleedOver: { 1257 | // now bounce back to origin 1258 | [self alignStackAnimated:YES duration:animationDuration bounceType:PSSVBounceBack]; 1259 | }break; 1260 | 1261 | // we're done here 1262 | case PSSVBounceNone: 1263 | case PSSVBounceBack: 1264 | default: { 1265 | lastDragOffset_ = 0; // clear last drag offset for the animation 1266 | //[self removeAnimationBlockerView]; 1267 | }break; 1268 | } 1269 | } 1270 | 1271 | } 1272 | ]; 1273 | } 1274 | else { 1275 | alignmentBlock(); 1276 | } 1277 | 1278 | } 1279 | 1280 | - (void)alignStackAnimated:(BOOL)animated; { 1281 | [self alignStackAnimated:animated duration:kPSSVStackAnimationDuration bounceType:PSSVBounceMoveToInitial]; 1282 | } 1283 | 1284 | - (NSUInteger)canCollapseStack; { 1285 | NSUInteger steps = [self.viewControllers count] - self.firstVisibleIndex - 1; 1286 | 1287 | if (self.lastVisibleIndex == [self.viewControllers count]-1) { 1288 | //PSSVLog(@"complete stack is displayed - aborting."); 1289 | steps = 0; 1290 | }else if (self.firstVisibleIndex + steps > [self.viewControllers count]-1) { 1291 | steps = [self.viewControllers count] - self.firstVisibleIndex - 1; 1292 | //PSSVLog(@"too much steps, adjusting to %d", steps); 1293 | } 1294 | 1295 | return steps; 1296 | } 1297 | 1298 | 1299 | - (NSUInteger)collapseStack:(NSInteger)steps animated:(BOOL)animated; { // (<--- increases firstVisibleIndex) 1300 | PSSVLog(@"collapsing stack with %d steps [%d-%d]", steps, self.firstVisibleIndex, self.lastVisibleIndex); 1301 | 1302 | CGFloat newFloatIndex = self.floatIndex; 1303 | while (steps > 0) { 1304 | newFloatIndex = [self nextFloatIndex:newFloatIndex]; 1305 | steps--; 1306 | } 1307 | 1308 | if (newFloatIndex > 0.f) { 1309 | self.floatIndex = MAX(newFloatIndex, self.floatIndex); 1310 | } 1311 | 1312 | [self alignStackAnimated:animated]; 1313 | return steps; 1314 | } 1315 | 1316 | 1317 | - (NSUInteger)canExpandStack; { 1318 | NSUInteger steps = self.firstVisibleIndex; 1319 | 1320 | // sanity check 1321 | if (steps >= [self.viewControllers count]-1) { 1322 | PSSVLog(@"Warning: firstVisibleIndex is higher than viewController count!"); 1323 | steps = [self.viewControllers count]-1; 1324 | } 1325 | 1326 | return steps; 1327 | } 1328 | 1329 | - (NSUInteger)expandStack:(NSInteger)steps animated:(BOOL)animated; { // (---> decreases firstVisibleIndex) 1330 | steps = abs(steps); // normalize 1331 | PSSVLog(@"expanding stack with %d steps [%d-%d]", steps, self.firstVisibleIndex, self.lastVisibleIndex); 1332 | 1333 | CGFloat newFloatIndex = self.floatIndex; 1334 | while (steps > 0) { 1335 | newFloatIndex = [self prevFloatIndex:newFloatIndex]; 1336 | steps--; 1337 | } 1338 | 1339 | self.floatIndex = MIN(newFloatIndex, self.floatIndex); 1340 | 1341 | [self alignStackAnimated:animated]; 1342 | return steps; 1343 | } 1344 | 1345 | - (void)setLeftInset:(NSUInteger)leftInset { 1346 | [self setLeftInset:leftInset animated:NO]; 1347 | } 1348 | 1349 | - (void)setLeftInset:(NSUInteger)leftInset animated:(BOOL)animated; { 1350 | leftInset_ = leftInset; 1351 | [self alignStackAnimated:animated]; 1352 | } 1353 | 1354 | - (void)setLargeLeftInset:(NSUInteger)leftInset { 1355 | [self setLargeLeftInset:leftInset animated:NO]; 1356 | } 1357 | 1358 | - (void)setLargeLeftInset:(NSUInteger)leftInset animated:(BOOL)animated; { 1359 | largeLeftInset_ = leftInset; 1360 | [self alignStackAnimated:animated]; 1361 | } 1362 | 1363 | /////////////////////////////////////////////////////////////////////////////////////////////////// 1364 | #pragma mark - UIView 1365 | 1366 | - (void)viewDidLoad { 1367 | [super viewDidLoad]; 1368 | 1369 | // embedding rootViewController 1370 | if (self.rootViewController) { 1371 | [self.view addSubview:self.rootViewController.view]; 1372 | self.rootViewController.view.frame = self.view.bounds; 1373 | self.rootViewController.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 1374 | } 1375 | 1376 | for (UIViewController *controller in self.viewControllers) { 1377 | // forces view loading, calls viewDidLoad via system 1378 | UIView *controllerView = controller.view; 1379 | #pragma unused(controllerView) 1380 | // [controller viewDidLoad]; 1381 | } 1382 | } 1383 | 1384 | - (void)viewWillAppear:(BOOL)animated { 1385 | [super viewWillAppear:animated]; 1386 | 1387 | IF_PRE_IOS5( 1388 | [self.rootViewController viewWillAppear:animated]; 1389 | for (UIViewController *controller in self.viewControllers) { 1390 | [controller viewWillAppear:animated]; 1391 | }) 1392 | 1393 | // enlarge/shrinken stack 1394 | [self updateViewControllerSizes]; 1395 | [self updateViewControllerMasksAndShadow]; 1396 | [self alignStackAnimated:NO]; 1397 | } 1398 | 1399 | - (void)viewDidAppear:(BOOL)animated { 1400 | [super viewDidAppear:animated]; 1401 | 1402 | [self.rootViewController viewDidAppear:animated]; 1403 | for (UIViewController *controller in self.viewControllers) { 1404 | [controller viewDidAppear:animated]; 1405 | } 1406 | } 1407 | 1408 | - (void)viewWillDisappear:(BOOL)animated { 1409 | [super viewWillDisappear:animated]; 1410 | 1411 | [self.rootViewController viewWillDisappear:animated]; 1412 | for (UIViewController *controller in self.viewControllers) { 1413 | [controller viewWillDisappear:animated]; 1414 | } 1415 | } 1416 | 1417 | - (void)viewDidDisappear:(BOOL)animated { 1418 | [super viewDidDisappear:animated]; 1419 | 1420 | [self.rootViewController viewDidDisappear:animated]; 1421 | for (UIViewController *controller in self.viewControllers) { 1422 | [controller viewDidDisappear:animated]; 1423 | } 1424 | } 1425 | 1426 | - (void)viewDidUnload { 1427 | [self.rootViewController.view removeFromSuperview]; 1428 | self.rootViewController.view = nil; 1429 | [self.rootViewController viewDidUnload]; 1430 | 1431 | for (UIViewController *controller in self.viewControllers) { 1432 | [controller.view removeFromSuperview]; 1433 | controller.view = nil; 1434 | [controller viewDidUnload]; 1435 | } 1436 | 1437 | [super viewDidUnload]; 1438 | } 1439 | 1440 | - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation { 1441 | if (PSIsIpad()) { 1442 | return YES; 1443 | }else { 1444 | return toInterfaceOrientation != UIInterfaceOrientationPortraitUpsideDown; 1445 | } 1446 | } 1447 | 1448 | // event relay 1449 | - (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration; { 1450 | //lastVisibleIndexBeforeRotation_ = self.lastVisibleIndex; 1451 | 1452 | [rootViewController_ willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration]; 1453 | 1454 | for (UIViewController *controller in self.viewControllers) { 1455 | [controller willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration]; 1456 | } 1457 | } 1458 | 1459 | // event relay 1460 | - (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation; { 1461 | [rootViewController_ didRotateFromInterfaceOrientation:fromInterfaceOrientation]; 1462 | 1463 | if (self.isReducingAnimations) { 1464 | [self updateViewControllerSizes]; 1465 | [self updateViewControllerMasksAndShadow]; 1466 | } 1467 | 1468 | for (UIViewController *controller in self.viewControllers) { 1469 | [controller didRotateFromInterfaceOrientation:fromInterfaceOrientation]; 1470 | } 1471 | 1472 | // ensure we're correctly aligned (may be messed up in willAnimate, if panRecognizer is still active) 1473 | [self alignStackAnimated:!self.isReducingAnimations]; 1474 | } 1475 | 1476 | // event relay 1477 | - (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration; { 1478 | [rootViewController_ willAnimateRotationToInterfaceOrientation:toInterfaceOrientation duration:duration]; 1479 | 1480 | if (!self.isReducingAnimations) { 1481 | [self updateViewControllerSizes]; 1482 | [self updateViewControllerMasksAndShadow]; 1483 | } 1484 | 1485 | // finally relay rotation events 1486 | for (UIViewController *controller in self.viewControllers) { 1487 | [controller willAnimateRotationToInterfaceOrientation:toInterfaceOrientation duration:duration]; 1488 | } 1489 | 1490 | // enlarge/shrinken stack 1491 | [self alignStackAnimated:!self.isReducingAnimations]; 1492 | } 1493 | 1494 | /////////////////////////////////////////////////////////////////////////////////////////////////// 1495 | #pragma mark - UIGestureRecognizerDelegate 1496 | 1497 | - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch { 1498 | if ([touch.view isKindOfClass:[UIControl class]]) { 1499 | // prevent recognizing touches on the slider 1500 | return NO; 1501 | } 1502 | return YES; 1503 | } 1504 | 1505 | @end 1506 | --------------------------------------------------------------------------------