├── Examples └── StackExample │ ├── StackExample.xcodeproj │ └── project.pbxproj │ ├── StackExample │ ├── ChildTestViewController.h │ ├── ChildTestViewController.m │ ├── ChildTestViewController.xib │ ├── Default-568h@2x.png │ ├── Default.png │ ├── Default@2x.png │ ├── RootTestViewController.h │ ├── RootTestViewController.m │ ├── RootTestViewController.xib │ ├── SPAppDelegate.h │ ├── SPAppDelegate.m │ ├── StackExample-Info.plist │ ├── StackExample-Prefix.pch │ ├── en.lproj │ │ └── InfoPlist.strings │ └── main.m │ └── icons-from-glyphish │ ├── 00-Read me first - license.txt │ ├── 114-balloon.png │ └── 185-printer.png ├── LICENSE ├── README.md ├── Resources ├── SPSideTabBar-button-overlay+normal.png ├── SPSideTabBar-button-overlay+selected+pressed.png ├── SPSideTabBar-button-overlay+selected.png ├── backgroundTexture.png ├── backgroundTexture@2x.png ├── bg-tb@2x~ipad.png ├── bg-tb~ipad.png ├── stackShadow-right~ipad.png ├── stackShadow-right~ipad@2x.png ├── stackShadow~ipad.png └── stackShadow~ipad@2x.png ├── SPStackedNav.podspec ├── Sources ├── SPBadgeView.h ├── SPBadgeView.m ├── SPFunctional-mini.h ├── SPFunctional-mini.m ├── SPSeparatorView.h ├── SPSeparatorView.m ├── SPSideTabBar.m ├── SPSideTabController.m ├── SPSideTabItemButton.h ├── SPSideTabItemButton.m ├── SPStackedNavigationController.m ├── SPStackedNavigationScrollView.h ├── SPStackedNavigationScrollView.m ├── SPStackedPageContainer.h ├── SPStackedPageContainer.m ├── UIImage+SPTabBarImage.h └── UIImage+SPTabBarImage.m └── include └── SPStackedNav ├── SPSideTabBar.h ├── SPSideTabController.h ├── SPStackedNav.h └── SPStackedNavigationController.h /Examples/StackExample/StackExample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 05A4D248163C2F4000309444 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05A4D247163C2F4000309444 /* UIKit.framework */; }; 11 | 05A4D24A163C2F4000309444 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05A4D249163C2F4000309444 /* Foundation.framework */; }; 12 | 05A4D24C163C2F4000309444 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05A4D24B163C2F4000309444 /* CoreGraphics.framework */; }; 13 | 05A4D252163C2F4000309444 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 05A4D250163C2F4000309444 /* InfoPlist.strings */; }; 14 | 05A4D254163C2F4000309444 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 05A4D253163C2F4000309444 /* main.m */; }; 15 | 05A4D258163C2F4000309444 /* SPAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 05A4D257163C2F4000309444 /* SPAppDelegate.m */; }; 16 | 05A4D25A163C2F4000309444 /* Default.png in Resources */ = {isa = PBXBuildFile; fileRef = 05A4D259163C2F4000309444 /* Default.png */; }; 17 | 05A4D25C163C2F4000309444 /* Default@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 05A4D25B163C2F4000309444 /* Default@2x.png */; }; 18 | 05A4D25E163C2F4000309444 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 05A4D25D163C2F4000309444 /* Default-568h@2x.png */; }; 19 | 05A4D292163C2F8600309444 /* SPBadgeView.m in Sources */ = {isa = PBXBuildFile; fileRef = 05A4D285163C2F8600309444 /* SPBadgeView.m */; }; 20 | 05A4D293163C2F8600309444 /* SPSeparatorView.m in Sources */ = {isa = PBXBuildFile; fileRef = 05A4D287163C2F8600309444 /* SPSeparatorView.m */; }; 21 | 05A4D294163C2F8600309444 /* SPSideTabController.m in Sources */ = {isa = PBXBuildFile; fileRef = 05A4D288163C2F8600309444 /* SPSideTabController.m */; }; 22 | 05A4D295163C2F8600309444 /* SPSideTabItemButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 05A4D28A163C2F8600309444 /* SPSideTabItemButton.m */; }; 23 | 05A4D296163C2F8600309444 /* SPStackedNavigationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 05A4D28B163C2F8600309444 /* SPStackedNavigationController.m */; }; 24 | 05A4D297163C2F8600309444 /* SPStackedNavigationScrollView.m in Sources */ = {isa = PBXBuildFile; fileRef = 05A4D28D163C2F8600309444 /* SPStackedNavigationScrollView.m */; }; 25 | 05A4D298163C2F8600309444 /* SPStackedPageContainer.m in Sources */ = {isa = PBXBuildFile; fileRef = 05A4D28F163C2F8600309444 /* SPStackedPageContainer.m */; }; 26 | 05A4D299163C2F8600309444 /* UIImage+SPTabBarImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 05A4D291163C2F8600309444 /* UIImage+SPTabBarImage.m */; }; 27 | 05A4D29C163C2FDA00309444 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05A4D29B163C2FDA00309444 /* QuartzCore.framework */; }; 28 | 05A4D2A0163C306C00309444 /* SPSideTabBar.m in Sources */ = {isa = PBXBuildFile; fileRef = 05A4D29F163C306C00309444 /* SPSideTabBar.m */; }; 29 | 05A4D2A4163C31C900309444 /* RootTestViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 05A4D2A2163C31C800309444 /* RootTestViewController.m */; }; 30 | 05A4D2A5163C31C900309444 /* RootTestViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 05A4D2A3163C31C800309444 /* RootTestViewController.xib */; }; 31 | 05A4D2A9163C330300309444 /* ChildTestViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 05A4D2A7163C330200309444 /* ChildTestViewController.m */; }; 32 | 05A4D2AA163C330300309444 /* ChildTestViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 05A4D2A8163C330200309444 /* ChildTestViewController.xib */; }; 33 | 05A4D2AE163C365C00309444 /* bg-tb@2x~ipad.png in Resources */ = {isa = PBXBuildFile; fileRef = 05A4D2AC163C365C00309444 /* bg-tb@2x~ipad.png */; }; 34 | 05A4D2AF163C365C00309444 /* bg-tb~ipad.png in Resources */ = {isa = PBXBuildFile; fileRef = 05A4D2AD163C365C00309444 /* bg-tb~ipad.png */; }; 35 | 05A4D2B2163C36D700309444 /* backgroundTexture.png in Resources */ = {isa = PBXBuildFile; fileRef = 05A4D2B0163C36D700309444 /* backgroundTexture.png */; }; 36 | 05A4D2B3163C36D700309444 /* backgroundTexture@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 05A4D2B1163C36D700309444 /* backgroundTexture@2x.png */; }; 37 | 05A4D2B8163C36EE00309444 /* stackShadow-right~ipad.png in Resources */ = {isa = PBXBuildFile; fileRef = 05A4D2B4163C36EE00309444 /* stackShadow-right~ipad.png */; }; 38 | 05A4D2B9163C36EE00309444 /* stackShadow-right~ipad@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 05A4D2B5163C36EE00309444 /* stackShadow-right~ipad@2x.png */; }; 39 | 05A4D2BA163C36EE00309444 /* stackShadow~ipad.png in Resources */ = {isa = PBXBuildFile; fileRef = 05A4D2B6163C36EE00309444 /* stackShadow~ipad.png */; }; 40 | 05A4D2BB163C36EE00309444 /* stackShadow~ipad@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 05A4D2B7163C36EE00309444 /* stackShadow~ipad@2x.png */; }; 41 | 05A4D2C3163C37BB00309444 /* SPSideTabBar-button-overlay+normal.png in Resources */ = {isa = PBXBuildFile; fileRef = 05A4D2C0163C37BB00309444 /* SPSideTabBar-button-overlay+normal.png */; }; 42 | 05A4D2C4163C37BB00309444 /* SPSideTabBar-button-overlay+selected.png in Resources */ = {isa = PBXBuildFile; fileRef = 05A4D2C1163C37BB00309444 /* SPSideTabBar-button-overlay+selected.png */; }; 43 | 05A4D2C5163C37BB00309444 /* SPSideTabBar-button-overlay+selected+pressed.png in Resources */ = {isa = PBXBuildFile; fileRef = 05A4D2C2163C37BB00309444 /* SPSideTabBar-button-overlay+selected+pressed.png */; }; 44 | 05A4D2C8163C3E2900309444 /* 114-balloon.png in Resources */ = {isa = PBXBuildFile; fileRef = 05A4D2C6163C3E2900309444 /* 114-balloon.png */; }; 45 | 05A4D2C9163C3E2900309444 /* 185-printer.png in Resources */ = {isa = PBXBuildFile; fileRef = 05A4D2C7163C3E2900309444 /* 185-printer.png */; }; 46 | 05BD7D1E18CF8DBE00A990C9 /* SPFunctional-mini.m in Sources */ = {isa = PBXBuildFile; fileRef = 05BD7D1D18CF8DBE00A990C9 /* SPFunctional-mini.m */; }; 47 | /* End PBXBuildFile section */ 48 | 49 | /* Begin PBXFileReference section */ 50 | 05A4D243163C2F4000309444 /* StackExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = StackExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 51 | 05A4D247163C2F4000309444 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; 52 | 05A4D249163C2F4000309444 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 53 | 05A4D24B163C2F4000309444 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; 54 | 05A4D24F163C2F4000309444 /* StackExample-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "StackExample-Info.plist"; sourceTree = ""; }; 55 | 05A4D251163C2F4000309444 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 56 | 05A4D253163C2F4000309444 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 57 | 05A4D255163C2F4000309444 /* StackExample-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "StackExample-Prefix.pch"; sourceTree = ""; }; 58 | 05A4D256163C2F4000309444 /* SPAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SPAppDelegate.h; sourceTree = ""; }; 59 | 05A4D257163C2F4000309444 /* SPAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SPAppDelegate.m; sourceTree = ""; }; 60 | 05A4D259163C2F4000309444 /* Default.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Default.png; sourceTree = ""; }; 61 | 05A4D25B163C2F4000309444 /* Default@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default@2x.png"; sourceTree = ""; }; 62 | 05A4D25D163C2F4000309444 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-568h@2x.png"; sourceTree = ""; }; 63 | 05A4D281163C2F8600309444 /* SPSideTabController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPSideTabController.h; sourceTree = ""; }; 64 | 05A4D282163C2F8600309444 /* SPStackedNavigationController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPStackedNavigationController.h; sourceTree = ""; }; 65 | 05A4D284163C2F8600309444 /* SPBadgeView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPBadgeView.h; sourceTree = ""; }; 66 | 05A4D285163C2F8600309444 /* SPBadgeView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPBadgeView.m; sourceTree = ""; }; 67 | 05A4D286163C2F8600309444 /* SPSeparatorView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPSeparatorView.h; sourceTree = ""; }; 68 | 05A4D287163C2F8600309444 /* SPSeparatorView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPSeparatorView.m; sourceTree = ""; }; 69 | 05A4D288163C2F8600309444 /* SPSideTabController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPSideTabController.m; sourceTree = ""; }; 70 | 05A4D289163C2F8600309444 /* SPSideTabItemButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPSideTabItemButton.h; sourceTree = ""; }; 71 | 05A4D28A163C2F8600309444 /* SPSideTabItemButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPSideTabItemButton.m; sourceTree = ""; }; 72 | 05A4D28B163C2F8600309444 /* SPStackedNavigationController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPStackedNavigationController.m; sourceTree = ""; }; 73 | 05A4D28C163C2F8600309444 /* SPStackedNavigationScrollView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPStackedNavigationScrollView.h; sourceTree = ""; }; 74 | 05A4D28D163C2F8600309444 /* SPStackedNavigationScrollView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPStackedNavigationScrollView.m; sourceTree = ""; }; 75 | 05A4D28E163C2F8600309444 /* SPStackedPageContainer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPStackedPageContainer.h; sourceTree = ""; }; 76 | 05A4D28F163C2F8600309444 /* SPStackedPageContainer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPStackedPageContainer.m; sourceTree = ""; }; 77 | 05A4D290163C2F8600309444 /* UIImage+SPTabBarImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIImage+SPTabBarImage.h"; sourceTree = ""; }; 78 | 05A4D291163C2F8600309444 /* UIImage+SPTabBarImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIImage+SPTabBarImage.m"; sourceTree = ""; }; 79 | 05A4D29B163C2FDA00309444 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; 80 | 05A4D29D163C300400309444 /* SPStackedNav.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPStackedNav.h; sourceTree = ""; }; 81 | 05A4D29E163C306200309444 /* SPSideTabBar.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPSideTabBar.h; sourceTree = ""; }; 82 | 05A4D29F163C306C00309444 /* SPSideTabBar.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPSideTabBar.m; sourceTree = ""; }; 83 | 05A4D2A1163C31C800309444 /* RootTestViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RootTestViewController.h; sourceTree = ""; }; 84 | 05A4D2A2163C31C800309444 /* RootTestViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RootTestViewController.m; sourceTree = ""; }; 85 | 05A4D2A3163C31C800309444 /* RootTestViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = RootTestViewController.xib; sourceTree = ""; }; 86 | 05A4D2A6163C330200309444 /* ChildTestViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ChildTestViewController.h; sourceTree = ""; }; 87 | 05A4D2A7163C330200309444 /* ChildTestViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ChildTestViewController.m; sourceTree = ""; }; 88 | 05A4D2A8163C330200309444 /* ChildTestViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ChildTestViewController.xib; sourceTree = ""; }; 89 | 05A4D2AC163C365C00309444 /* bg-tb@2x~ipad.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "bg-tb@2x~ipad.png"; sourceTree = ""; }; 90 | 05A4D2AD163C365C00309444 /* bg-tb~ipad.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "bg-tb~ipad.png"; sourceTree = ""; }; 91 | 05A4D2B0163C36D700309444 /* backgroundTexture.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = backgroundTexture.png; sourceTree = ""; }; 92 | 05A4D2B1163C36D700309444 /* backgroundTexture@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "backgroundTexture@2x.png"; sourceTree = ""; }; 93 | 05A4D2B4163C36EE00309444 /* stackShadow-right~ipad.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stackShadow-right~ipad.png"; sourceTree = ""; }; 94 | 05A4D2B5163C36EE00309444 /* stackShadow-right~ipad@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stackShadow-right~ipad@2x.png"; sourceTree = ""; }; 95 | 05A4D2B6163C36EE00309444 /* stackShadow~ipad.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stackShadow~ipad.png"; sourceTree = ""; }; 96 | 05A4D2B7163C36EE00309444 /* stackShadow~ipad@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stackShadow~ipad@2x.png"; sourceTree = ""; }; 97 | 05A4D2C0163C37BB00309444 /* SPSideTabBar-button-overlay+normal.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "SPSideTabBar-button-overlay+normal.png"; sourceTree = ""; }; 98 | 05A4D2C1163C37BB00309444 /* SPSideTabBar-button-overlay+selected.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "SPSideTabBar-button-overlay+selected.png"; sourceTree = ""; }; 99 | 05A4D2C2163C37BB00309444 /* SPSideTabBar-button-overlay+selected+pressed.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "SPSideTabBar-button-overlay+selected+pressed.png"; sourceTree = ""; }; 100 | 05A4D2C6163C3E2900309444 /* 114-balloon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "114-balloon.png"; path = "icons-from-glyphish/114-balloon.png"; sourceTree = SOURCE_ROOT; }; 101 | 05A4D2C7163C3E2900309444 /* 185-printer.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "185-printer.png"; path = "icons-from-glyphish/185-printer.png"; sourceTree = SOURCE_ROOT; }; 102 | 05BD7D1C18CF8DBE00A990C9 /* SPFunctional-mini.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SPFunctional-mini.h"; sourceTree = ""; }; 103 | 05BD7D1D18CF8DBE00A990C9 /* SPFunctional-mini.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "SPFunctional-mini.m"; sourceTree = ""; }; 104 | /* End PBXFileReference section */ 105 | 106 | /* Begin PBXFrameworksBuildPhase section */ 107 | 05A4D240163C2F4000309444 /* Frameworks */ = { 108 | isa = PBXFrameworksBuildPhase; 109 | buildActionMask = 2147483647; 110 | files = ( 111 | 05A4D29C163C2FDA00309444 /* QuartzCore.framework in Frameworks */, 112 | 05A4D248163C2F4000309444 /* UIKit.framework in Frameworks */, 113 | 05A4D24A163C2F4000309444 /* Foundation.framework in Frameworks */, 114 | 05A4D24C163C2F4000309444 /* CoreGraphics.framework in Frameworks */, 115 | ); 116 | runOnlyForDeploymentPostprocessing = 0; 117 | }; 118 | /* End PBXFrameworksBuildPhase section */ 119 | 120 | /* Begin PBXGroup section */ 121 | 05A4D238163C2F4000309444 = { 122 | isa = PBXGroup; 123 | children = ( 124 | 05A4D24D163C2F4000309444 /* StackExample */, 125 | 05A4D29A163C2F8900309444 /* Stacked nav */, 126 | 05A4D246163C2F4000309444 /* Frameworks */, 127 | 05A4D244163C2F4000309444 /* Products */, 128 | ); 129 | sourceTree = ""; 130 | }; 131 | 05A4D244163C2F4000309444 /* Products */ = { 132 | isa = PBXGroup; 133 | children = ( 134 | 05A4D243163C2F4000309444 /* StackExample.app */, 135 | ); 136 | name = Products; 137 | sourceTree = ""; 138 | }; 139 | 05A4D246163C2F4000309444 /* Frameworks */ = { 140 | isa = PBXGroup; 141 | children = ( 142 | 05A4D29B163C2FDA00309444 /* QuartzCore.framework */, 143 | 05A4D247163C2F4000309444 /* UIKit.framework */, 144 | 05A4D249163C2F4000309444 /* Foundation.framework */, 145 | 05A4D24B163C2F4000309444 /* CoreGraphics.framework */, 146 | ); 147 | name = Frameworks; 148 | sourceTree = ""; 149 | }; 150 | 05A4D24D163C2F4000309444 /* StackExample */ = { 151 | isa = PBXGroup; 152 | children = ( 153 | 05A4D256163C2F4000309444 /* SPAppDelegate.h */, 154 | 05A4D257163C2F4000309444 /* SPAppDelegate.m */, 155 | 05A4D2C6163C3E2900309444 /* 114-balloon.png */, 156 | 05A4D2C7163C3E2900309444 /* 185-printer.png */, 157 | 05A4D2A1163C31C800309444 /* RootTestViewController.h */, 158 | 05A4D2A2163C31C800309444 /* RootTestViewController.m */, 159 | 05A4D2A3163C31C800309444 /* RootTestViewController.xib */, 160 | 05A4D2A6163C330200309444 /* ChildTestViewController.h */, 161 | 05A4D2A7163C330200309444 /* ChildTestViewController.m */, 162 | 05A4D2A8163C330200309444 /* ChildTestViewController.xib */, 163 | 05A4D24E163C2F4000309444 /* Supporting Files */, 164 | ); 165 | path = StackExample; 166 | sourceTree = ""; 167 | }; 168 | 05A4D24E163C2F4000309444 /* Supporting Files */ = { 169 | isa = PBXGroup; 170 | children = ( 171 | 05A4D24F163C2F4000309444 /* StackExample-Info.plist */, 172 | 05A4D250163C2F4000309444 /* InfoPlist.strings */, 173 | 05A4D253163C2F4000309444 /* main.m */, 174 | 05A4D255163C2F4000309444 /* StackExample-Prefix.pch */, 175 | 05A4D259163C2F4000309444 /* Default.png */, 176 | 05A4D25B163C2F4000309444 /* Default@2x.png */, 177 | 05A4D25D163C2F4000309444 /* Default-568h@2x.png */, 178 | ); 179 | name = "Supporting Files"; 180 | sourceTree = ""; 181 | }; 182 | 05A4D27F163C2F8600309444 /* include */ = { 183 | isa = PBXGroup; 184 | children = ( 185 | 05A4D280163C2F8600309444 /* SPStackedNav */, 186 | ); 187 | name = include; 188 | path = ../../include; 189 | sourceTree = ""; 190 | }; 191 | 05A4D280163C2F8600309444 /* SPStackedNav */ = { 192 | isa = PBXGroup; 193 | children = ( 194 | 05A4D29D163C300400309444 /* SPStackedNav.h */, 195 | 05A4D281163C2F8600309444 /* SPSideTabController.h */, 196 | 05A4D282163C2F8600309444 /* SPStackedNavigationController.h */, 197 | 05A4D29E163C306200309444 /* SPSideTabBar.h */, 198 | ); 199 | path = SPStackedNav; 200 | sourceTree = ""; 201 | }; 202 | 05A4D283163C2F8600309444 /* Sources */ = { 203 | isa = PBXGroup; 204 | children = ( 205 | 05A4D284163C2F8600309444 /* SPBadgeView.h */, 206 | 05A4D285163C2F8600309444 /* SPBadgeView.m */, 207 | 05A4D286163C2F8600309444 /* SPSeparatorView.h */, 208 | 05A4D287163C2F8600309444 /* SPSeparatorView.m */, 209 | 05A4D288163C2F8600309444 /* SPSideTabController.m */, 210 | 05A4D29F163C306C00309444 /* SPSideTabBar.m */, 211 | 05A4D289163C2F8600309444 /* SPSideTabItemButton.h */, 212 | 05A4D28A163C2F8600309444 /* SPSideTabItemButton.m */, 213 | 05A4D28B163C2F8600309444 /* SPStackedNavigationController.m */, 214 | 05A4D28C163C2F8600309444 /* SPStackedNavigationScrollView.h */, 215 | 05A4D28D163C2F8600309444 /* SPStackedNavigationScrollView.m */, 216 | 05A4D28E163C2F8600309444 /* SPStackedPageContainer.h */, 217 | 05A4D28F163C2F8600309444 /* SPStackedPageContainer.m */, 218 | 05A4D290163C2F8600309444 /* UIImage+SPTabBarImage.h */, 219 | 05A4D291163C2F8600309444 /* UIImage+SPTabBarImage.m */, 220 | 05BD7D1C18CF8DBE00A990C9 /* SPFunctional-mini.h */, 221 | 05BD7D1D18CF8DBE00A990C9 /* SPFunctional-mini.m */, 222 | ); 223 | name = Sources; 224 | path = ../../Sources; 225 | sourceTree = ""; 226 | }; 227 | 05A4D29A163C2F8900309444 /* Stacked nav */ = { 228 | isa = PBXGroup; 229 | children = ( 230 | 05A4D2AB163C365C00309444 /* Resources */, 231 | 05A4D27F163C2F8600309444 /* include */, 232 | 05A4D283163C2F8600309444 /* Sources */, 233 | ); 234 | name = "Stacked nav"; 235 | sourceTree = ""; 236 | }; 237 | 05A4D2AB163C365C00309444 /* Resources */ = { 238 | isa = PBXGroup; 239 | children = ( 240 | 05A4D2AC163C365C00309444 /* bg-tb@2x~ipad.png */, 241 | 05A4D2AD163C365C00309444 /* bg-tb~ipad.png */, 242 | 05A4D2B0163C36D700309444 /* backgroundTexture.png */, 243 | 05A4D2B1163C36D700309444 /* backgroundTexture@2x.png */, 244 | 05A4D2B4163C36EE00309444 /* stackShadow-right~ipad.png */, 245 | 05A4D2B5163C36EE00309444 /* stackShadow-right~ipad@2x.png */, 246 | 05A4D2B6163C36EE00309444 /* stackShadow~ipad.png */, 247 | 05A4D2B7163C36EE00309444 /* stackShadow~ipad@2x.png */, 248 | 05A4D2C0163C37BB00309444 /* SPSideTabBar-button-overlay+normal.png */, 249 | 05A4D2C1163C37BB00309444 /* SPSideTabBar-button-overlay+selected.png */, 250 | 05A4D2C2163C37BB00309444 /* SPSideTabBar-button-overlay+selected+pressed.png */, 251 | ); 252 | name = Resources; 253 | path = ../../Resources; 254 | sourceTree = ""; 255 | }; 256 | /* End PBXGroup section */ 257 | 258 | /* Begin PBXNativeTarget section */ 259 | 05A4D242163C2F4000309444 /* StackExample */ = { 260 | isa = PBXNativeTarget; 261 | buildConfigurationList = 05A4D261163C2F4000309444 /* Build configuration list for PBXNativeTarget "StackExample" */; 262 | buildPhases = ( 263 | 05A4D23F163C2F4000309444 /* Sources */, 264 | 05A4D240163C2F4000309444 /* Frameworks */, 265 | 05A4D241163C2F4000309444 /* Resources */, 266 | ); 267 | buildRules = ( 268 | ); 269 | dependencies = ( 270 | ); 271 | name = StackExample; 272 | productName = StackExample; 273 | productReference = 05A4D243163C2F4000309444 /* StackExample.app */; 274 | productType = "com.apple.product-type.application"; 275 | }; 276 | /* End PBXNativeTarget section */ 277 | 278 | /* Begin PBXProject section */ 279 | 05A4D23A163C2F4000309444 /* Project object */ = { 280 | isa = PBXProject; 281 | attributes = { 282 | CLASSPREFIX = SP; 283 | LastUpgradeCheck = 0500; 284 | ORGANIZATIONNAME = Spotify; 285 | }; 286 | buildConfigurationList = 05A4D23D163C2F4000309444 /* Build configuration list for PBXProject "StackExample" */; 287 | compatibilityVersion = "Xcode 3.2"; 288 | developmentRegion = English; 289 | hasScannedForEncodings = 0; 290 | knownRegions = ( 291 | en, 292 | ); 293 | mainGroup = 05A4D238163C2F4000309444; 294 | productRefGroup = 05A4D244163C2F4000309444 /* Products */; 295 | projectDirPath = ""; 296 | projectRoot = ""; 297 | targets = ( 298 | 05A4D242163C2F4000309444 /* StackExample */, 299 | ); 300 | }; 301 | /* End PBXProject section */ 302 | 303 | /* Begin PBXResourcesBuildPhase section */ 304 | 05A4D241163C2F4000309444 /* Resources */ = { 305 | isa = PBXResourcesBuildPhase; 306 | buildActionMask = 2147483647; 307 | files = ( 308 | 05A4D252163C2F4000309444 /* InfoPlist.strings in Resources */, 309 | 05A4D25A163C2F4000309444 /* Default.png in Resources */, 310 | 05A4D25C163C2F4000309444 /* Default@2x.png in Resources */, 311 | 05A4D25E163C2F4000309444 /* Default-568h@2x.png in Resources */, 312 | 05A4D2A5163C31C900309444 /* RootTestViewController.xib in Resources */, 313 | 05A4D2AA163C330300309444 /* ChildTestViewController.xib in Resources */, 314 | 05A4D2AE163C365C00309444 /* bg-tb@2x~ipad.png in Resources */, 315 | 05A4D2AF163C365C00309444 /* bg-tb~ipad.png in Resources */, 316 | 05A4D2B2163C36D700309444 /* backgroundTexture.png in Resources */, 317 | 05A4D2B3163C36D700309444 /* backgroundTexture@2x.png in Resources */, 318 | 05A4D2B8163C36EE00309444 /* stackShadow-right~ipad.png in Resources */, 319 | 05A4D2B9163C36EE00309444 /* stackShadow-right~ipad@2x.png in Resources */, 320 | 05A4D2BA163C36EE00309444 /* stackShadow~ipad.png in Resources */, 321 | 05A4D2BB163C36EE00309444 /* stackShadow~ipad@2x.png in Resources */, 322 | 05A4D2C3163C37BB00309444 /* SPSideTabBar-button-overlay+normal.png in Resources */, 323 | 05A4D2C4163C37BB00309444 /* SPSideTabBar-button-overlay+selected.png in Resources */, 324 | 05A4D2C5163C37BB00309444 /* SPSideTabBar-button-overlay+selected+pressed.png in Resources */, 325 | 05A4D2C8163C3E2900309444 /* 114-balloon.png in Resources */, 326 | 05A4D2C9163C3E2900309444 /* 185-printer.png in Resources */, 327 | ); 328 | runOnlyForDeploymentPostprocessing = 0; 329 | }; 330 | /* End PBXResourcesBuildPhase section */ 331 | 332 | /* Begin PBXSourcesBuildPhase section */ 333 | 05A4D23F163C2F4000309444 /* Sources */ = { 334 | isa = PBXSourcesBuildPhase; 335 | buildActionMask = 2147483647; 336 | files = ( 337 | 05BD7D1E18CF8DBE00A990C9 /* SPFunctional-mini.m in Sources */, 338 | 05A4D254163C2F4000309444 /* main.m in Sources */, 339 | 05A4D258163C2F4000309444 /* SPAppDelegate.m in Sources */, 340 | 05A4D292163C2F8600309444 /* SPBadgeView.m in Sources */, 341 | 05A4D293163C2F8600309444 /* SPSeparatorView.m in Sources */, 342 | 05A4D294163C2F8600309444 /* SPSideTabController.m in Sources */, 343 | 05A4D295163C2F8600309444 /* SPSideTabItemButton.m in Sources */, 344 | 05A4D296163C2F8600309444 /* SPStackedNavigationController.m in Sources */, 345 | 05A4D297163C2F8600309444 /* SPStackedNavigationScrollView.m in Sources */, 346 | 05A4D298163C2F8600309444 /* SPStackedPageContainer.m in Sources */, 347 | 05A4D299163C2F8600309444 /* UIImage+SPTabBarImage.m in Sources */, 348 | 05A4D2A0163C306C00309444 /* SPSideTabBar.m in Sources */, 349 | 05A4D2A4163C31C900309444 /* RootTestViewController.m in Sources */, 350 | 05A4D2A9163C330300309444 /* ChildTestViewController.m in Sources */, 351 | ); 352 | runOnlyForDeploymentPostprocessing = 0; 353 | }; 354 | /* End PBXSourcesBuildPhase section */ 355 | 356 | /* Begin PBXVariantGroup section */ 357 | 05A4D250163C2F4000309444 /* InfoPlist.strings */ = { 358 | isa = PBXVariantGroup; 359 | children = ( 360 | 05A4D251163C2F4000309444 /* en */, 361 | ); 362 | name = InfoPlist.strings; 363 | sourceTree = ""; 364 | }; 365 | /* End PBXVariantGroup section */ 366 | 367 | /* Begin XCBuildConfiguration section */ 368 | 05A4D25F163C2F4000309444 /* Debug */ = { 369 | isa = XCBuildConfiguration; 370 | buildSettings = { 371 | ALWAYS_SEARCH_USER_PATHS = NO; 372 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 373 | CLANG_CXX_LIBRARY = "libc++"; 374 | CLANG_ENABLE_OBJC_ARC = YES; 375 | CLANG_WARN_EMPTY_BODY = YES; 376 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 377 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 378 | COPY_PHASE_STRIP = NO; 379 | GCC_C_LANGUAGE_STANDARD = gnu99; 380 | GCC_DYNAMIC_NO_PIC = NO; 381 | GCC_OPTIMIZATION_LEVEL = 0; 382 | GCC_PREPROCESSOR_DEFINITIONS = ( 383 | "DEBUG=1", 384 | "$(inherited)", 385 | ); 386 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 387 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 388 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 389 | GCC_WARN_UNUSED_VARIABLE = YES; 390 | IPHONEOS_DEPLOYMENT_TARGET = 6.0; 391 | ONLY_ACTIVE_ARCH = YES; 392 | SDKROOT = iphoneos; 393 | TARGETED_DEVICE_FAMILY = "1,2"; 394 | }; 395 | name = Debug; 396 | }; 397 | 05A4D260163C2F4000309444 /* Release */ = { 398 | isa = XCBuildConfiguration; 399 | buildSettings = { 400 | ALWAYS_SEARCH_USER_PATHS = NO; 401 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 402 | CLANG_CXX_LIBRARY = "libc++"; 403 | CLANG_ENABLE_OBJC_ARC = YES; 404 | CLANG_WARN_EMPTY_BODY = YES; 405 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 406 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 407 | COPY_PHASE_STRIP = YES; 408 | GCC_C_LANGUAGE_STANDARD = gnu99; 409 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 410 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 411 | GCC_WARN_UNUSED_VARIABLE = YES; 412 | IPHONEOS_DEPLOYMENT_TARGET = 6.0; 413 | OTHER_CFLAGS = "-DNS_BLOCK_ASSERTIONS=1"; 414 | SDKROOT = iphoneos; 415 | TARGETED_DEVICE_FAMILY = "1,2"; 416 | VALIDATE_PRODUCT = YES; 417 | }; 418 | name = Release; 419 | }; 420 | 05A4D262163C2F4000309444 /* Debug */ = { 421 | isa = XCBuildConfiguration; 422 | buildSettings = { 423 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 424 | GCC_PREFIX_HEADER = "StackExample/StackExample-Prefix.pch"; 425 | HEADER_SEARCH_PATHS = ( 426 | vendor/SPSuccinct, 427 | ../../include, 428 | ); 429 | INFOPLIST_FILE = "StackExample/StackExample-Info.plist"; 430 | PRODUCT_NAME = "$(TARGET_NAME)"; 431 | WRAPPER_EXTENSION = app; 432 | }; 433 | name = Debug; 434 | }; 435 | 05A4D263163C2F4000309444 /* Release */ = { 436 | isa = XCBuildConfiguration; 437 | buildSettings = { 438 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 439 | GCC_PREFIX_HEADER = "StackExample/StackExample-Prefix.pch"; 440 | HEADER_SEARCH_PATHS = ( 441 | vendor/SPSuccinct, 442 | ../../include, 443 | ); 444 | INFOPLIST_FILE = "StackExample/StackExample-Info.plist"; 445 | PRODUCT_NAME = "$(TARGET_NAME)"; 446 | WRAPPER_EXTENSION = app; 447 | }; 448 | name = Release; 449 | }; 450 | /* End XCBuildConfiguration section */ 451 | 452 | /* Begin XCConfigurationList section */ 453 | 05A4D23D163C2F4000309444 /* Build configuration list for PBXProject "StackExample" */ = { 454 | isa = XCConfigurationList; 455 | buildConfigurations = ( 456 | 05A4D25F163C2F4000309444 /* Debug */, 457 | 05A4D260163C2F4000309444 /* Release */, 458 | ); 459 | defaultConfigurationIsVisible = 0; 460 | defaultConfigurationName = Release; 461 | }; 462 | 05A4D261163C2F4000309444 /* Build configuration list for PBXNativeTarget "StackExample" */ = { 463 | isa = XCConfigurationList; 464 | buildConfigurations = ( 465 | 05A4D262163C2F4000309444 /* Debug */, 466 | 05A4D263163C2F4000309444 /* Release */, 467 | ); 468 | defaultConfigurationIsVisible = 0; 469 | defaultConfigurationName = Release; 470 | }; 471 | /* End XCConfigurationList section */ 472 | }; 473 | rootObject = 05A4D23A163C2F4000309444 /* Project object */; 474 | } 475 | -------------------------------------------------------------------------------- /Examples/StackExample/StackExample/ChildTestViewController.h: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Spotify 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // File created by Joachim Bengtsson on 2012-10-27. 16 | 17 | #import 18 | 19 | @interface ChildTestViewController : UIViewController 20 | - (id)init; 21 | @end 22 | -------------------------------------------------------------------------------- /Examples/StackExample/StackExample/ChildTestViewController.m: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Spotify 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // File created by Joachim Bengtsson on 2012-10-27. 16 | 17 | #import "ChildTestViewController.h" 18 | #import 19 | 20 | @implementation ChildTestViewController 21 | 22 | - (id)init 23 | { 24 | if (!(self = [super init])) 25 | return nil; 26 | 27 | self.stackedNavigationController.tabBarItem.badgeValue = @"0"; 28 | 29 | return self; 30 | } 31 | 32 | - (IBAction)test:(id)sender 33 | { 34 | [self.stackedNavigationController pushViewController:[ChildTestViewController new] onTopOf:self animated:YES]; 35 | self.stackedNavigationController.tabBarItem.badgeValue = [NSString stringWithFormat:@"%d", self.stackedNavigationController.viewControllers.count]; 36 | } 37 | 38 | - (SPStackedNavigationPageSize)stackedNavigationPageSize; 39 | { 40 | return [[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad ? kStackedPageHalfSize : kStackedPageFullSize; 41 | } 42 | 43 | @end 44 | -------------------------------------------------------------------------------- /Examples/StackExample/StackExample/ChildTestViewController.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1536 5 | 12C54 6 | 2840 7 | 1187.34 8 | 625.00 9 | 10 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 11 | 1926 12 | 13 | 14 | IBProxyObject 15 | IBUIButton 16 | IBUILabel 17 | IBUIView 18 | 19 | 20 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 21 | 22 | 23 | PluginDependencyRecalculationVersion 24 | 25 | 26 | 27 | 28 | IBFilesOwner 29 | IBIPadFramework 30 | 31 | 32 | IBFirstResponder 33 | IBIPadFramework 34 | 35 | 36 | 37 | 292 38 | 39 | 40 | 41 | 292 42 | 43 | 44 | 45 | 292 46 | {{20, 35}, {432, 44}} 47 | 48 | 49 | 50 | _NS:9 51 | NO 52 | YES 53 | 7 54 | NO 55 | IBIPadFramework 56 | Child view controller! 57 | 58 | 1 59 | MCAwIDAAA 60 | darkTextColor 61 | 62 | 63 | 0 64 | 1 65 | 66 | 1 67 | 24 68 | 69 | 70 | Helvetica 71 | 24 72 | 16 73 | 74 | NO 75 | 76 | 77 | {472, 114} 78 | 79 | 80 | 81 | _NS:9 82 | 83 | 3 84 | MQA 85 | 86 | 2 87 | 88 | 89 | IBIPadFramework 90 | 91 | 92 | 93 | 292 94 | {{0, 114}, {472, 1}} 95 | 96 | 97 | 98 | _NS:9 99 | 100 | 3 101 | MC43OTIxODExOTk2AA 102 | 103 | IBIPadFramework 104 | 105 | 106 | 107 | 292 108 | {{180, 172}, {99, 44}} 109 | 110 | 111 | 112 | _NS:9 113 | NO 114 | IBIPadFramework 115 | 0 116 | 0 117 | 1 118 | Test again 119 | 120 | 3 121 | MQA 122 | 123 | 124 | 1 125 | MC4xOTYwNzg0MzQ2IDAuMzA5ODAzOTMyOSAwLjUyMTU2ODY1NgA 126 | 127 | 128 | 3 129 | MC41AA 130 | 131 | 132 | 2 133 | 15 134 | 135 | 136 | Helvetica-Bold 137 | 15 138 | 16 139 | 140 | 141 | 142 | {472, 1004} 143 | 144 | 145 | 146 | 147 | 2 148 | MC45MjU0OTAyNjAxIDAuOTI1NDkwMjYwMSAwLjkwOTgwMzk4NjUAA 149 | 150 | NO 151 | IBIPadFramework 152 | 153 | 154 | 155 | 156 | 157 | 158 | view 159 | 160 | 161 | 162 | 3 163 | 164 | 165 | 166 | test: 167 | 168 | 169 | 7 170 | 171 | 9 172 | 173 | 174 | 175 | 176 | 177 | 0 178 | 179 | 180 | 181 | 182 | 183 | -1 184 | 185 | 186 | File's Owner 187 | 188 | 189 | -2 190 | 191 | 192 | 193 | 194 | 2 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 8 205 | 206 | 207 | 208 | 209 | 10 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 11 218 | 219 | 220 | 221 | 222 | 4 223 | 224 | 225 | 226 | 227 | 228 | 229 | ChildTestViewController 230 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 231 | UIResponder 232 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 233 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 234 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 235 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 236 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 237 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 238 | 239 | 240 | 241 | 242 | 243 | 11 244 | 245 | 246 | 247 | 248 | ChildTestViewController 249 | UIViewController 250 | 251 | test: 252 | id 253 | 254 | 255 | test: 256 | 257 | test: 258 | id 259 | 260 | 261 | 262 | IBProjectSource 263 | ./Classes/ChildTestViewController.h 264 | 265 | 266 | 267 | 268 | 0 269 | IBIPadFramework 270 | YES 271 | 3 272 | 1926 273 | 274 | 275 | -------------------------------------------------------------------------------- /Examples/StackExample/StackExample/Default-568h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spotify/SPStackedNav/b53830820b6302f6d7ab43e9743853ba5622eca1/Examples/StackExample/StackExample/Default-568h@2x.png -------------------------------------------------------------------------------- /Examples/StackExample/StackExample/Default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spotify/SPStackedNav/b53830820b6302f6d7ab43e9743853ba5622eca1/Examples/StackExample/StackExample/Default.png -------------------------------------------------------------------------------- /Examples/StackExample/StackExample/Default@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spotify/SPStackedNav/b53830820b6302f6d7ab43e9743853ba5622eca1/Examples/StackExample/StackExample/Default@2x.png -------------------------------------------------------------------------------- /Examples/StackExample/StackExample/RootTestViewController.h: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Spotify 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // File created by Joachim Bengtsson on 2012-10-27. 16 | 17 | #import 18 | 19 | @interface RootTestViewController : UIViewController 20 | - (id)init; 21 | - (IBAction)test:(id)sender; 22 | @end 23 | -------------------------------------------------------------------------------- /Examples/StackExample/StackExample/RootTestViewController.m: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Spotify 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // File created by Joachim Bengtsson on 2012-10-27. 16 | 17 | #import "RootTestViewController.h" 18 | #import 19 | #import "ChildTestViewController.h" 20 | 21 | @implementation RootTestViewController 22 | 23 | - (id)init 24 | { 25 | if (!(self = [super init])) 26 | return nil; 27 | 28 | 29 | return self; 30 | } 31 | 32 | - (SPStackedNavigationPageSize)stackedNavigationPageSize 33 | { 34 | return kStackedPageFullSize; 35 | } 36 | 37 | - (IBAction)test:(id)sender 38 | { 39 | [self.stackedNavigationController pushViewController:[ChildTestViewController new] onTopOf:self animated:YES]; 40 | } 41 | 42 | @end 43 | -------------------------------------------------------------------------------- /Examples/StackExample/StackExample/RootTestViewController.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1536 5 | 12C54 6 | 2840 7 | 1187.34 8 | 625.00 9 | 10 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 11 | 1926 12 | 13 | 14 | IBProxyObject 15 | IBUIButton 16 | IBUILabel 17 | IBUIView 18 | 19 | 20 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 21 | 22 | 23 | PluginDependencyRecalculationVersion 24 | 25 | 26 | 27 | 28 | IBFilesOwner 29 | IBIPadFramework 30 | 31 | 32 | IBFirstResponder 33 | IBIPadFramework 34 | 35 | 36 | 37 | 292 38 | 39 | 40 | 41 | 293 42 | {{357, 172}, {55, 44}} 43 | 44 | 45 | 46 | _NS:9 47 | NO 48 | IBIPadFramework 49 | 0 50 | 0 51 | 1 52 | Test 53 | 54 | 3 55 | MQA 56 | 57 | 58 | 1 59 | MC4xOTYwNzg0MzQ2IDAuMzA5ODAzOTMyOSAwLjUyMTU2ODY1NgA 60 | 61 | 62 | 3 63 | MC41AA 64 | 65 | 66 | 2 67 | 15 68 | 69 | 70 | Helvetica-Bold 71 | 15 72 | 16 73 | 74 | 75 | 76 | 77 | 290 78 | 79 | 80 | 81 | 290 82 | {{0, 35}, {768, 44}} 83 | 84 | 85 | 86 | _NS:9 87 | NO 88 | YES 89 | 7 90 | NO 91 | IBIPadFramework 92 | Root view controller! 93 | 94 | 1 95 | MCAwIDAAA 96 | darkTextColor 97 | 98 | 99 | 0 100 | 1 101 | 102 | 1 103 | 24 104 | 105 | 106 | Helvetica 107 | 24 108 | 16 109 | 110 | NO 111 | 112 | 113 | {768, 114} 114 | 115 | 116 | 117 | _NS:9 118 | 119 | 3 120 | MQA 121 | 122 | 2 123 | 124 | 125 | IBIPadFramework 126 | 127 | 128 | 129 | 292 130 | {{0, 114}, {472, 1}} 131 | 132 | 133 | 134 | _NS:9 135 | 136 | 3 137 | MC43OTIxODExOTk2AA 138 | 139 | IBIPadFramework 140 | 141 | 142 | {{0, 20}, {768, 1004}} 143 | 144 | 145 | 146 | 147 | 2 148 | MC45MjU0OTAyNjAxIDAuOTI1NDkwMjYwMSAwLjkwOTgwMzk4NjUAA 149 | 150 | NO 151 | 152 | 2 153 | 154 | IBIPadFramework 155 | 156 | 157 | 158 | 159 | 160 | 161 | view 162 | 163 | 164 | 165 | 3 166 | 167 | 168 | 169 | test: 170 | 171 | 172 | 7 173 | 174 | 7 175 | 176 | 177 | 178 | 179 | 180 | 0 181 | 182 | 183 | 184 | 185 | 186 | -1 187 | 188 | 189 | File's Owner 190 | 191 | 192 | -2 193 | 194 | 195 | 196 | 197 | 2 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 4 208 | 209 | 210 | 211 | 212 | 8 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 9 221 | 222 | 223 | 224 | 225 | 10 226 | 227 | 228 | 229 | 230 | 231 | 232 | RootTestViewController 233 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 234 | UIResponder 235 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 236 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 237 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 238 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 239 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 240 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 241 | 242 | 243 | 244 | 245 | 246 | 10 247 | 248 | 249 | 250 | 251 | RootTestViewController 252 | UIViewController 253 | 254 | test: 255 | id 256 | 257 | 258 | test: 259 | 260 | test: 261 | id 262 | 263 | 264 | 265 | IBProjectSource 266 | ./Classes/RootTestViewController.h 267 | 268 | 269 | 270 | 271 | 0 272 | IBIPadFramework 273 | YES 274 | 3 275 | 1926 276 | 277 | 278 | -------------------------------------------------------------------------------- /Examples/StackExample/StackExample/SPAppDelegate.h: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Spotify 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // File created by Joachim Bengtsson on 2012-10-27. 16 | 17 | #import 18 | #import 19 | 20 | @interface SPAppDelegate : UIResponder 21 | 22 | @property (strong, nonatomic) UIWindow *window; 23 | @property (strong, nonatomic) SPSideTabController *tabs; 24 | 25 | @end 26 | -------------------------------------------------------------------------------- /Examples/StackExample/StackExample/SPAppDelegate.m: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Spotify 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // File created by Joachim Bengtsson on 2012-10-27. 16 | 17 | #import "SPAppDelegate.h" 18 | #import "RootTestViewController.h" 19 | 20 | @implementation SPAppDelegate 21 | 22 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 23 | { 24 | self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; 25 | // Override point for customization after application launch. 26 | self.window.backgroundColor = [UIColor whiteColor]; 27 | 28 | self.tabs = [[SPSideTabController alloc] init]; 29 | 30 | RootTestViewController *root1 = [RootTestViewController new]; 31 | root1.title = @"Root 1"; 32 | root1.tabBarItem.image = [UIImage imageNamed:@"114-balloon"]; 33 | RootTestViewController *root2 = [RootTestViewController new]; 34 | root2.title = @"Root 2"; 35 | root2.tabBarItem.image = [UIImage imageNamed:@"185-printer"]; 36 | 37 | self.tabs.viewControllers = @[ 38 | [[SPStackedNavigationController alloc] initWithRootViewController:root1], 39 | [[SPStackedNavigationController alloc] initWithRootViewController:root2], 40 | ]; 41 | 42 | 43 | self.window.rootViewController = self.tabs; 44 | [self.window makeKeyAndVisible]; 45 | return YES; 46 | } 47 | 48 | @end 49 | -------------------------------------------------------------------------------- /Examples/StackExample/StackExample/StackExample-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | ${PRODUCT_NAME} 9 | CFBundleExecutable 10 | ${EXECUTABLE_NAME} 11 | CFBundleIdentifier 12 | com.spotify.${PRODUCT_NAME:rfc1034identifier} 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | ${PRODUCT_NAME} 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1.0 25 | LSRequiresIPhoneOS 26 | 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Examples/StackExample/StackExample/StackExample-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header for all source files of the 'StackExample' target in the 'StackExample' project 3 | // 4 | 5 | #import 6 | 7 | #ifndef __IPHONE_3_0 8 | #warning "This project uses features only available in iOS SDK 3.0 and later." 9 | #endif 10 | 11 | #ifdef __OBJC__ 12 | #import 13 | #import 14 | #endif 15 | -------------------------------------------------------------------------------- /Examples/StackExample/StackExample/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /Examples/StackExample/StackExample/main.m: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Spotify 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // File created by Joachim Bengtsson on 2012-10-27. 16 | 17 | #import 18 | 19 | #import "SPAppDelegate.h" 20 | 21 | int main(int argc, char *argv[]) 22 | { 23 | @autoreleasepool { 24 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([SPAppDelegate class])); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Examples/StackExample/icons-from-glyphish/00-Read me first - license.txt: -------------------------------------------------------------------------------- 1 | FREE ICONS BY GLYPHISH 2 | Created by Joseph Wain, 2012 3 | Web: http://glyphish.com or http://penandthink.com 4 | Twitter: @glyphish or @jpwain 5 | 6 | Created by Joseph Wain and downloaded from http://glyphish.com 7 | 8 | This work is licensed under the Creative Commons Attribution 3.0 United States License. To view a copy of this license, visit http://creativecommons.org/licenses/by/3.0/us/ 9 | 10 | You are free to share and to remix it remix under the following conditions: 11 | 12 | * You must attribute the work in the manner specified by the author (SEE BELOW). 13 | 14 | * For any reuse or distribution, you must make clear to others the license terms of this work. 15 | 16 | * The above conditions can be waived if you get permission from the copyright holder 17 | (send me an email to discuss). 18 | 19 | You're free to use these icons for commercial and non-commercial purposes, for yourself, your company and your clients, and to edit, remix and otherwise modify them, as long as clear attribution is provided. You may not sell or redistribute the icons themselves as icons. 20 | 21 | Additionally, you may not use them in a way that encourages downstream distribution -- no templates or skins or theme kits or similar uses, no app-building tools or similar. (The person using the theme or template might not know where the icons came from and thus wouldn't be following the license.) This specifically prohibits the use in any kind of "app builder" tool or platform, especially where the icons are provided as choices to people using the tool to create apps. 22 | 23 | ATTRIBUTION -- A note on your app's website, like "Icons by Glyphish" or similar, plus a link back to glyphish.com, is the preferred form of attribution. For questions about attribution, contact me via the Glyphish website. 24 | 25 | USE WITHOUT ATTRIBUTION -- If attribution is not possible, workable or desirable for your application, get in touch to discuss other options. Or, get Glyphish Pro which does not require attribution. 26 | 27 | GET MORE ICONS! -- Get Glyphish Pro! 400 icons, each in two sizes and two colors, totally awesome. Available from http://glyphish.com, or purchase directly here: http://pul.ly/b/19959 -------------------------------------------------------------------------------- /Examples/StackExample/icons-from-glyphish/114-balloon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spotify/SPStackedNav/b53830820b6302f6d7ab43e9743853ba5622eca1/Examples/StackExample/icons-from-glyphish/114-balloon.png -------------------------------------------------------------------------------- /Examples/StackExample/icons-from-glyphish/185-printer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spotify/SPStackedNav/b53830820b6302f6d7ab43e9743853ba5622eca1/Examples/StackExample/icons-from-glyphish/185-printer.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | 3 | Version 2.0, January 2004 4 | 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 16 | 17 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 18 | 19 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. 20 | 21 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. 22 | 23 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). 24 | 25 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. 26 | 27 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." 28 | 29 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 30 | 31 | 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 32 | 33 | 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 34 | 35 | 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: 36 | 37 | You must give any other recipients of the Work or Derivative Works a copy of this License; and 38 | You must cause any modified files to carry prominent notices stating that You changed the files; and 39 | You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and 40 | If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. 41 | 42 | You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 43 | 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 44 | 45 | 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 46 | 47 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 48 | 49 | 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 50 | 51 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. 52 | 53 | END OF TERMS AND CONDITIONS 54 | 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/) 2 | 3 | # :warning: Deprecated :warning: 4 | 5 | This library is deprecated. 6 | 7 | SPStackedNav 8 | ============ 9 | Joachim Bengtsson 10 | 11 | 12 | 13 | SPStackedNavigationController 14 | ----------------------------- 15 | 16 | SPStackedNavigationController is a UINavigationController drop-in replacement, which represents its content in stacks of panes, rather than one at a time. This interface trend was started by Loren Brichter in Tweetie for iPad, and has spread to many apps in many variations since. 17 | 18 | There are two main advantages to this approach: 19 | 20 | * You can display two pieces of main content at once, allowing you to navigate in one while using content in the other. 21 | * Navigation is direct instead of indirect, which is faster and more intuitive to use. You actually grab the UI and *pull* it to where you want it. In contrast, a standard navigation controller requires you to find and tap a button with an abstract "back" concept. 22 | 23 | The main drawback is that you should no longer use horizontal gestures, as they will interfere with navigation, or the other way around. 24 | 25 | At Spotify, we use this style for navigation in our iPad app. We are very proud of the outcome, and are contributing it back to the community, in hopes that others will find it as useful as we do. This code has been used for several years at Spotify and should be very stable. 26 | 27 | In our implementation, a page can either be "full size" and thus cover the whole width of the parent container (which we use for the root view controllers in our stacks), or half-size (exactly two will fit in landscape, or one and a half in portrait). 28 | 29 | SPSideTabController 30 | ------------------- 31 | 32 | In addition, SPSideTabController is a drop-in replacement for UITabBarController, but with tabs along the left side rather than along the bottom. This is one of the UIs that are commonly combined with a stacked navigation. 33 | 34 | Extra tab bar items can be added along the bottom (e g for "Settings"), and the whole bottom of the screen can have an attachment, which we use to show the currently playing track in Spotify. 35 | 36 | Usage Instructions 37 | ------------------ 38 | 39 | 1. Pull in "include", "Sources" and "Graphics" into your main project. 40 | 2. Go to your project settings, then Build Settings for your app target, and change "Header Search Paths" to include and "{your path to SPStackedNav}/include". 41 | 3. #import either from your prefix header, or the source file where you want to use these classes. 42 | 43 | See Examples/StackExample for some example usage. 44 | 45 | Version History 46 | --------------- 47 | 48 | 1.0: Initial release 49 | -------------------------------------------------------------------------------- /Resources/SPSideTabBar-button-overlay+normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spotify/SPStackedNav/b53830820b6302f6d7ab43e9743853ba5622eca1/Resources/SPSideTabBar-button-overlay+normal.png -------------------------------------------------------------------------------- /Resources/SPSideTabBar-button-overlay+selected+pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spotify/SPStackedNav/b53830820b6302f6d7ab43e9743853ba5622eca1/Resources/SPSideTabBar-button-overlay+selected+pressed.png -------------------------------------------------------------------------------- /Resources/SPSideTabBar-button-overlay+selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spotify/SPStackedNav/b53830820b6302f6d7ab43e9743853ba5622eca1/Resources/SPSideTabBar-button-overlay+selected.png -------------------------------------------------------------------------------- /Resources/backgroundTexture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spotify/SPStackedNav/b53830820b6302f6d7ab43e9743853ba5622eca1/Resources/backgroundTexture.png -------------------------------------------------------------------------------- /Resources/backgroundTexture@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spotify/SPStackedNav/b53830820b6302f6d7ab43e9743853ba5622eca1/Resources/backgroundTexture@2x.png -------------------------------------------------------------------------------- /Resources/bg-tb@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spotify/SPStackedNav/b53830820b6302f6d7ab43e9743853ba5622eca1/Resources/bg-tb@2x~ipad.png -------------------------------------------------------------------------------- /Resources/bg-tb~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spotify/SPStackedNav/b53830820b6302f6d7ab43e9743853ba5622eca1/Resources/bg-tb~ipad.png -------------------------------------------------------------------------------- /Resources/stackShadow-right~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spotify/SPStackedNav/b53830820b6302f6d7ab43e9743853ba5622eca1/Resources/stackShadow-right~ipad.png -------------------------------------------------------------------------------- /Resources/stackShadow-right~ipad@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spotify/SPStackedNav/b53830820b6302f6d7ab43e9743853ba5622eca1/Resources/stackShadow-right~ipad@2x.png -------------------------------------------------------------------------------- /Resources/stackShadow~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spotify/SPStackedNav/b53830820b6302f6d7ab43e9743853ba5622eca1/Resources/stackShadow~ipad.png -------------------------------------------------------------------------------- /Resources/stackShadow~ipad@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spotify/SPStackedNav/b53830820b6302f6d7ab43e9743853ba5622eca1/Resources/stackShadow~ipad@2x.png -------------------------------------------------------------------------------- /SPStackedNav.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | 3 | s.name = "SPStackedNav" 4 | s.version = "1.0.0" 5 | s.summary = "UINavigationController drop-in replacement with stacks of panes, like Spotify or old Twitter." 6 | 7 | s.description = <<-DESC 8 | SPStackedNavigationController is a UINavigationController drop-in replacement, 9 | which represents its content in stacks of panes, rather than one at a time. 10 | This interface trend was started by Loren Brichter in Tweetie for iPad, and 11 | has spread to many apps in many variations since. 12 | 13 | There are two main advantages to this approach: 14 | 15 | * You can display two pieces of main content at once, allowing you to 16 | navigate in one while using content in the other. 17 | * Navigation is direct instead of indirect, which is faster and more 18 | intuitive to use. You actually grab the UI and *pull* it to where you want 19 | it. In contrast, a standard navigation controller requires you to find and 20 | tap a button with an abstract "back" concept. 21 | 22 | The main drawback is that you should no longer use horizontal gestures, as 23 | they will interfere with navigation, or the other way around. 24 | DESC 25 | 26 | s.homepage = "http://github.com/spotify/SPStackedNav" 27 | s.screenshots = "http://f.cl.ly/items/2H2p0b1H3A2K3T0E040u/mzl.lmmfkkux.480x480-75.jpg" 28 | 29 | s.license = { :type => 'MIT', :file => 'LICENSE' } 30 | 31 | s.author = { "Joachim Bengtsson" => "nevyn@spotify.com" } 32 | s.social_media_url = "http://twitter.com/nevyn" 33 | 34 | s.platform = :ios, '5.0' 35 | 36 | s.source = { :git => "https://github.com/spotify/SPStackedNav.git", :tag => "1.0.0" } 37 | s.source_files = 'Classes', 'Sources/*.{h,m}' 38 | s.public_header_files = 'include/SPStackedNav/*.h' 39 | s.preserve_path = "include" 40 | 41 | s.requires_arc = true 42 | end 43 | -------------------------------------------------------------------------------- /Sources/SPBadgeView.h: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Spotify 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // File created by Joachim Bengtsson on 2010-04-14. 16 | 17 | #import 18 | 19 | 20 | @interface SPBadgeView : UIControl 21 | @property(nonatomic) NSInteger count; 22 | @property(nonatomic, copy) NSString *text; 23 | @property (nonatomic, retain) UIImage *backgroundImage; 24 | @property (nonatomic, retain) UIImage *highlightedBackgroundImage; 25 | @property (nonatomic, retain) UIFont *font; 26 | @property (nonatomic, retain) UIColor *textColor; 27 | @property (nonatomic, assign) BOOL textTransparentOnHighlightedAndSelected; 28 | 29 | @end 30 | 31 | -------------------------------------------------------------------------------- /Sources/SPBadgeView.m: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Spotify 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // File created by Joachim Bengtsson on 2010-04-14. 16 | 17 | #import "SPBadgeView.h" 18 | 19 | 20 | @implementation SPBadgeView 21 | + (UIFont*)badgeFont 22 | { 23 | return [UIFont boldSystemFontOfSize:15]; 24 | } 25 | 26 | - (id)initWithFrame:(CGRect)frame 27 | { 28 | if (!(self = [super initWithFrame:frame])) 29 | return nil; 30 | 31 | self.count = 0; 32 | self.opaque = NO; 33 | self.font = SPBadgeView.badgeFont; 34 | self.textColor = [UIColor blackColor]; 35 | self.textTransparentOnHighlightedAndSelected = NO; 36 | self.userInteractionEnabled = NO; 37 | 38 | return self; 39 | } 40 | 41 | - (void)setCount:(NSInteger)count 42 | { 43 | _count = count; 44 | self.text = [NSString stringWithFormat:@"%d", count]; 45 | } 46 | 47 | - (void)setText:(NSString *)text 48 | { 49 | _text = [text copy]; 50 | // Resize to fit the badge 51 | CGRect r = self.frame; 52 | r.size = [self.text sizeWithFont:self.font]; 53 | r.size.width += 16; 54 | r.size.height += 2; 55 | 56 | if (self.backgroundImage){ 57 | if (r.size.width < self.backgroundImage.size.width) 58 | r.size.width = self.backgroundImage.size.width; 59 | if (r.size.height < self.backgroundImage.size.height) 60 | r.size.height = self.backgroundImage.size.height; 61 | } 62 | self.frame = CGRectIntegral(r); 63 | 64 | self.hidden = !text.length > 0; 65 | 66 | [self.superview setNeedsLayout]; 67 | [self setNeedsDisplay]; 68 | } 69 | 70 | - (void)drawRect:(CGRect)rect 71 | { 72 | CGContextRef ctx = UIGraphicsGetCurrentContext(); 73 | 74 | float r = self.bounds.size.height /2.; 75 | UIImage *image = self.backgroundImage; 76 | if (self.highlighted || self.selected){ 77 | CGContextSetFillColorWithColor(ctx, [[UIColor whiteColor] CGColor]); 78 | image = self.highlightedBackgroundImage; 79 | } else { 80 | CGContextSetFillColor(ctx, (CGFloat[4]){0.369, 0.369, 0.369, 1}); 81 | } 82 | 83 | if (image){ 84 | [image drawInRect:self.bounds]; 85 | } else { 86 | CGContextBeginPath(ctx); 87 | 88 | CGContextAddArc(ctx, r, r, r, M_PI / 2 , 3 * M_PI / 2, NO); 89 | CGContextAddArc(ctx, self.bounds.size.width - r, r, r, 3 * M_PI / 2, M_PI / 2, NO); 90 | CGContextClosePath(ctx); 91 | CGContextFillPath(ctx); 92 | } 93 | 94 | UIColor *textColor = self.textColor; 95 | if ((self.highlighted || self.selected) && 96 | self.textTransparentOnHighlightedAndSelected) { 97 | CGContextSetBlendMode(ctx, kCGBlendModeDestinationOut); 98 | textColor = [UIColor blackColor]; 99 | } 100 | 101 | CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); 102 | CGContextSetFillColorSpace(ctx, colorSpace); 103 | [textColor set]; 104 | CGColorSpaceRelease(colorSpace); 105 | 106 | CGRect textRect = self.bounds; 107 | CGSize textSize = [self.text sizeWithFont:self.font]; 108 | textRect.origin.y = textRect.size.height / 2 - textSize.height / 2; 109 | [self.text drawInRect:textRect 110 | withFont:self.font 111 | lineBreakMode:NSLineBreakByClipping 112 | alignment:NSTextAlignmentCenter]; 113 | } 114 | 115 | @end 116 | 117 | 118 | -------------------------------------------------------------------------------- /Sources/SPFunctional-mini.h: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Spotify 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #import 16 | /*! 17 | Minimal version of SPFunctional from http://github.com/nevyn/SPSuccinct, 18 | adding some functional programming methods to NSArray. 19 | */ 20 | @interface NSArray (SPStackedFunctional) 21 | -(NSArray*)spstacked_filter:(BOOL(^)(id obj))predicate; 22 | -(id)spstacked_any:(BOOL(^)(id obj))iterator; 23 | @end 24 | -------------------------------------------------------------------------------- /Sources/SPFunctional-mini.m: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Spotify 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #import "SPFunctional-mini.h" 16 | 17 | @implementation NSArray (SPStackedFunctional) 18 | -(NSArray*)spstacked_filter:(BOOL(^)(id obj))predicate; 19 | { 20 | return [self objectsAtIndexes:[self indexesOfObjectsPassingTest:^(id obj, NSUInteger idx, BOOL *stop) { 21 | return predicate(obj); 22 | }]]; 23 | } 24 | 25 | -(id)spstacked_any:(BOOL(^)(id obj))iterator; 26 | { 27 | for(id obj in self) 28 | if (iterator(obj)) 29 | return obj; 30 | return NULL; 31 | } 32 | @end 33 | -------------------------------------------------------------------------------- /Sources/SPSeparatorView.h: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Spotify 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | enum SPSeparatorStyle 16 | { 17 | SPSeparatorStyleSingleLine = 0, // { 0, 0, 0, 0.25 } 18 | SPSeparatorStyleSingleLineEtched // [ { 0, 0, 0, 0.25 }, { 1, 1, 1, 1 } ] 19 | }; 20 | typedef enum SPSeparatorStyle SPSeparatorStyle; 21 | 22 | @interface SPSeparatorView : UIView 23 | @property (nonatomic) SPSeparatorStyle style; 24 | 25 | // one color if single line, two colors if etched single line. 26 | // see SPSeparatorStyle for default values (if -colors is nil). 27 | - (NSArray*)colors; 28 | - (void)setColors:(NSArray*)colors; 29 | @end 30 | -------------------------------------------------------------------------------- /Sources/SPSeparatorView.m: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Spotify 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #import "SPSeparatorView.h" 16 | 17 | @interface SPSeparatorView () 18 | { 19 | SPSeparatorStyle _style; 20 | NSArray * _colors; 21 | } 22 | @end 23 | 24 | @implementation SPSeparatorView 25 | 26 | - (void)commonSetup 27 | { 28 | [self setBackgroundColor:[UIColor clearColor]]; 29 | [self setOpaque:NO]; 30 | } 31 | 32 | - (id)init 33 | { 34 | if (!(self = [super init])) 35 | return nil; 36 | 37 | [self commonSetup]; 38 | 39 | return self; 40 | } 41 | 42 | - (id)initWithFrame:(CGRect)frame 43 | { 44 | if (!(self = [super initWithFrame:frame])) 45 | return nil; 46 | 47 | [self commonSetup]; 48 | 49 | return self; 50 | } 51 | 52 | - (id)initWithCoder:(NSCoder *)aDecoder 53 | { 54 | if (!(self = [super initWithCoder:aDecoder])) 55 | return nil; 56 | 57 | [self commonSetup]; 58 | 59 | return self; 60 | } 61 | 62 | 63 | #pragma mark Style 64 | 65 | - (SPSeparatorStyle)style { return _style; } 66 | - (void)setStyle:(SPSeparatorStyle)style 67 | { 68 | if (_style != style) 69 | { 70 | _style = style; 71 | [self setNeedsDisplay]; 72 | } 73 | } 74 | 75 | 76 | - (NSArray *)colors { return _colors; } 77 | - (void)setColors:(NSArray *)colors 78 | { 79 | if (![_colors isEqualToArray:colors]) 80 | { 81 | _colors = [colors copy]; 82 | [self setNeedsDisplay]; 83 | } 84 | } 85 | 86 | 87 | #pragma mark Drawing 88 | 89 | - (NSArray*)_actualColors 90 | { 91 | NSMutableArray *colors = [NSMutableArray array]; 92 | if ([self colors]) 93 | [colors addObjectsFromArray:[self colors]]; 94 | if ([colors count] == 0) 95 | [colors addObject:[UIColor colorWithWhite:0.0 alpha:0.25]]; 96 | if ([self style] == SPSeparatorStyleSingleLineEtched && [colors count] < 2) 97 | [colors addObject:[UIColor colorWithWhite:1.0 alpha:1.0]]; 98 | // you should never return a mutable instance, but who cares here? 99 | return colors; 100 | } 101 | 102 | - (void)drawRect:(CGRect)rect 103 | { 104 | NSArray *colors = [self _actualColors]; 105 | 106 | if ([self style] == SPSeparatorStyleSingleLine) 107 | { 108 | [colors[0] setFill]; 109 | UIRectFill(rect); 110 | } 111 | else 112 | { 113 | CGRect frame = rect; 114 | if (frame.size.width > frame.size.height) 115 | { 116 | frame.size.height /= 2; 117 | [colors[0] setFill]; 118 | UIRectFill(CGRectIntegral(frame)); 119 | frame.origin.y += frame.size.height; 120 | [colors[1] setFill]; 121 | UIRectFill(CGRectIntegral(frame)); 122 | } 123 | else 124 | { 125 | frame.size.width /= 2; 126 | [colors[0] setFill]; 127 | UIRectFill(CGRectIntegral(frame)); 128 | frame.origin.x += frame.size.width; 129 | [colors[1] setFill]; 130 | UIRectFill(CGRectIntegral(frame)); 131 | } 132 | } 133 | } 134 | 135 | @end 136 | -------------------------------------------------------------------------------- /Sources/SPSideTabBar.m: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Spotify 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #import 16 | #import "SPBadgeView.h" 17 | #import "SPSideTabItemButton.h" 18 | #import "UIImage+SPTabBarImage.h" 19 | #import "SPSideTabController.h" 20 | 21 | @interface SPSideTabBar () 22 | { 23 | UIColor * _backgroundPattern; 24 | } 25 | @property(nonatomic,retain) NSArray *itemButtons; 26 | @property(nonatomic,retain) NSArray *additionalItemButtons; 27 | - (void)itemButtonWasTapped:(SPSideTabItemButton*)button; 28 | @end 29 | 30 | @interface SPSideTabBadgeView : SPBadgeView 31 | + (SPSideTabBadgeView *) badgeViewWithFrame:(CGRect)frame; 32 | - (void)bindToTabItem:(UITabBarItem *)item; 33 | @end 34 | 35 | 36 | @implementation SPSideTabBar 37 | 38 | - (void)commonSetup 39 | { 40 | UIImage *bgI = [UIImage imageNamed:@"bg-tb"]; 41 | _backgroundPattern = [UIColor colorWithPatternImage:bgI]; 42 | 43 | CGRect r = self.frame; 44 | if (r.size.width > [bgI size].width) { 45 | r.size.width = [bgI size].width; 46 | self.frame = r; 47 | } 48 | } 49 | 50 | - (id)initWithFrame:(CGRect)r 51 | { 52 | if (!(self = [super initWithFrame:r])) 53 | return nil; 54 | 55 | [self commonSetup]; 56 | 57 | return self; 58 | } 59 | 60 | - (id)initWithCoder:(NSCoder*)decoder 61 | { 62 | if (!(self = [super initWithCoder:decoder])) 63 | return nil; 64 | 65 | [self commonSetup]; 66 | 67 | return self; 68 | } 69 | 70 | - (void)drawRect:(CGRect)rect 71 | { 72 | // we are drawing the background manually to avoid blending the layer. 73 | CGContextRef context = UIGraphicsGetCurrentContext(); 74 | CGContextSaveGState(context); 75 | { 76 | UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:[self bounds] 77 | byRoundingCorners:UIRectCornerTopLeft 78 | cornerRadii:CGSizeMake(5, 5)]; 79 | CGContextAddPath(context, [path CGPath]); 80 | CGContextClip(context); 81 | 82 | [_backgroundPattern setFill]; 83 | CGContextFillRect(context, rect); 84 | } CGContextRestoreGState(context); 85 | } 86 | 87 | - (UIImage*)imageForState:(UIControlState)state inItem:(UITabBarItem*)item 88 | { 89 | UIImage *image = nil; 90 | 91 | if (item.image) { 92 | if (state & (UIControlStateSelected)) 93 | return [item.image sp_selectedImageForTabBar]; 94 | if (state & (UIControlStateSelected|UIControlStateHighlighted)) 95 | return [item.image sp_selectedAndHighlightedImageForTabBar]; 96 | return [item.image sp_imageForTabBar]; 97 | } 98 | 99 | SPTabBarItem *spItem = (SPTabBarItem*)item; 100 | if (![item isKindOfClass:[SPTabBarItem class]]) 101 | return nil; 102 | 103 | NSString *imageName = spItem.imageName; 104 | if (!imageName) 105 | return nil; 106 | 107 | if (state == (UIControlStateSelected|UIControlStateHighlighted) && (image = [UIImage imageNamed:[NSString stringWithFormat:@"%@-%@-tb", imageName, @"selected+pressed"]])) 108 | return image; 109 | if (state & UIControlStateHighlighted && (image = [UIImage imageNamed:[NSString stringWithFormat:@"%@-%@-tb", imageName, @"pressed"]])) 110 | return image; 111 | if (state & UIControlStateSelected && (image = [UIImage imageNamed:[NSString stringWithFormat:@"%@-%@-tb", imageName, @"selected"]])) 112 | return image; 113 | 114 | return [UIImage imageNamed:[NSString stringWithFormat:@"%@-tb", imageName]]; 115 | } 116 | 117 | - (UIView*)buttonForItem:(UITabBarItem*)item withFrame:(CGRect)pen 118 | { 119 | if ([item isKindOfClass:[SPTabBarItem class]] && [(SPTabBarItem*)item view]) { 120 | UIView *view = [(SPTabBarItem*)item view]; 121 | [view setFrame:pen]; 122 | return view; 123 | } 124 | 125 | SPSideTabItemButton *b = [[SPSideTabItemButton alloc] initWithFrame:pen]; 126 | 127 | [b addTarget:self action:@selector(itemButtonWasTapped:) forControlEvents:UIControlEventTouchUpInside]; 128 | [b setTitle:[item title] forState:UIControlStateNormal]; 129 | 130 | [b setImage:[self imageForState:UIControlStateNormal inItem:item] forState:UIControlStateNormal]; 131 | [b setImage:[self imageForState:UIControlStateSelected inItem:item] forState:UIControlStateSelected]; 132 | [b setImage:[self imageForState:UIControlStateHighlighted inItem:item] forState:UIControlStateHighlighted]; 133 | [b setImage:[self imageForState:UIControlStateSelected|UIControlStateHighlighted inItem:item] forState:UIControlStateSelected|UIControlStateHighlighted]; 134 | 135 | SPSideTabBadgeView *badge = [SPSideTabBadgeView badgeViewWithFrame:CGRectMake(0, 0, 0, 0)]; 136 | badge.center = CGPointMake(50, 6); 137 | [badge bindToTabItem:item]; 138 | b.badgeView = badge; 139 | 140 | return b; 141 | } 142 | 143 | - (void)setItems:(NSArray*)items 144 | { 145 | if ([items isEqual:_items]) return; 146 | 147 | self.selectedItem = nil; 148 | 149 | _items = [items copy]; 150 | 151 | for(UIView *b in _itemButtons) [b removeFromSuperview]; 152 | self.itemButtons = nil; 153 | 154 | if (_items) { 155 | NSMutableArray *itemButtons = [NSMutableArray array]; 156 | CGRect pen = CGRectMake(0, 10, 80, 70); 157 | for(UITabBarItem *item in _items) { 158 | UIView *b = [self buttonForItem:item withFrame:pen]; 159 | [itemButtons addObject:b]; 160 | [self addSubview:b]; 161 | pen.origin.y += pen.size.height + 10; 162 | } 163 | self.itemButtons = itemButtons; 164 | } 165 | } 166 | static const int kIsAdditionalItem = 1; 167 | - (void)setAdditionalItems:(NSArray*)moreItems 168 | { 169 | if ([moreItems isEqual:_additionalItems]) return; 170 | 171 | _additionalItems = [moreItems copy]; 172 | 173 | for(UIView *b in _additionalItemButtons) [b removeFromSuperview]; 174 | self.additionalItemButtons = nil; 175 | 176 | if (_additionalItems) { 177 | NSMutableArray *itemButtons = [NSMutableArray array]; 178 | CGRect pen = CGRectMake(0, self.frame.size.height-70-10, 80, 70); 179 | for(UITabBarItem *item in _additionalItems) { 180 | UIView *b = [self buttonForItem:item withFrame:pen]; 181 | b.tag = kIsAdditionalItem; 182 | b.autoresizingMask = UIViewAutoresizingFlexibleTopMargin; 183 | [itemButtons addObject:b]; 184 | [self addSubview:b]; 185 | pen.origin.y -= pen.size.height - 10; 186 | } 187 | self.additionalItemButtons = itemButtons; 188 | } 189 | } 190 | 191 | - (void)itemButtonWasTapped:(SPSideTabItemButton*)button 192 | { 193 | NSArray *items = (button.tag == kIsAdditionalItem)?_additionalItems:_items; 194 | NSArray *buttons = (button.tag == kIsAdditionalItem)?_additionalItemButtons:_itemButtons; 195 | 196 | UITabBarItem *tappedItem = items[[buttons indexOfObject:button]]; 197 | if (!_delegate) 198 | self.selectedItem = tappedItem; 199 | else 200 | [self.delegate tabBar:self didSelectItem:tappedItem]; 201 | } 202 | - (void)setSelectedItem:(UITabBarItem*)item 203 | { 204 | if (item == _selectedItem) return; 205 | 206 | if (_selectedItem) [_itemButtons[[_items indexOfObject:_selectedItem]] setSelected:NO]; 207 | 208 | _selectedItem = item; 209 | 210 | if (_selectedItem) [_itemButtons[[_items indexOfObject:_selectedItem]] setSelected:YES]; 211 | } 212 | - (void)select:(BOOL)selected additionalItem:(UITabBarItem*)item 213 | { 214 | [_additionalItemButtons[[_additionalItems indexOfObject:item]] setSelected:selected]; 215 | } 216 | - (CGRect)rectForItem:(UITabBarItem*)item 217 | { 218 | NSUInteger idx = [_items indexOfObject:item]; 219 | UIView *button = nil; 220 | if (idx != NSNotFound) { 221 | button = _itemButtons[idx]; 222 | } else { 223 | idx = [_additionalItems indexOfObject:item]; 224 | button = _additionalItemButtons[idx]; 225 | } 226 | return button.frame; 227 | } 228 | @end 229 | 230 | static void *kSPSideTabBadgeViewBadgeValueObservationContext = &kSPSideTabBadgeViewBadgeValueObservationContext; 231 | @implementation SPSideTabBadgeView 232 | { 233 | UITabBarItem *_item; 234 | } 235 | 236 | - (void)dealloc 237 | { 238 | if(_item) { 239 | [_item removeObserver:self forKeyPath:@"badgeValue" context:kSPSideTabBadgeViewBadgeValueObservationContext]; 240 | _item = nil; 241 | } 242 | 243 | } 244 | 245 | + (SPSideTabBadgeView *) badgeViewWithFrame:(CGRect)frame 246 | { 247 | SPSideTabBadgeView *badge = [[SPSideTabBadgeView alloc] initWithFrame:frame]; 248 | badge.backgroundImage = [[UIImage imageNamed:@"unread-tb~ipad.png"] resizableImageWithCapInsets:UIEdgeInsetsMake(11, 10, 11, 10)]; 249 | badge.textColor = [UIColor whiteColor]; 250 | badge.font = [UIFont systemFontOfSize:12]; 251 | 252 | return badge; 253 | } 254 | 255 | - (void)bindToTabItem:(UITabBarItem *)item 256 | { 257 | if(_item) { 258 | [_item removeObserver:self forKeyPath:@"badgeValue" context:kSPSideTabBadgeViewBadgeValueObservationContext]; 259 | _item = nil; 260 | } 261 | if(item) { 262 | _item = item; 263 | [_item addObserver:self forKeyPath:@"badgeValue" options:NSKeyValueObservingOptionInitial context:kSPSideTabBadgeViewBadgeValueObservationContext]; 264 | } 265 | } 266 | 267 | - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context 268 | { 269 | if(context == kSPSideTabBadgeViewBadgeValueObservationContext) { 270 | self.text = _item.badgeValue; 271 | self.hidden = _item.badgeValue.length == 0; 272 | } else [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; 273 | } 274 | 275 | 276 | @end 277 | -------------------------------------------------------------------------------- /Sources/SPSideTabController.m: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Spotify 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #import "SPSideTabController.h" 16 | 17 | @interface SPSideTabController () { 18 | BOOL _bottomAttachmentHidden; 19 | NSArray *_additionalItems; 20 | } 21 | @property(nonatomic,readwrite,retain) SPSideTabBar *tabBar; 22 | @property(nonatomic,retain) UIView *mainContainer; 23 | @property(nonatomic,retain) UIView *bottomContainer; 24 | - (void)addBottomAttachmentToContainer; 25 | @end 26 | 27 | @implementation SPSideTabController 28 | #define BOTTOM_BAR_HEIGHT 69 29 | 30 | - (void)loadView 31 | { 32 | CGRect afRect = [[UIScreen mainScreen] applicationFrame]; 33 | CGRect pen = afRect; 34 | UIView *root = [[UIView alloc] initWithFrame:pen]; 35 | self.view = root; 36 | 37 | root.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight; 38 | 39 | pen.origin = (CGPoint){0,0}; 40 | pen.size.width = 80; 41 | self.tabBar = [[SPSideTabBar alloc] initWithFrame:pen]; 42 | self.tabBar.delegate = self; 43 | [root addSubview:_tabBar]; 44 | 45 | 46 | pen.origin.x += pen.size.width; 47 | pen.size.width = afRect.size.width-pen.size.width; 48 | pen.size.height -= BOTTOM_BAR_HEIGHT; 49 | _mainContainer = [[UIView alloc] initWithFrame:pen]; 50 | [root addSubview:_mainContainer]; 51 | 52 | pen.origin.y = CGRectGetMaxY(pen); 53 | pen.size.height = BOTTOM_BAR_HEIGHT; 54 | _bottomContainer = [[UIView alloc] initWithFrame:pen]; 55 | [_bottomContainer setClipsToBounds:NO]; 56 | [root addSubview:_bottomContainer]; 57 | 58 | [self addBottomAttachmentToContainer]; 59 | 60 | root.backgroundColor = [UIColor blackColor]; 61 | _mainContainer.backgroundColor = [UIColor blackColor]; 62 | _bottomContainer.backgroundColor = [UIColor colorWithRed:0.7 green:0.7 blue:0.7 alpha:1.0]; 63 | 64 | if (_selectedViewController) { 65 | UIViewController *sel = _selectedViewController; 66 | [self setSelectedViewController:nil]; 67 | [self setSelectedViewController:sel]; 68 | } 69 | 70 | [self addBottomAttachmentToContainer]; 71 | [self setBottomAttachmentHidden:YES animated:NO]; 72 | } 73 | 74 | - (void)viewDidLoad 75 | { 76 | [super viewDidLoad]; 77 | 78 | // We just created the tab bar: make selection and contents match internal state 79 | NSArray *allItems = [_viewControllers valueForKey:@"tabBarItem"]; 80 | NSMutableArray *validItems = [NSMutableArray array]; 81 | for (int i = 0; i < [allItems count]; i++) 82 | { 83 | id item = allItems[i]; 84 | if ([item isKindOfClass:[NSNull class]]) 85 | NSLog(@"Error, NULL tab bar item from view controller '%@'.", _viewControllers[i]); 86 | else 87 | [validItems addObject:item]; 88 | } 89 | _tabBar.items = validItems; 90 | _tabBar.additionalItems = self.additionalItems; 91 | _tabBar.selectedItem = (_tabBar.items)[self.selectedIndex]; 92 | 93 | // If it was hidden before view was loaded 94 | [self setBottomAttachmentHidden:_bottomAttachmentHidden animated:NO]; 95 | } 96 | 97 | - (void)viewDidUnload 98 | { 99 | self.bottomContainer = nil; 100 | self.mainContainer = nil; 101 | self.tabBar.delegate = nil; 102 | self.tabBar = nil; 103 | [super viewDidUnload]; 104 | } 105 | 106 | - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation 107 | { 108 | return YES; 109 | } 110 | 111 | - (NSSet*)keyPathsForValuesAffectingValueForSelectedIndex 112 | { 113 | return [NSSet setWithObject:@"selectedViewController"]; 114 | } 115 | - (void)setViewControllers:(NSArray *)viewControllers 116 | { 117 | if ([viewControllers isEqual:_viewControllers]) return; 118 | 119 | [self setSelectedViewController:nil]; 120 | 121 | for(UIViewController *vc in _viewControllers) [vc removeFromParentViewController]; 122 | 123 | _viewControllers = [viewControllers copy]; 124 | 125 | for(UIViewController *vc in _viewControllers) [self addChildViewController:vc]; 126 | 127 | _tabBar.items = [_viewControllers valueForKey:@"tabBarItem"]; 128 | self.selectedViewController = _viewControllers[0]; 129 | 130 | for (UIViewController *vc in _viewControllers) [vc didMoveToParentViewController:self]; 131 | } 132 | - (void)setSelectedViewController:(UIViewController *)newVC 133 | { 134 | if (newVC == _selectedViewController) return; 135 | 136 | UIViewController *oldVC = _selectedViewController; 137 | _selectedViewController = newVC; 138 | 139 | if (![self isViewLoaded]) return; 140 | 141 | _tabBar.selectedItem = newVC.tabBarItem; 142 | 143 | UIView *oldView = oldVC.view; 144 | UIView *newView = newVC.view; 145 | 146 | CGRect centered = _mainContainer.bounds; 147 | CGRect above = _mainContainer.bounds; above.origin.y -= above.size.height; 148 | CGRect below = _mainContainer.bounds; below.origin.y += below.size.height; 149 | 150 | newView.frame = centered; 151 | 152 | [oldView removeFromSuperview]; 153 | [_mainContainer addSubview:newView]; 154 | } 155 | - (NSUInteger)selectedIndex 156 | { 157 | return [_viewControllers indexOfObject:_selectedViewController]; 158 | } 159 | - (void)setSelectedIndex:(NSUInteger)selectedIndex 160 | { 161 | [self setSelectedViewController:_viewControllers[selectedIndex]]; 162 | } 163 | - (NSArray *)additionalItems 164 | { 165 | return _additionalItems; 166 | } 167 | - (void)setAdditionalItems:(NSArray *)additionalItems 168 | { 169 | if (_additionalItems != additionalItems) 170 | { 171 | _additionalItems = additionalItems; 172 | if ([self isViewLoaded]) 173 | [_tabBar setAdditionalItems:additionalItems]; 174 | } 175 | } 176 | 177 | - (void)setBottomAttachment:(UIViewController *)bottomAttachment 178 | { 179 | if (bottomAttachment == _bottomAttachment) { 180 | return; 181 | } 182 | 183 | [_bottomAttachment removeFromParentViewController]; 184 | [_bottomAttachment.view removeFromSuperview]; 185 | _bottomAttachment = bottomAttachment; 186 | 187 | [self addBottomAttachmentToContainer]; 188 | } 189 | 190 | - (void)addBottomAttachmentToContainer 191 | { 192 | if (!self.bottomAttachment) { 193 | return; 194 | } 195 | 196 | [self addChildViewController:self.bottomAttachment]; 197 | 198 | self.bottomAttachment.view.frame = self.bottomContainer.bounds; 199 | [self.bottomContainer addSubview:self.bottomAttachment.view]; 200 | 201 | [self.bottomAttachment didMoveToParentViewController:self]; 202 | } 203 | - (void)tabBar:(SPSideTabBar *)tabBar didSelectItem:(UITabBarItem *)item 204 | { 205 | NSArray *vcItems = [self.viewControllers valueForKey:@"tabBarItem"]; 206 | if ([vcItems containsObject:item]) { 207 | UIViewController *newVC = (self.viewControllers)[[vcItems indexOfObject:item]]; 208 | if (newVC != self.selectedViewController) 209 | self.selectedViewController = newVC; 210 | else { 211 | if ([newVC respondsToSelector:@selector(popToRootViewControllerAnimated:)]) 212 | [(id)newVC popToRootViewControllerAnimated:YES]; 213 | } 214 | } else 215 | [_tabBarDelegate tabBar:tabBar didSelectItem:item]; 216 | } 217 | 218 | - (BOOL)isBottomAttachmentHidden 219 | { 220 | return _bottomAttachmentHidden; 221 | } 222 | 223 | - (void)setBottomAttachmentHidden:(BOOL)hide animated:(BOOL)animated 224 | { 225 | _bottomAttachmentHidden = hide; 226 | if (![self isViewLoaded]) return; 227 | 228 | CGRect mainFrame = _mainContainer.frame; 229 | CGRect bottomFrame = _bottomContainer.frame; 230 | 231 | if (!hide) { 232 | // shrink main frame 233 | mainFrame.size.height = self.view.frame.size.height - BOTTOM_BAR_HEIGHT; 234 | bottomFrame.origin.y = CGRectGetMaxY(mainFrame); 235 | } else { 236 | mainFrame.size.height = self.view.frame.size.height; 237 | bottomFrame.origin.y = CGRectGetMaxY(mainFrame) + 1; 238 | } 239 | 240 | if (animated) { 241 | static const CGFloat kAnimationDuration = .25; 242 | 243 | [UIView animateWithDuration:kAnimationDuration 244 | delay:0 245 | options:UIViewAnimationOptionBeginFromCurrentState 246 | animations:^{ 247 | _bottomContainer.frame = bottomFrame; 248 | 249 | if (hide) { 250 | _mainContainer.frame = mainFrame; 251 | } 252 | } 253 | completion:^(BOOL finished) { 254 | /** 255 | * NOTE: 256 | * Dispatch async since the main frame will be updated before the bottom frame otherwise (is the thread blocked?). 257 | * That the main and bottom frames can't be animated at the same time is very strange, but this works for now... 258 | */ 259 | if (!hide) { 260 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, kAnimationDuration * 2 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ 261 | _mainContainer.frame = mainFrame; 262 | }); 263 | } 264 | } 265 | ]; 266 | } 267 | else { 268 | _mainContainer.frame = mainFrame; 269 | _bottomContainer.frame = bottomFrame; 270 | } 271 | } 272 | 273 | 274 | - (void)viewDidLayoutSubviews 275 | { 276 | [super viewDidLayoutSubviews]; 277 | 278 | CGRect bounds = self.view.bounds; 279 | 280 | _tabBar.frame = CGRectMake(0, 0, _tabBar.frame.size.width, bounds.size.height); 281 | 282 | if (_bottomAttachmentHidden) 283 | { 284 | _mainContainer.frame = CGRectMake(CGRectGetMaxX(_tabBar.frame), 285 | 0, 286 | bounds.size.width - CGRectGetMaxX(_tabBar.frame), 287 | bounds.size.height); 288 | _bottomContainer.frame = CGRectMake(CGRectGetMaxX(_tabBar.frame), 289 | CGRectGetMaxY(bounds) + 1, 290 | bounds.size.width - CGRectGetMaxX(_tabBar.frame), 291 | BOTTOM_BAR_HEIGHT); 292 | } 293 | else 294 | { 295 | _mainContainer.frame = CGRectMake(CGRectGetMaxX(_tabBar.frame), 296 | 0, 297 | bounds.size.width - CGRectGetMaxX(_tabBar.frame), 298 | bounds.size.height - BOTTOM_BAR_HEIGHT); 299 | _bottomContainer.frame = CGRectMake(CGRectGetMaxX(_tabBar.frame), 300 | CGRectGetMaxY(_mainContainer.frame), 301 | bounds.size.width - CGRectGetMaxX(_tabBar.frame), 302 | BOTTOM_BAR_HEIGHT); 303 | } 304 | 305 | for (UIView *superview in @[_mainContainer, _bottomContainer]) 306 | for (UIView *subview in superview.subviews) 307 | { 308 | subview.frame = superview.bounds; 309 | [subview setNeedsLayout]; 310 | } 311 | } 312 | 313 | @end 314 | 315 | @implementation SPTabBarItem 316 | @synthesize view = _buttonView; 317 | @synthesize imageName = _imageName; 318 | - (id)initWithTitle:(NSString *)title imageName:(NSString*)imageName tag:(NSInteger)tag 319 | { 320 | if (!(self = [super init])) 321 | return nil; 322 | 323 | self.title = title; 324 | self.imageName = imageName; 325 | self.tag = tag; 326 | 327 | return self; 328 | } 329 | @end 330 | 331 | -------------------------------------------------------------------------------- /Sources/SPSideTabItemButton.h: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Spotify 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #import 16 | 17 | @interface SPSideTabItemButton : UIButton 18 | @property (nonatomic, retain) UIView *badgeView; 19 | @end 20 | 21 | 22 | -------------------------------------------------------------------------------- /Sources/SPSideTabItemButton.m: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Spotify 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #import "SPSideTabItemButton.h" 16 | 17 | @implementation SPSideTabItemButton 18 | - (id)initWithFrame:(CGRect)frame { 19 | if (!(self = [super initWithFrame:frame])) return nil; 20 | 21 | UILabel *label = [self titleLabel]; 22 | label.font = [UIFont boldSystemFontOfSize:11]; 23 | label.shadowOffset = CGSizeMake(0, -1); 24 | 25 | [self setTitleShadowColor:[UIColor colorWithWhite:0.0 alpha:0.7] forState:UIControlStateNormal]; 26 | [self setTitleColor:[UIColor colorWithWhite:0.663 alpha:1.000] forState:UIControlStateNormal]; 27 | [self setTitleColor:[UIColor colorWithWhite:0.925 alpha:1.000] forState:UIControlStateSelected]; 28 | 29 | return self; 30 | } 31 | 32 | - (void)layoutSubviews { 33 | [super layoutSubviews]; 34 | 35 | UIImageView *imageView = [self imageView]; 36 | UILabel *label = [self titleLabel]; 37 | 38 | [imageView sizeToFit]; 39 | [label sizeToFit]; 40 | 41 | CGRect imageFrame = imageView.frame; 42 | CGRect labelFrame = label.frame; 43 | 44 | imageFrame.origin.y = 4; 45 | labelFrame.origin.y = 46; 46 | 47 | imageFrame.origin.x = roundf((self.bounds.size.width - imageFrame.size.width) / 2); 48 | labelFrame.origin.x = roundf((self.bounds.size.width - labelFrame.size.width) / 2); 49 | 50 | imageView.frame = imageFrame; 51 | label.frame = labelFrame; 52 | 53 | if (self.badgeView){ 54 | CGPoint center = CGPointMake(self.bounds.size.width/2, self.bounds.size.height/2); 55 | self.badgeView.center = CGPointMake(floorf(center.x + center.x/3), floorf(center.y - center.y/2)); 56 | self.badgeView.frame = CGRectIntegral(self.badgeView.frame); 57 | } 58 | } 59 | 60 | - (void)setBadgeView:(UIView *)badgeView 61 | { 62 | [_badgeView removeFromSuperview]; 63 | _badgeView = badgeView; 64 | if (badgeView) 65 | [self addSubview:badgeView]; 66 | [self setNeedsLayout]; 67 | } 68 | 69 | @end 70 | 71 | -------------------------------------------------------------------------------- /Sources/SPStackedNavigationController.m: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Spotify 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #import "SPStackedNavigationController.h" 16 | #import "SPStackedPageContainer.h" 17 | #import "SPStackedNavigationScrollView.h" 18 | 19 | #import 20 | 21 | @interface SPStackedNavigationController () 22 | { 23 | SPStackedNavigationScrollView *_scroll; 24 | } 25 | @end 26 | 27 | @implementation SPStackedNavigationController 28 | 29 | - (id)init 30 | { 31 | if (!(self = [super init])) return nil; 32 | return self; 33 | } 34 | - (id)initWithRootViewController:(UIViewController *)rootViewController 35 | { 36 | if (!(self = [self init])) return nil; 37 | 38 | [self pushViewController:rootViewController animated:NO]; 39 | [self setActiveViewController:rootViewController position:SPStackedNavigationPagePositionLeft animated:NO]; 40 | 41 | return self; 42 | } 43 | 44 | static const float kUnknownFrameSize = 10; 45 | - (void)loadView 46 | { 47 | CGRect frame = CGRectMake(0, 0, [[UIScreen mainScreen] bounds].size.width, [[UIScreen mainScreen] bounds].size.height); 48 | UIView *root = [[UIView alloc] initWithFrame:frame]; 49 | root.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 50 | root.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"backgroundTexture.png"]]; 51 | 52 | _scroll = [[SPStackedNavigationScrollView alloc] initWithFrame:frame]; 53 | _scroll.delegate = self; 54 | _scroll.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight; 55 | [root addSubview:_scroll]; 56 | 57 | self.view = root; 58 | 59 | for (UIViewController *viewController in [self childViewControllers]) 60 | [self pushPageContainerWithViewController:viewController]; 61 | } 62 | 63 | - (void)viewDidLayoutSubviews 64 | { 65 | [super viewDidLayoutSubviews]; 66 | [self setActiveViewController:self.activeViewController position:self.activeViewControllerPagePosition animated:NO]; 67 | } 68 | 69 | #pragma mark view controllers manipulation entry points 70 | - (void)pushPageContainerWithViewController:(UIViewController*)viewController 71 | { 72 | CGSize size = self.view.frame.size; 73 | CGRect frame = CGRectMake(self.view.bounds.size.width, 0, 0, size.height); 74 | frame.size.width = (viewController.stackedNavigationPageSize == kStackedPageHalfSize ? 75 | kSPStackedNavigationHalfPageWidth : 76 | size.width); 77 | 78 | SPStackedPageContainer *pageC = [[SPStackedPageContainer alloc] initWithFrame:frame VC:viewController]; 79 | [_scroll addSubview:pageC]; 80 | } 81 | 82 | // Only these two methods actually manipulate _viewControllers 83 | - (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated 84 | { 85 | [self pushViewController:viewController animated:animated activate:YES]; 86 | } 87 | - (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated activate:(BOOL)activate 88 | { 89 | if (!viewController) 90 | return; 91 | 92 | SPStackedNavigationPagePosition activePosition = SPStackedNavigationPagePositionRight; 93 | if (![self.childViewControllers count]) 94 | activePosition = SPStackedNavigationPagePositionLeft; 95 | 96 | if ([viewController parentViewController] == self && activate) 97 | { 98 | [self setActiveViewController:viewController position:activePosition animated:animated]; 99 | return; 100 | } 101 | NSAssert([viewController parentViewController] == nil, @"cannot push view controller with an existing parent"); 102 | 103 | [self willChangeValueForKey:@"viewControllers"]; 104 | [self addChildViewController:viewController]; 105 | 106 | if ([self isViewLoaded]) 107 | [self pushPageContainerWithViewController:viewController]; 108 | 109 | if (activate) 110 | [self setActiveViewController:viewController position:activePosition animated:animated]; 111 | 112 | [viewController didMoveToParentViewController:self]; 113 | [self didChangeValueForKey:@"viewControllers"]; 114 | } 115 | - (void)pushViewController:(UIViewController *)viewController onTopOf:(UIViewController*)parent animated:(BOOL)animated 116 | { 117 | [self pushViewController:viewController onTopOf:parent animated:animated activate:YES]; 118 | } 119 | - (void)pushViewController:(UIViewController *)viewController onTopOf:(UIViewController*)parent animated:(BOOL)animated activate:(BOOL)activate 120 | { 121 | while (![self.viewControllers containsObject:parent] && parent != nil) 122 | parent = [parent parentViewController]; 123 | [self popToViewController:parent animated:animated]; 124 | [self pushViewController:viewController animated:animated activate:activate]; 125 | } 126 | - (UIViewController *)popViewControllerAnimated:(BOOL)animated 127 | { 128 | UIViewController *viewController = [[self childViewControllers] lastObject]; 129 | if (!viewController) 130 | return nil; 131 | 132 | [self willChangeValueForKey:@"viewControllers"]; 133 | [viewController willMoveToParentViewController:nil]; 134 | 135 | if ([self isViewLoaded]) 136 | { 137 | SPStackedPageContainer *pageC = [_scroll containerForViewController:viewController]; 138 | pageC.markedForSuperviewRemoval = YES; 139 | } 140 | 141 | 142 | [viewController removeFromParentViewController]; 143 | [self didChangeValueForKey:@"viewControllers"]; 144 | 145 | [self setActiveViewController:[self.childViewControllers lastObject] 146 | position:SPStackedNavigationPagePositionRight 147 | animated:animated]; 148 | 149 | return viewController; 150 | } 151 | 152 | #pragma mark Convenience methods to the above two methods. 153 | - (void)setViewControllers:(NSArray *)viewControllers animated:(BOOL)animated 154 | { 155 | id commonVC = nil; int startI = NSNotFound; 156 | for(int i = 0, c = MIN([self.viewControllers count], [viewControllers count]); i < c; i++) 157 | { 158 | if ([viewControllers[i] isEqual:(self.viewControllers)[i]]) 159 | { 160 | startI = i; 161 | commonVC = viewControllers[i]; 162 | } 163 | else 164 | break; 165 | } 166 | 167 | NSArray *toPush = viewControllers; 168 | if (startI != NSNotFound) 169 | { 170 | [self popToViewController:commonVC animated:animated]; 171 | toPush = [viewControllers subarrayWithRange:NSMakeRange(startI+1, [viewControllers count] - startI - 1)]; 172 | } 173 | for(id vc in toPush) 174 | [self pushViewController:vc animated:animated]; 175 | } 176 | - (void)setViewControllers:(NSArray *)viewControllers 177 | { 178 | [self setViewControllers:viewControllers animated:NO]; 179 | } 180 | - (void)setActiveViewController:(UIViewController*)viewController animated:(BOOL)animated 181 | { 182 | if (self.activeViewController == viewController || 183 | [self.viewControllers indexOfObject:viewController] == NSNotFound) 184 | return; 185 | NSUInteger currentIndex = [self.viewControllers indexOfObject:self.activeViewController]; 186 | NSUInteger newIndex = [self.viewControllers indexOfObject:viewController]; 187 | [self setActiveViewController:viewController 188 | position:(newIndex > currentIndex ? 189 | SPStackedNavigationPagePositionRight : 190 | SPStackedNavigationPagePositionLeft) 191 | animated:animated]; 192 | } 193 | - (void)setActiveViewController:(UIViewController *)viewController position:(SPStackedNavigationPagePosition)position animated:(BOOL)animated 194 | { 195 | NSArray *viewControllers = [self viewControllers]; 196 | NSUInteger index = [viewControllers indexOfObject:viewController]; 197 | if (index == NSNotFound) return; 198 | 199 | [self setActiveViewController:viewController position:position]; 200 | [_scroll setContentOffset:CGPointMake([_scroll scrollOffsetForAligningPage:(_scroll.subviews)[index] 201 | position:self.activeViewControllerPagePosition], 202 | 0) 203 | animated:animated]; 204 | } 205 | - (void)setActiveViewController:(UIViewController *)activeViewController position:(SPStackedNavigationPagePosition)position 206 | { 207 | if (_activeViewController != activeViewController) 208 | { 209 | UIViewController *oldActiveViewController = _activeViewController; 210 | _activeViewController = activeViewController; 211 | [oldActiveViewController viewDidBecomeInactiveInStackedNavigation]; 212 | [activeViewController viewDidBecomeActiveInStackedNavigation]; 213 | } 214 | _activeViewControllerPagePosition = position; 215 | } 216 | - (NSArray *)visibleViewControllers 217 | { 218 | NSInteger activeIndex = [[self viewControllers] indexOfObject:self.activeViewController]; 219 | if (activeIndex == NSNotFound) { 220 | return @[]; 221 | } 222 | 223 | if ([self.activeViewController stackedNavigationPageSize] == kStackedPageFullSize) { 224 | return @[self.activeViewController]; 225 | } 226 | 227 | NSInteger otherVisibleIndex = activeIndex + (self.activeViewControllerPagePosition == SPStackedNavigationPagePositionLeft ? 1 : -1); 228 | NSRange range; 229 | range.location = otherVisibleIndex >= 0 ? MIN(otherVisibleIndex, activeIndex) : activeIndex; 230 | range.length = (otherVisibleIndex >= 0 && otherVisibleIndex < [[self viewControllers] count]) ? 2 : 1; 231 | 232 | return [[self viewControllers] subarrayWithRange:range]; 233 | } 234 | - (NSArray*)viewControllers; { return [self childViewControllers]; } 235 | - (NSArray *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated 236 | { 237 | NSMutableArray *vcs = [NSMutableArray array]; 238 | while(self.viewControllers.count > 0 && self.viewControllers.lastObject != viewController) 239 | [vcs addObject:[self popViewControllerAnimated:animated]]; 240 | return vcs; 241 | } 242 | - (NSArray *)popToRootViewControllerAnimated:(BOOL)animated 243 | { 244 | int targetCount = 1; 245 | if (self.viewControllers.count > 0 && [(self.viewControllers)[0] stackedNavigationPageSize] == kStackedPageHalfSize) 246 | targetCount = 2; 247 | 248 | NSMutableArray *vcs = [NSMutableArray array]; 249 | while(self.viewControllers.count > targetCount) 250 | [vcs addObject:[self popViewControllerAnimated:animated]]; 251 | [self setActiveViewController:(self.viewControllers)[0] position:SPStackedNavigationPagePositionLeft animated:animated]; 252 | return vcs; 253 | } 254 | - (UIViewController*)topViewController 255 | { 256 | return self.viewControllers.lastObject; 257 | } 258 | - (UIGestureRecognizer*)panGestureRecognizer 259 | { 260 | (void)self.view; // make sure we're loaded 261 | return [_scroll panGestureRecognizer]; 262 | } 263 | 264 | #pragma mark VC integration 265 | - (UITabBarItem*)tabBarItem 266 | { 267 | return self.viewControllers.count==0?nil:[(self.viewControllers)[0] tabBarItem]; 268 | } 269 | 270 | #pragma KVC 271 | - (NSSet*)keyPathsForValuesAffectingTabBarItem 272 | { 273 | return [NSSet setWithObject:@"viewControllers"]; 274 | } 275 | - (NSSet*)keyPathsForValuesAffectingTopViewController 276 | { 277 | return [NSSet setWithObject:@"viewControllers"]; 278 | } 279 | - (NSSet*)keyPathsForValuesAffectingVisibleViewController 280 | { 281 | return [NSSet setWithObject:@"viewControllers"]; 282 | } 283 | 284 | - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation 285 | { 286 | return YES; 287 | } 288 | 289 | 290 | #pragma mark Scroll delegate 291 | - (void)stackedNavigationScrollView:(SPStackedNavigationScrollView *)stackedNavigationScrollView 292 | didStopAtPageContainer:(SPStackedPageContainer *)stackedPageContainer 293 | pagePosition:(SPStackedNavigationPagePosition)pagePosition 294 | { 295 | [self setActiveViewController:stackedPageContainer.vc position:pagePosition]; 296 | } 297 | 298 | @end 299 | 300 | 301 | @implementation UIViewController (SPStackedNavigationControllerItem) 302 | - (SPStackedNavigationController*)stackedNavigationController 303 | { 304 | id parent = self.parentViewController; 305 | if([parent isKindOfClass:[SPStackedNavigationController class]]) 306 | return parent; 307 | return nil; 308 | } 309 | 310 | - (void)viewDidBecomeActiveInStackedNavigation { } // Default implementation does nothing 311 | 312 | - (void)viewDidBecomeInactiveInStackedNavigation { } // Default implementation does nothing 313 | 314 | - (void)activateInStackedNavigationAnimated:(BOOL)animated 315 | { 316 | [self.stackedNavigationController setActiveViewController:self animated:animated]; 317 | } 318 | 319 | - (BOOL)isActiveInStackedNavigation 320 | { 321 | return (self.stackedNavigationController.activeViewController == self); 322 | } 323 | 324 | @end 325 | 326 | @implementation NSObject (SPStackedNavigationChild) 327 | - (SPStackedNavigationPageSize)stackedNavigationPageSize 328 | { 329 | return kStackedPageFullSize; 330 | } 331 | @end 332 | 333 | @implementation UINavigationController (SPStackedNavigationControllerCompatibility) 334 | - (void)pushViewController:(UIViewController *)viewController onTopOf:(UIViewController*)parent animated:(BOOL)animated 335 | { 336 | [self popToViewController:parent animated:animated]; 337 | [self pushViewController:viewController animated:animated]; 338 | } 339 | @end -------------------------------------------------------------------------------- /Sources/SPStackedNavigationScrollView.h: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Spotify 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #import "SPStackedNavigationController.h" 16 | 17 | @class SPStackedPageContainer; 18 | @protocol SPStackedNavigationScrollViewDelegate; 19 | 20 | /// PRIVATE IMPLEMENTATION DETAIL of SPStackedNavigationController 21 | 22 | @interface SPStackedNavigationScrollView : UIView 23 | @property(nonatomic) CGPoint contentOffset; 24 | @property(nonatomic,assign) id delegate; 25 | @property(nonatomic,retain,readonly) UIPanGestureRecognizer *panGestureRecognizer; 26 | - (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated; 27 | - (void)snapToClosest; 28 | 29 | - (NSRange)scrollRange; 30 | - (CGFloat)scrollOffsetForAligningPageWithRightEdge:(SPStackedPageContainer*)pageC; 31 | - (CGFloat)scrollOffsetForAligningPageWithLeftEdge:(SPStackedPageContainer*)pageC; 32 | - (CGFloat)scrollOffsetForAligningPage:(SPStackedPageContainer*)pageC position:(SPStackedNavigationPagePosition)position; 33 | - (SPStackedPageContainer*)containerForViewController:(UIViewController*)viewController; 34 | @end 35 | 36 | @protocol SPStackedNavigationScrollViewDelegate 37 | - (void)stackedNavigationScrollView:(SPStackedNavigationScrollView *)stackedNavigationScrollView 38 | didStopAtPageContainer:(SPStackedPageContainer *)stackedPageContainer 39 | pagePosition:(SPStackedNavigationPagePosition)pagePosition; 40 | @end -------------------------------------------------------------------------------- /Sources/SPStackedNavigationScrollView.m: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Spotify 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #import "SPStackedNavigationScrollView.h" 16 | #import "SPStackedPageContainer.h" 17 | #import 18 | #import "SPFunctional-mini.h" 19 | 20 | #ifndef CLAMP 21 | #define CLAMP(v, min, max) ({ \ 22 | __typeof(v) _v = v; \ 23 | __typeof(min) _min = min; \ 24 | __typeof(max) _max = max; \ 25 | MAX(_min, MIN(_v, _max)); \ 26 | }) 27 | #endif 28 | 29 | #define fcompare(actual, expected, epsilon) ({ \ 30 | __typeof(actual) _actual = actual; \ 31 | __typeof(expected) _expected = expected; \ 32 | __typeof(epsilon) _epsilon = epsilon; \ 33 | fabs(_actual - _expected) < _epsilon; \ 34 | }) 35 | #define fsign(f) ({ __typeof(f) _f = f; _f > 0. ? 1. : (_f < 0.) ? -1. : 0.; }) 36 | 37 | static const CGFloat kScrollDoneMarginOvershoot = 3; 38 | static const CGFloat kScrollDoneMarginNormal = 1; 39 | static const CGFloat kPanCaptureAngle = ((55.f) / 180.f * M_PI); 40 | static const CGFloat kPanScrollViewDeceleratingCaptureAngle = ((40.f) / 180.f * M_PI); 41 | 42 | @interface SPStackedNavigationScrollView () 43 | @property(nonatomic,retain) UIPanGestureRecognizer *scrollRec; 44 | @property(nonatomic,retain) CADisplayLink *scrollAnimationTimer; 45 | @property(nonatomic,copy) void(^onScrollDone)(); 46 | - (void)scrollGesture:(UIPanGestureRecognizer*)grec; 47 | - (void)updateContainerVisibilityByShowing:(BOOL)doShow byHiding:(BOOL)doHide; 48 | @end 49 | 50 | @implementation SPStackedNavigationScrollView { 51 | CGPoint _actualOffset; 52 | CGPoint _targetOffset; 53 | CGPoint _scrollAtStartOfPan; 54 | CGFloat _scrollDoneMargin; 55 | BOOL _runningRunLoop; 56 | BOOL _inRunLoop; 57 | } 58 | @synthesize scrollRec = _scrollRec; 59 | @synthesize scrollAnimationTimer = _scrollAnimationTimer; 60 | @synthesize onScrollDone = _onScrollDone; 61 | @synthesize delegate = _delegate; 62 | 63 | - (id)initWithFrame:(CGRect)frame 64 | { 65 | if (!(self = [super initWithFrame:frame])) 66 | return nil; 67 | 68 | self.scrollRec = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(scrollGesture:)]; 69 | _scrollRec.maximumNumberOfTouches = 1; 70 | _scrollRec.delaysTouchesBegan = _scrollRec.delaysTouchesEnded = NO; 71 | _scrollRec.cancelsTouchesInView = YES; 72 | _scrollRec.delegate = self; 73 | 74 | [self addGestureRecognizer:_scrollRec]; 75 | 76 | return self; 77 | } 78 | 79 | #pragma mark Gesture recognizing 80 | - (NSRange)scrollRangeForPageContainer:(SPStackedPageContainer*)pageC 81 | { 82 | CGFloat width = 0.; 83 | for(SPStackedPageContainer *pc in self.subviews) 84 | { 85 | if (pc == pageC) 86 | break; 87 | if (pc.vc.stackedNavigationPageSize == kStackedPageFullSize) 88 | width += self.frame.size.width; 89 | else 90 | width += pc.frame.size.width; 91 | } 92 | return NSMakeRange(width, (pageC.vc.stackedNavigationPageSize == kStackedPageFullSize ? 93 | self.frame.size.width : 94 | pageC.frame.size.width)); 95 | } 96 | 97 | - (NSRange)scrollRange 98 | { 99 | return [self scrollRangeForPageContainer:[self.subviews lastObject]]; 100 | } 101 | 102 | - (CGFloat)scrollOffsetForAligningPageWithRightEdge:(SPStackedPageContainer*)pageC 103 | { 104 | NSRange scrollRange = [self scrollRangeForPageContainer:pageC]; 105 | return scrollRange.location // align left edge with left edge of screen 106 | - self.frame.size.width // scroll it completely out of screen to the right 107 | + scrollRange.length; // scroll it back just so it's exactly on screen. 108 | } 109 | 110 | - (CGFloat)scrollOffsetForAligningPageWithLeftEdge:(SPStackedPageContainer*)pageC 111 | { 112 | NSRange scrollRange = [self scrollRangeForPageContainer:pageC]; 113 | return scrollRange.location; 114 | } 115 | 116 | - (CGFloat)scrollOffsetForAligningPage:(SPStackedPageContainer*)pageC position:(SPStackedNavigationPagePosition)position 117 | { 118 | return (position == SPStackedNavigationPagePositionLeft ? 119 | [self scrollOffsetForAligningPageWithLeftEdge:pageC] : 120 | [self scrollOffsetForAligningPageWithRightEdge:pageC]); 121 | } 122 | 123 | - (SPStackedPageContainer*)containerForViewController:(UIViewController*)viewController 124 | { 125 | for (SPStackedPageContainer *pc in self.subviews) 126 | { 127 | if (pc.vc == viewController) 128 | return pc; 129 | } 130 | return nil; 131 | } 132 | 133 | - (void)scrollAndSnapWithVelocity:(float)vel animated:(BOOL)animated 134 | { 135 | // this is ugly, but we need to ensure that all views are loaded correctly to calculate left/right containers 136 | [self setNeedsLayout]; 137 | [self layoutIfNeeded]; 138 | 139 | // If swiping to the left, snap to the left; and vice versa. 140 | CGFloat targetPoint; 141 | SPStackedPageContainer *target = nil; 142 | 143 | SPStackedPageContainer *left = [self.subviews spstacked_any:^BOOL(id obj) { return [obj VCVisible]; }]; 144 | SPStackedPageContainer *right = [self.subviews spstacked_filter:^BOOL(id obj) { return [obj VCVisible]; }].lastObject; 145 | 146 | if (vel < 0) // trying to reveal to the left 147 | target = left; 148 | else // trying to reveal to the right 149 | target = right; 150 | 151 | // scroll extra far if user scrolls really fast 152 | int extraMove = (fabs(vel) > 8500 ? 2 : (fabs(vel) > 5500) ? 1 : 0)*fsign(vel); 153 | if (extraMove != 0) 154 | target = (self.subviews)[CLAMP((int)[self.subviews indexOfObject:target]+extraMove, 0, (int)(self.subviews.count-1))]; 155 | 156 | // Align with left edge if scrolling left, or vice versa 157 | NSRange leftScrollRange = [self scrollRangeForPageContainer:left]; 158 | if (vel < 0 && extraMove == 0 && _actualOffset.x > (leftScrollRange.location + leftScrollRange.length/2)) { 159 | SPStackedPageContainer *targetView = (self.subviews)[CLAMP((int)[self.subviews indexOfObject:left]+1, 0, (int)(self.subviews.count-1))]; 160 | if (UIInterfaceOrientationIsPortrait([[UIApplication sharedApplication] statusBarOrientation])) 161 | target = targetView; 162 | targetPoint = [self scrollOffsetForAligningPageWithRightEdge:targetView]; 163 | } else if (vel < 0) 164 | targetPoint = [self scrollRangeForPageContainer:target].location; 165 | else 166 | targetPoint = [self scrollOffsetForAligningPageWithRightEdge:target]; 167 | 168 | // Overshoot the target a bit 169 | if (animated) 170 | { 171 | __weak typeof(self) weakSelf = self; 172 | self.onScrollDone = ^{ 173 | __strong __typeof(self) strongSelf = weakSelf; 174 | [strongSelf setContentOffset:CGPointMake(targetPoint, 0) animated:animated]; 175 | [strongSelf->_delegate stackedNavigationScrollView:strongSelf 176 | didStopAtPageContainer:target 177 | pagePosition:(target == left ? SPStackedNavigationPagePositionLeft : 178 | SPStackedNavigationPagePositionRight)]; 179 | }; 180 | } 181 | 182 | targetPoint += MAX(10, fabs(vel/150))*fsign(vel); 183 | 184 | [self setContentOffset:CGPointMake(targetPoint, 0) animated:animated]; 185 | _scrollDoneMargin = kScrollDoneMarginOvershoot; 186 | 187 | if (!animated) 188 | [_delegate stackedNavigationScrollView:self 189 | didStopAtPageContainer:target 190 | pagePosition:(target == left ? SPStackedNavigationPagePositionLeft : 191 | SPStackedNavigationPagePositionRight)]; 192 | } 193 | 194 | 195 | - (void)scrollGesture:(UIPanGestureRecognizer*)grec 196 | { 197 | if (grec.state == UIGestureRecognizerStateBegan) { 198 | _scrollAtStartOfPan = _actualOffset; 199 | [self startRunLoop]; 200 | } 201 | else if (grec.state == UIGestureRecognizerStateChanged) { 202 | self.contentOffset = CGPointMake(_scrollAtStartOfPan.x-[grec translationInView:self].x, 0); 203 | } else if (grec.state == UIGestureRecognizerStateFailed || grec.state == UIGestureRecognizerStateCancelled) { 204 | [self stopRunLoop]; 205 | [self setContentOffset:_scrollAtStartOfPan animated:YES]; 206 | } else if (grec.state == UIGestureRecognizerStateRecognized) { 207 | // minus: swipe left means navigate to VC to the right 208 | [self stopRunLoop]; 209 | [self scrollAndSnapWithVelocity:-[grec velocityInView:self].x animated:YES]; 210 | } 211 | } 212 | 213 | - (void)startRunLoop 214 | { 215 | if (!_runningRunLoop) 216 | { 217 | _runningRunLoop = YES; 218 | CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, ^{ 219 | if (_inRunLoop) 220 | return; 221 | _inRunLoop = YES; 222 | while (_runningRunLoop) 223 | [[NSRunLoop currentRunLoop] runMode:UITrackingRunLoopMode beforeDate:[NSDate distantFuture]]; 224 | _inRunLoop = NO; 225 | }); 226 | } 227 | } 228 | 229 | - (void)stopRunLoop 230 | { 231 | _runningRunLoop = NO; 232 | } 233 | 234 | - (void)snapToClosest 235 | { 236 | [self scrollAndSnapWithVelocity:0 animated:NO]; 237 | } 238 | 239 | - (UIPanGestureRecognizer *)panGestureRecognizer { return self.scrollRec; } 240 | 241 | - (BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)gestureRecognizer 242 | { 243 | CGPoint velocity = [gestureRecognizer velocityInView:[gestureRecognizer view]]; 244 | CGFloat angle = velocity.x == 0.0 ?: atanf(fabsf(velocity.y / velocity.x)); 245 | 246 | CGFloat captureAngle = kPanCaptureAngle; 247 | if ([[gestureRecognizer view] isKindOfClass:[UIScrollView class]] && [(UIScrollView*)[gestureRecognizer view] isDecelerating]) 248 | captureAngle = kPanScrollViewDeceleratingCaptureAngle; 249 | 250 | return captureAngle >= angle; 251 | } 252 | 253 | #pragma mark Animating content offset 254 | @synthesize contentOffset = _targetOffset; 255 | - (void)setContentOffset:(CGPoint)contentOffset 256 | { 257 | [self setContentOffset:contentOffset animated:NO]; 258 | } 259 | 260 | - (void)scrollAnimationFrame:(CADisplayLink*)cdl 261 | { 262 | if (fcompare(_targetOffset.x, _actualOffset.x, _scrollDoneMargin)) { 263 | [self.scrollAnimationTimer invalidate]; self.scrollAnimationTimer = nil; 264 | [self setNeedsLayout]; 265 | _actualOffset = _targetOffset; 266 | if (_onScrollDone) { 267 | self.onScrollDone(); 268 | self.onScrollDone = nil; 269 | } else 270 | // we're done animating, hide everything that needs to be hidden 271 | [self updateContainerVisibilityByShowing:YES byHiding:YES]; 272 | 273 | // TODO: Unblock processing 274 | } 275 | NSTimeInterval delta = cdl.duration; 276 | CGFloat diff = _targetOffset.x - _actualOffset.x; 277 | CGFloat movementPerSecond = CLAMP(abs(diff)*14, 20, 4000)*fsign(diff); 278 | CGFloat movement = movementPerSecond * delta; 279 | 280 | if (abs(movement) > abs(diff)) movement = diff; // so we never step over the target point 281 | 282 | _actualOffset.x += movement; 283 | [self setNeedsLayout]; 284 | } 285 | - (void)animateToTargetScrollOffset 286 | { 287 | if (_scrollAnimationTimer) return; 288 | _scrollDoneMargin = kScrollDoneMarginNormal; 289 | self.scrollAnimationTimer = [CADisplayLink displayLinkWithTarget:self selector:@selector(scrollAnimationFrame:)]; 290 | [_scrollAnimationTimer addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; 291 | // TODO: Block processing 292 | } 293 | 294 | 295 | - (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated 296 | { 297 | _targetOffset = contentOffset; 298 | if (animated) 299 | [self animateToTargetScrollOffset]; 300 | else { 301 | _actualOffset = _targetOffset; 302 | if (_onScrollDone) 303 | { 304 | self.onScrollDone(); 305 | self.onScrollDone = nil; 306 | } 307 | [self setNeedsLayout]; 308 | } 309 | } 310 | 311 | 312 | 313 | - (void)layoutSubviews 314 | { 315 | CGRect pen = CGRectZero; 316 | pen.origin.x = -_actualOffset.x; 317 | 318 | // stretch scroll at start and end 319 | if (_actualOffset.x < 0) 320 | pen.origin.x = -_actualOffset.x/2; 321 | CGFloat maxScroll = [self scrollOffsetForAligningPageWithRightEdge:self.subviews.lastObject]; 322 | if (_actualOffset.x > maxScroll) 323 | pen.origin.x = -(maxScroll + (_actualOffset.x-maxScroll)/2); 324 | 325 | int i = 0; 326 | CGFloat markedForSuperviewRemovalOffset = pen.origin.x; 327 | NSMutableArray *stackedViews = [NSMutableArray array]; 328 | for(SPStackedPageContainer *pageC in self.subviews) { 329 | pen.size = pageC.bounds.size; 330 | pen.size.height = self.frame.size.height; 331 | if (pageC.vc.stackedNavigationPageSize == kStackedPageFullSize) 332 | pen.size.width = self.frame.size.width; 333 | 334 | CGRect actualPen = pen; 335 | if (pageC.markedForSuperviewRemoval) 336 | actualPen.origin.x = markedForSuperviewRemovalOffset; 337 | // Stack on the left 338 | if (actualPen.origin.x < (MIN(i, 3))*3) 339 | [stackedViews addObject:pageC]; 340 | else 341 | pageC.hidden = NO; 342 | if (self.scrollAnimationTimer == nil) 343 | actualPen.origin.x = floorf(actualPen.origin.x); 344 | pageC.frame = actualPen; 345 | markedForSuperviewRemovalOffset += pen.size.width; 346 | if (!pageC.markedForSuperviewRemoval) 347 | pen.origin.x += pen.size.width; 348 | 349 | if (actualPen.origin.x <= 0 && pageC != [self.subviews lastObject]) { 350 | pageC.overlayOpacity = 0.3/actualPen.size.width*abs(actualPen.origin.x); 351 | } else { 352 | pageC.overlayOpacity = 0.0; 353 | } 354 | 355 | i++; 356 | } 357 | 358 | i = 0; 359 | for (NSInteger index = 0; index < [stackedViews count]; index++) 360 | { 361 | SPStackedPageContainer *pageC = stackedViews[index]; 362 | if ([stackedViews count] > 3 && index < ([stackedViews count]-3)) 363 | pageC.hidden = YES; 364 | else 365 | { 366 | pageC.hidden = NO; 367 | CGRect frame = pageC.frame; 368 | frame.origin.x = 0 + MIN(i, 3)*3; 369 | pageC.frame = frame; 370 | 371 | i++; 372 | } 373 | } 374 | 375 | // Only make sure we show what we need to, don't unload stuff until we're done animating 376 | [self updateContainerVisibilityByShowing:YES byHiding:NO]; 377 | } 378 | 379 | #pragma mark Visibility 380 | - (void)updateContainerVisibilityByShowing:(BOOL)doShow byHiding:(BOOL)doHide 381 | { 382 | BOOL bouncing = self.scrollAnimationTimer && fabsf(_targetOffset.x - _actualOffset.x) < 30; 383 | CGFloat pen = -_actualOffset.x; 384 | 385 | // stretch scroll at start and end 386 | if (_actualOffset.x < 0) 387 | pen = -_actualOffset.x/2; 388 | CGFloat maxScroll = [self scrollOffsetForAligningPageWithRightEdge:self.subviews.lastObject]; 389 | if (_actualOffset.x > maxScroll) 390 | pen = -(maxScroll + (_actualOffset.x-maxScroll)/2); 391 | 392 | CGFloat markedForSuperviewRemovalOffset = pen; 393 | NSMutableArray *viewsToDelete = [NSMutableArray array]; 394 | for(SPStackedPageContainer *pageC in self.subviews) { 395 | CGFloat currentPen = pen; 396 | if (pageC.markedForSuperviewRemoval) 397 | currentPen = markedForSuperviewRemovalOffset; 398 | 399 | BOOL isOffScreenToTheRight = currentPen >= self.bounds.size.width; 400 | NSRange scrollRange = [self scrollRangeForPageContainer:pageC]; 401 | BOOL isCovered = currentPen + scrollRange.length <= 0; 402 | BOOL isVisible = !isOffScreenToTheRight && !isCovered; 403 | 404 | if (pageC.VCVisible != isVisible && ((!isVisible && doHide) || (isVisible && doShow))) 405 | { 406 | if (!isVisible || !bouncing || (isVisible && pageC.needsInitialPresentation)) { 407 | pageC.needsInitialPresentation = NO; 408 | pageC.VCVisible = isVisible; 409 | } 410 | } 411 | if (doHide && pageC.markedForSuperviewRemoval) 412 | [viewsToDelete addObject:pageC]; 413 | markedForSuperviewRemovalOffset += pageC.frame.size.width; 414 | if (!pageC.markedForSuperviewRemoval) 415 | pen += pageC.frame.size.width; 416 | } 417 | 418 | [viewsToDelete makeObjectsPerformSelector:@selector(removeFromSuperview)]; 419 | } 420 | 421 | @end 422 | -------------------------------------------------------------------------------- /Sources/SPStackedPageContainer.h: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Spotify 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #import 16 | 17 | /// PRIVATE IMPLEMENTATION DETAIL of SPStackedNavigationController 18 | 19 | 20 | /// Holds a VC in the navigation stack, visual decorations, and info about it. 21 | /// It will also make sure to load/unload its view as needed when it appears/ 22 | /// disappears. 23 | @interface SPStackedPageContainer : UIView 24 | @property(nonatomic,retain) UIViewController *vc; 25 | @property(nonatomic,retain) UIView *vcContainer; 26 | @property(nonatomic) BOOL VCVisible; 27 | @property(nonatomic,retain) UIImageView *screenshot; 28 | @property(nonatomic) BOOL markedForSuperviewRemoval; 29 | @property(nonatomic) BOOL needsInitialPresentation; 30 | @property(nonatomic) CGFloat overlayOpacity; 31 | 32 | - (id)initWithFrame:(CGRect)frame VC:(UIViewController*)vc; 33 | @end 34 | -------------------------------------------------------------------------------- /Sources/SPStackedPageContainer.m: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Spotify 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #import "SPStackedPageContainer.h" 16 | #import "SPSeparatorView.h" 17 | #import 18 | #include 19 | #import "SPStackedNavigationController.h" 20 | 21 | // AKA "is too slow for transparency" 22 | static BOOL IsIPad11() { 23 | size_t size; 24 | sysctlbyname("hw.machine", NULL, &size, NULL, 0); 25 | char *machine = (char *)malloc(size); 26 | sysctlbyname("hw.machine", machine, &size, NULL, 0); 27 | NSString *ident = @(machine); 28 | free(machine); 29 | return [ident isEqualToString:@"iPad1,1"]; 30 | } 31 | 32 | @interface SPStackedPageContainer () 33 | { 34 | UIView * highlight; 35 | UIView * leftShadow; 36 | CALayer * _overlayLayer; 37 | } 38 | @end 39 | 40 | @implementation SPStackedPageContainer 41 | @synthesize vc = _vc; 42 | @synthesize vcContainer = _vcContainer; 43 | @synthesize screenshot = _screenshot; 44 | @synthesize markedForSuperviewRemoval = _markedForSuperviewRemoval; 45 | @synthesize needsInitialPresentation = _needsInitialPresentation; 46 | @synthesize overlayOpacity = _overlayOpacity; 47 | 48 | - (id)initWithFrame:(CGRect)frame VC:(UIViewController*)vc 49 | { 50 | if (!(self = [super initWithFrame:frame])) 51 | return nil; 52 | 53 | self.vc = vc; 54 | 55 | self.backgroundColor = [UIColor colorWithHue:0.167 saturation:0.017 brightness:0.925 alpha:1.000]; 56 | self.opaque = NO; 57 | 58 | if (!IsIPad11()) 59 | { 60 | for(NSString *img in @[@"stackShadow.png", @"stackShadow-right.png"]) { 61 | UIImage *shadowImage = [[UIImage imageNamed:img] resizableImageWithCapInsets:UIEdgeInsetsMake(12, 0, 12, 0)]; 62 | BOOL left = [img rangeOfString:@"right"].location == NSNotFound; 63 | UIImageView *shadow = [[UIImageView alloc] initWithFrame:CGRectMake(left ? -13 : self.bounds.size.width, 0, 13, self.bounds.size.height)]; 64 | shadow.image = shadowImage; 65 | shadow.autoresizingMask = UIViewAutoresizingFlexibleHeight|(left?UIViewAutoresizingFlexibleRightMargin:UIViewAutoresizingFlexibleLeftMargin); 66 | shadow.userInteractionEnabled = NO; 67 | [self addSubview:shadow]; 68 | if (!leftShadow) 69 | leftShadow = shadow; 70 | } 71 | } 72 | else 73 | { 74 | UIColor *color1 = [UIColor colorWithRed:0x87/255. green:0x86/255. blue:0x84/255. alpha:1]; 75 | UIColor *color2 = [UIColor colorWithRed:0xcf/255. green:0xce/255. blue:0xcb/255. alpha:1]; 76 | 77 | SPSeparatorView *separatorView = [[SPSeparatorView alloc] initWithFrame:CGRectMake(-2, 0, 2, self.bounds.size.height)]; 78 | separatorView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleRightMargin; 79 | separatorView.style = SPSeparatorStyleSingleLineEtched; 80 | separatorView.colors = @[color2, 81 | color1]; 82 | [self addSubview:separatorView]; 83 | leftShadow = separatorView; 84 | 85 | separatorView = [[SPSeparatorView alloc] initWithFrame:CGRectMake(self.bounds.size.width, 0, 2, self.bounds.size.height)]; 86 | separatorView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleLeftMargin; 87 | separatorView.style = SPSeparatorStyleSingleLineEtched; 88 | separatorView.colors = @[color1, 89 | color2]; 90 | [self addSubview:separatorView]; 91 | } 92 | 93 | _vcContainer = [[UIView alloc] initWithFrame:self.bounds]; 94 | _vcContainer.autoresizingMask = UIViewAutoresizingFlexibleHeight|UIViewAutoresizingFlexibleWidth; 95 | [self addSubview:_vcContainer]; 96 | 97 | highlight = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.bounds.size.width, 1)]; 98 | highlight.backgroundColor = [UIColor colorWithWhite:1 alpha:.46]; 99 | highlight.autoresizingMask = UIViewAutoresizingFlexibleWidth; 100 | highlight.userInteractionEnabled = NO; 101 | [_vcContainer addSubview:highlight]; 102 | 103 | UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleGestureRecognizer:)]; 104 | panGestureRecognizer.delegate = self; 105 | panGestureRecognizer.cancelsTouchesInView = NO; 106 | panGestureRecognizer.delaysTouchesBegan = NO; 107 | panGestureRecognizer.delaysTouchesEnded = NO; 108 | [self addGestureRecognizer:panGestureRecognizer]; 109 | 110 | UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleGestureRecognizer:)]; 111 | tapGestureRecognizer.delegate = self; 112 | tapGestureRecognizer.cancelsTouchesInView = NO; 113 | tapGestureRecognizer.delaysTouchesBegan = NO; 114 | tapGestureRecognizer.delaysTouchesEnded = NO; 115 | [self addGestureRecognizer:tapGestureRecognizer]; 116 | 117 | self.needsInitialPresentation = YES; 118 | 119 | if (!IsIPad11()) { 120 | _overlayLayer = [CALayer layer]; 121 | _overlayLayer.frame = _vcContainer.bounds; 122 | _overlayLayer.backgroundColor = [[UIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:1.0] CGColor]; 123 | _overlayLayer.opacity = 0.0; 124 | [_vcContainer.layer addSublayer:_overlayLayer]; 125 | } 126 | 127 | return self; 128 | } 129 | 130 | 131 | - (BOOL)isEqual:(id)object 132 | { 133 | return [super isEqual:object] || [_vc isEqual:object] || 134 | ([object isKindOfClass:[self class]] && [self.vc isEqual: [object vc]]); 135 | } 136 | - (NSString*)description 137 | { 138 | return [NSString stringWithFormat:@"<%@ %p: %@>", NSStringFromClass(self.class), self, self.vc]; 139 | } 140 | - (void)setVCVisible:(BOOL)VCVisible 141 | { 142 | if (VCVisible == self.VCVisible) return; 143 | 144 | if (VCVisible) { 145 | [self.screenshot removeFromSuperview]; 146 | self.screenshot = nil; 147 | if (!self.markedForSuperviewRemoval || [_vc isViewLoaded]) 148 | { 149 | _vcContainer.backgroundColor = _vc.view.backgroundColor; 150 | _vc.view.frame = CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height); 151 | if (!_vc.view.superview) 152 | [_vcContainer insertSubview:_vc.view atIndex:0]; 153 | } 154 | } else { 155 | //self.screenshot = [[[UIImageView alloc] initWithImage:_vc.view.sp_screenshot] autorelease]; 156 | if ([_vc isViewLoaded]) 157 | [_vc.view removeFromSuperview]; 158 | //[_vcContainer insertSubview:_screenshot atIndex:0]; 159 | } 160 | } 161 | - (BOOL)VCVisible 162 | { 163 | return _vc.isViewLoaded && _vc.view.superview; 164 | } 165 | 166 | - (void)setFrame:(CGRect)frame 167 | { 168 | [super setFrame:frame]; 169 | leftShadow.hidden = (frame.origin.x == 0); 170 | } 171 | 172 | - (void)layoutSubviews 173 | { 174 | if ([_vc isViewLoaded]) 175 | _vc.view.frame = CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height); 176 | highlight.frame = CGRectMake(0, 0, self.bounds.size.width, 1); 177 | } 178 | 179 | - (void)handleGestureRecognizer:(UIGestureRecognizer*)gestureRecognizer 180 | { 181 | if (([gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]] && [gestureRecognizer state] == UIGestureRecognizerStateEnded) || 182 | ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]] && [gestureRecognizer state] == UIGestureRecognizerStateChanged)) 183 | [self.vc activateInStackedNavigationAnimated:YES]; 184 | } 185 | 186 | - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch 187 | { 188 | return ![(id)self.vc isActiveInStackedNavigation]; 189 | } 190 | 191 | - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer 192 | { 193 | return YES; 194 | } 195 | 196 | - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer 197 | { 198 | if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) 199 | { 200 | CGPoint translation = [(UIPanGestureRecognizer*)gestureRecognizer translationInView:[gestureRecognizer view]]; 201 | return (fabsf(translation.y) > fabsf(translation.x)); 202 | } 203 | return YES; 204 | } 205 | 206 | 207 | #pragma mark - Overlay opacity 208 | - (CGFloat)overlayOpacity 209 | { 210 | return _overlayLayer.opacity; 211 | } 212 | 213 | - (void)setOverlayOpacity:(CGFloat)overlayOpacity 214 | { 215 | [CATransaction begin]; 216 | [CATransaction setDisableActions:YES]; 217 | _overlayLayer.opacity = overlayOpacity; 218 | [CATransaction commit]; 219 | } 220 | 221 | @end 222 | -------------------------------------------------------------------------------- /Sources/UIImage+SPTabBarImage.h: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Spotify 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #import 16 | 17 | @interface UIImage (SPTabBarImage) 18 | - (UIImage*)sp_imageForTabBar; 19 | - (UIImage*)sp_selectedImageForTabBar; 20 | - (UIImage*)sp_selectedAndHighlightedImageForTabBar; 21 | @end 22 | -------------------------------------------------------------------------------- /Sources/UIImage+SPTabBarImage.m: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Spotify 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #import "UIImage+SPTabBarImage.h" 16 | 17 | #define $hx(x) ((x)/(float)0xff) 18 | #define $hexcolor(x) [UIColor colorWithRed:$hx(x>>16) green:$hx((x>>8) & 0xff) blue:$hx(x&0xff) alpha:1] 19 | 20 | static inline CGRect SPRectCenterInRectSize(CGRect rect, CGSize containerSize) { 21 | CGSize delta = CGSizeMake( 22 | (containerSize.width - rect.size.width) / 2, 23 | (containerSize.height - rect.size.height) / 2 24 | ); 25 | 26 | return CGRectIntegral(CGRectOffset(rect, delta.width, delta.height)); 27 | } 28 | 29 | enum 30 | { 31 | SPTabBarImageStateNormal = 0, 32 | SPTabBarImageStateSelected, 33 | SPTabBarImageStateSelectedHiglighted 34 | }; 35 | typedef NSInteger SPTabBarImageState; 36 | 37 | static UIImage *SPMakeTabBarImage(UIImage *source, SPTabBarImageState state) 38 | { 39 | if (!source) return nil; 40 | 41 | UIImage *normalOverlay = [UIImage imageNamed:@"SPSideTabBar-button-overlay+normal.png"]; 42 | UIImage *shinyOverlay = [UIImage imageNamed:@"SPSideTabBar-button-overlay+selected.png"]; 43 | UIImage *shinyHighlightedOverlay = [UIImage imageNamed:@"SPSideTabBar-button-overlay+selected+pressed.png"]; 44 | UIImage *overlay = nil; 45 | switch (state) 46 | { 47 | case SPTabBarImageStateNormal: overlay = normalOverlay; break; 48 | case SPTabBarImageStateSelected: overlay = shinyOverlay; break; 49 | case SPTabBarImageStateSelectedHiglighted: overlay = shinyHighlightedOverlay; break; 50 | default: @throw([NSException exceptionWithName:NSCocoaErrorDomain reason:@"Invalid SPTabBarImageState" userInfo:nil]); 51 | } 52 | 53 | CGRect r = {.size={50,50}}; 54 | CGRect centeredIcon = SPRectCenterInRectSize((CGRect){.size=source.size}, r.size); 55 | centeredIcon.origin = (CGPoint){(r.size.width-centeredIcon.size.width)/2, (r.size.height-centeredIcon.size.height)/2}; 56 | centeredIcon = CGRectIntegral(centeredIcon); 57 | 58 | UIGraphicsBeginImageContextWithOptions(r.size, NO, [[UIScreen mainScreen] scale]); 59 | CGContextRef ctx = UIGraphicsGetCurrentContext(); 60 | if (!ctx) return nil; 61 | 62 | 63 | if (state > SPTabBarImageStateNormal) { // Draw glow if selected 64 | CGContextSetShadowWithColor(ctx, (CGSize){0,0}, 10, [UIColor colorWithHue:0.246 saturation:0.622 brightness:0.527 alpha:1.0].CGColor); 65 | [source drawInRect:centeredIcon]; 66 | CGContextSetShadowWithColor(ctx, (CGSize){0,0}, 0, 0); 67 | } 68 | 69 | // Draw outline 70 | CGContextSaveGState(ctx); 71 | CGContextTranslateCTM(ctx, 0, r.size.height); 72 | CGContextScaleCTM(ctx, 1.0, -1.0); 73 | CGContextSetAlpha(ctx, .6); 74 | CGContextDrawImage(ctx, CGRectOffset(centeredIcon, -1, 0), source.CGImage); 75 | CGContextDrawImage(ctx, CGRectOffset(centeredIcon, 1, 0), source.CGImage); 76 | CGContextDrawImage(ctx, CGRectOffset(centeredIcon, 0, 1), source.CGImage); 77 | // And a bottom highlight "shadow" below the shape 78 | CGContextSetShadowWithColor(ctx, CGSizeMake(0, 1), 0, [UIColor colorWithWhite:1 alpha:.05].CGColor); 79 | CGContextDrawImage(ctx, CGRectOffset(centeredIcon, 0, -1), source.CGImage); 80 | CGContextRestoreGState(ctx); 81 | 82 | 83 | // Draw gradient + main shape 84 | UIImage *gradientedImage = nil; 85 | { 86 | UIGraphicsBeginImageContextWithOptions(r.size, NO, [[UIScreen mainScreen] scale]); 87 | CGContextRef ctx2 = UIGraphicsGetCurrentContext(); 88 | 89 | [source drawInRect:centeredIcon]; 90 | 91 | CGContextSetBlendMode(ctx2, kCGBlendModeSourceIn); 92 | CGContextTranslateCTM(ctx2, 0, r.size.height); 93 | CGContextScaleCTM(ctx2, 1.0, -1.0); 94 | 95 | CGContextDrawImage(ctx2, r, overlay.CGImage); 96 | 97 | gradientedImage = UIGraphicsGetImageFromCurrentImageContext(); 98 | UIGraphicsEndImageContext(); 99 | } 100 | [gradientedImage drawInRect:r]; 101 | 102 | // Draw highlight 103 | CGImageRef highlight = nil; 104 | { 105 | struct __attribute__((packed)) Pixel { 106 | union { 107 | struct {char r, g, b, a;} col; 108 | char v[4]; 109 | } reps; 110 | }; 111 | struct Pixel pixels[(int)(r.size.width*r.size.height)]; 112 | memset(pixels, 0, sizeof(pixels)); 113 | CGColorSpaceRef cspace = CGColorSpaceCreateDeviceRGB(); 114 | CGContextRef maskContext = CGBitmapContextCreate(pixels, r.size.width, r.size.height, 8, r.size.width*4, cspace, (CGBitmapInfo)kCGImageAlphaPremultipliedLast); 115 | CGColorSpaceRelease(cspace); 116 | CGContextTranslateCTM(maskContext, 0, r.size.height); 117 | CGContextScaleCTM(maskContext, 1.0, -1.0); 118 | 119 | CGContextDrawImage(maskContext, CGRectOffset(centeredIcon, 0, 1), source.CGImage); 120 | CGContextSetBlendMode(maskContext, kCGBlendModeSourceOut); 121 | CGContextDrawImage(maskContext, CGRectOffset(centeredIcon, 0, 2), source.CGImage); 122 | 123 | // black > white x_x 124 | for(int i = 0, c = r.size.width*r.size.height; i < c; i++) { 125 | float a = pixels[i].reps.col.a/(float)0xff; 126 | for(int j = 0; j < 3; j++) { 127 | float v = pixels[i].reps.v[j]/(float)0xff; // depremultiply and shift to normalized range 128 | v = 1.0 - v; // do the change 129 | pixels[i].reps.v[j] = (v * a) * 0xff; // repremultiply and shift back to char range 130 | } 131 | } 132 | 133 | UIColor *highlightColor = (state > SPTabBarImageStateNormal) ? $hexcolor(0xd6ff59) : [UIColor colorWithWhite:1 alpha:.45]; 134 | CGContextSetBlendMode(maskContext, kCGBlendModeSourceIn); 135 | CGContextSetFillColorWithColor(maskContext, highlightColor.CGColor); 136 | CGContextFillRect(maskContext, r); 137 | 138 | highlight = CGBitmapContextCreateImage(maskContext); 139 | CGContextRelease(maskContext); 140 | } 141 | CGContextDrawImage(ctx, CGRectOffset(r, 0, 2), highlight); 142 | CGImageRelease(highlight); 143 | 144 | 145 | UIImage *result = UIGraphicsGetImageFromCurrentImageContext(); 146 | UIGraphicsEndImageContext(); 147 | return result; 148 | } 149 | 150 | 151 | @implementation UIImage (SPTabBarImage) 152 | - (UIImage*)sp_imageForTabBar 153 | { 154 | return SPMakeTabBarImage(self, SPTabBarImageStateNormal); 155 | } 156 | - (UIImage*)sp_selectedImageForTabBar 157 | { 158 | return SPMakeTabBarImage(self, SPTabBarImageStateSelected); 159 | } 160 | - (UIImage*)sp_selectedAndHighlightedImageForTabBar 161 | { 162 | return SPMakeTabBarImage(self, SPTabBarImageStateSelectedHiglighted); 163 | } 164 | @end -------------------------------------------------------------------------------- /include/SPStackedNav/SPSideTabBar.h: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Spotify 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #import 16 | @protocol SPSideTabBarDelegate; 17 | 18 | @interface SPSideTabBar : UIView 19 | - (id)initWithFrame:(CGRect)r; 20 | @property(nonatomic,assign) id delegate; // weak reference. default is nil 21 | @property(nonatomic,copy) NSArray *items; // get/set visible UITabBarItems. default is nil. changes not animated. shown in order 22 | @property(nonatomic,retain) UITabBarItem *selectedItem; 23 | @property(nonatomic,copy) NSArray *additionalItems; // shown starting from the bottom, not associated with a view controller 24 | - (void)select:(BOOL)selected additionalItem:(UITabBarItem*)item; 25 | - (CGRect)rectForItem:(UITabBarItem*)item; 26 | @end 27 | 28 | 29 | @protocol SPSideTabBarDelegate 30 | @optional 31 | - (void)tabBar:(SPSideTabBar *)tabBar didSelectItem:(UITabBarItem *)item; // called when a new view is selected by the user (but not programatically) 32 | @end 33 | -------------------------------------------------------------------------------- /include/SPStackedNav/SPSideTabController.h: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Spotify 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #import 16 | #import 17 | 18 | @interface SPSideTabController : UIViewController 19 | 20 | @property(nonatomic,copy) NSArray *viewControllers; 21 | @property(nonatomic,retain) UIViewController *selectedViewController; // This may return the "More" navigation controller if it exists. 22 | @property(nonatomic) NSUInteger selectedIndex; 23 | @property(nonatomic,retain) UIViewController *bottomAttachment; 24 | @property(nonatomic,readonly,retain) SPSideTabBar *tabBar; 25 | @property(nonatomic,assign) id tabBarDelegate; // is forwarded additionalItems 26 | @property(nonatomic,retain) NSArray *additionalItems; 27 | 28 | - (BOOL)isBottomAttachmentHidden; 29 | - (void)setBottomAttachmentHidden:(BOOL)hidden animated:(BOOL)animated; 30 | @end 31 | 32 | /** 33 | * SPTabBarItem is a UITabBarItem subclass that adds SPSideTabController specific functionality. 34 | */ 35 | @interface SPTabBarItem : UITabBarItem 36 | - (id)initWithTitle:(NSString *)title imageName:(NSString*)imageName tag:(NSInteger)tag; 37 | 38 | 39 | /// SPSideTabController will present this view (if available) instead of the normal button (default = nil). 40 | @property(nonatomic,strong) UIView *view; 41 | 42 | /// instead of -image, if you want to use pre-composited images in the form "{name}-{state(s)}-tb.png" 43 | @property(nonatomic,copy) NSString *imageName; 44 | @end 45 | -------------------------------------------------------------------------------- /include/SPStackedNav/SPStackedNav.h: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Spotify 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // File created by Joachim Bengtsson on 2012-10-27. 16 | 17 | #import 18 | #import -------------------------------------------------------------------------------- /include/SPStackedNav/SPStackedNavigationController.h: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Spotify 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | 16 | #import 17 | 18 | typedef enum 19 | { 20 | SPStackedNavigationPagePositionLeft = 0, 21 | SPStackedNavigationPagePositionRight, 22 | } SPStackedNavigationPagePosition; 23 | 24 | /** 25 | @class SPStackedNavigationController 26 | @author nevyn@spotify.com 27 | 28 | @abstract "Twitter for iPad"-style navigation, where you have panes of content that 29 | you can scroll between by swiping sideways, often having more than one pane 30 | visible at once. API-wise, it behaves almost exactly like a UINavigationController, 31 | except you can push VCs in the middle of it. 32 | */ 33 | @interface SPStackedNavigationController : UIViewController 34 | - (id)initWithRootViewController:(UIViewController *)rootViewController; 35 | 36 | // activate specifies whether the pushed view controller should become the active view controller or not 37 | - (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated; // activate = YES 38 | - (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated activate:(BOOL)activate; 39 | // replace everything on the stack after 'parent' with 'viewController' 40 | - (void)pushViewController:(UIViewController *)viewController onTopOf:(UIViewController*)parent animated:(BOOL)animated; // activate = YES 41 | - (void)pushViewController:(UIViewController *)viewController onTopOf:(UIViewController*)parent animated:(BOOL)animated activate:(BOOL)activate; 42 | 43 | - (UIViewController *)popViewControllerAnimated:(BOOL)animated; 44 | - (NSArray *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated; 45 | - (NSArray *)popToRootViewControllerAnimated:(BOOL)animated; 46 | 47 | /// Top of the stack 48 | @property(nonatomic,readonly) UIViewController *topViewController; 49 | /// The current active view controller 50 | /// Positioned to the right or left as specified in -activeViewControllerPagePosition. 51 | @property(nonatomic,retain,readonly) UIViewController *activeViewController; 52 | /// Active view controller position 53 | /// Aligns the active view controller to the specified edge of the screen (left or right). 54 | /// Preserved between orientation changes. 55 | @property(nonatomic,assign,readonly) SPStackedNavigationPagePosition activeViewControllerPagePosition; 56 | - (void)setActiveViewController:(UIViewController*)viewController animated:(BOOL)animated; // automatically calculates position 57 | - (void)setActiveViewController:(UIViewController *)viewController position:(SPStackedNavigationPagePosition)position animated:(BOOL)animated; 58 | 59 | /// Modal if it exists, otherwise active 60 | - (NSArray *)visibleViewControllers; 61 | @property(nonatomic,copy) NSArray *viewControllers; 62 | - (void)setViewControllers:(NSArray *)viewControllers animated:(BOOL)animated; 63 | 64 | // The scroll views pan gesture recognizer. 65 | // Will load the view if accessed, so make sure to check -isViewLoaded first. 66 | - (UIGestureRecognizer*)panGestureRecognizer; 67 | @end 68 | 69 | 70 | 71 | 72 | static const CGFloat kSPStackedNavigationHalfPageWidth = 472; 73 | 74 | enum { 75 | /// Fills the width of the screen in landscape (excluding sidebar) 76 | kStackedPageHalfSize = 1, 77 | /// Uses the full available width 78 | kStackedPageFullSize = 2 79 | }; 80 | typedef int SPStackedNavigationPageSize; 81 | 82 | /// Informal protocol used to optionally customize the behavior of child VCs 83 | /// to a SPStackedNavigationController. 84 | @interface NSObject (SPStackedNavigationChild) 85 | /// How much width does this VC use when pushed on a SPStackedNavigationController? 86 | /// Default kStackedPageFullSize 87 | - (SPStackedNavigationPageSize)stackedNavigationPageSize; 88 | @end 89 | 90 | /// Up-accessors from child VCs 91 | @interface UIViewController (SPStackedNavigationControllerItem) 92 | @property(nonatomic,readonly) SPStackedNavigationController *stackedNavigationController; // If this view controller has been pushed onto a navigation controller, return it. 93 | 94 | // Sent to child view controllers when active view controller is changed 95 | - (void)viewDidBecomeActiveInStackedNavigation; 96 | - (void)viewDidBecomeInactiveInStackedNavigation; 97 | 98 | // Calls -[SPStackedNavigationController setActiveViewController:animated:] 99 | // Used to activate yourself when user interacts with a managed view (scrolling, opening a context menu, playing a track). 100 | - (void)activateInStackedNavigationAnimated:(BOOL)animated; 101 | 102 | // Returns true if this is the currently active view controller. 103 | - (BOOL)isActiveInStackedNavigation; 104 | @end 105 | 106 | @interface UINavigationController (SPStackedNavigationControllerCompatibility) 107 | - (void)pushViewController:(UIViewController *)viewController onTopOf:(UIViewController*)parent animated:(BOOL)animated; 108 | @end --------------------------------------------------------------------------------