├── .gitignore ├── .travis.yml ├── Assets ├── card_shadow.png └── card_shine.png ├── CONTRIBUTING.md ├── CardStack.podspec ├── Images └── screenshot.png ├── LICENSE.md ├── Pod ├── Pod.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ ├── Pod.xcscheme │ │ └── Tests.xcscheme ├── Pod.xcworkspace │ └── contents.xcworkspacedata ├── Pod │ ├── AppDelegate │ │ ├── AppDelegate.h │ │ ├── AppDelegate.m │ │ ├── ResizeableViewController.h │ │ └── ResizeableViewController.m │ ├── Images.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── LaunchImage.launchimage │ │ │ └── Contents.json │ ├── Info.plist │ └── main.m ├── Podfile ├── Podfile.lock └── Tests │ ├── Info.plist │ └── Tests.m ├── README.md └── Source ├── CardStackController.h ├── CardStackController.m ├── CardView.h └── CardView.m /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | .AppleDouble 4 | .LSOverride 5 | Icon 6 | ._* 7 | .Spotlight-V100 8 | .Trashes 9 | 10 | # Xcode 11 | # 12 | build/ 13 | *.pbxuser 14 | !default.pbxuser 15 | *.mode1v3 16 | !default.mode1v3 17 | *.mode2v3 18 | !default.mode2v3 19 | *.perspectivev3 20 | !default.perspectivev3 21 | xcuserdata 22 | *.xccheckout 23 | *.moved-aside 24 | DerivedData 25 | *.hmap 26 | *.ipa 27 | *.xcuserstate 28 | 29 | # CocoaPods 30 | Pods 31 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | cache: cocoapods 3 | before_install: gem install cocoapods obcd slather -N 4 | script: xctool -project Pod/Pod.xcodeproj -scheme Tests -sdk iphonesimulator build test GCC_INSTRUMENT_PROGRAM_FLOW_ARCS=YES GCC_GENERATE_TEST_COVERAGE_FILES=YES clean test 5 | podfile: Pod/Podfile 6 | script: xctool -workspace Pod/Pod.xcworkspace -scheme Tests -sdk iphonesimulator build test GCC_INSTRUMENT_PROGRAM_FLOW_ARCS=YES GCC_GENERATE_TEST_COVERAGE_FILES=YES clean test 7 | after_success: slather 8 | -------------------------------------------------------------------------------- /Assets/card_shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperoslo/CardStack/36bb47fc2999b5d9f604ee446851d342649d58e9/Assets/card_shadow.png -------------------------------------------------------------------------------- /Assets/card_shine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperoslo/CardStack/36bb47fc2999b5d9f604ee446851d342649d58e9/Assets/card_shine.png -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | GitHub Issues is for reporting bugs, discussing features and general feedback in **CardStack**. Be sure to check our [documentation](http://cocoadocs.org/docsets/CardStack), [FAQ](https://github.com/hyperoslo/CardStack/wiki/FAQ) and [past issues](https://github.com/hyperoslo/CardStack/issues?state=closed) before opening any new issues. 2 | 3 | If you are posting about a crash in your application, a stack trace is helpful, but additional context, in the form of code and explanation, is necessary to be of any use. 4 | -------------------------------------------------------------------------------- /CardStack.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "CardStack" 3 | s.summary = "A container view controller implementing a stack of 'cards' (each card is a view controller)" 4 | s.version = "0.2" 5 | s.homepage = "https://github.com/hyperoslo/CardStack" 6 | s.license = 'MIT' 7 | s.author = { "Hyper Interaktiv AS" => "ios@hyper.no" } 8 | s.source = { :git => "https://github.com/hyperoslo/CardStack.git", :tag => s.version.to_s } 9 | s.resource_bundles = { 'CardStack' => ['Assets/*.{png}'] } 10 | s.social_media_url = 'https://twitter.com/hyperoslo' 11 | s.platform = :ios, '7.0' 12 | s.requires_arc = true 13 | s.source_files = 'Source/**/*' 14 | s.dependency 'pop', '~> 1.0' 15 | s.dependency 'Masonry', '~> 0.6' 16 | end 17 | -------------------------------------------------------------------------------- /Images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperoslo/CardStack/36bb47fc2999b5d9f604ee446851d342649d58e9/Images/screenshot.png -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Licensed under the **MIT** license 2 | 3 | > Copyright (c) 2015 Hyper Interaktiv AS 4 | > 5 | > Permission is hereby granted, free of charge, to any person obtaining 6 | > a copy of this software and associated documentation files (the 7 | > "Software"), to deal in the Software without restriction, including 8 | > without limitation the rights to use, copy, modify, merge, publish, 9 | > distribute, sublicense, and/or sell copies of the Software, and to 10 | > permit persons to whom the Software is furnished to do so, subject to 11 | > the following conditions: 12 | > 13 | > The above copyright notice and this permission notice shall be 14 | > included in all copies or substantial portions of the Software. 15 | > 16 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | > EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | > MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | > IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | > CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | > TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | > SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Pod/Pod.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1450496F1AB7858300D9745F /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 1450496E1AB7858300D9745F /* AppDelegate.m */; }; 11 | 146D72991AB782920058798C /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 146D72981AB782920058798C /* main.m */; }; 12 | 146D72A41AB782920058798C /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 146D72A31AB782920058798C /* Images.xcassets */; }; 13 | 146D72B31AB782920058798C /* Tests.m in Sources */ = {isa = PBXBuildFile; fileRef = 146D72B21AB782920058798C /* Tests.m */; }; 14 | 147872E71AE542020091AE1D /* CardView.m in Sources */ = {isa = PBXBuildFile; fileRef = 147872E61AE542020091AE1D /* CardView.m */; }; 15 | 332C1F8F69719E0E7DE34A23 /* libPods.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 47DD127F087D98A0D25096AC /* libPods.a */; }; 16 | 9A3286291AF0C9C700149E6C /* CardView.m in Sources */ = {isa = PBXBuildFile; fileRef = 147872E61AE542020091AE1D /* CardView.m */; }; 17 | 9A32862A1AF0C9CC00149E6C /* CardStackController.m in Sources */ = {isa = PBXBuildFile; fileRef = 9AC5E53F1ADC009300FE1E0D /* CardStackController.m */; }; 18 | 9A3E19BA1B0CDDDB00A0FAF3 /* ResizeableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 9A3E19B91B0CDDDB00A0FAF3 /* ResizeableViewController.m */; }; 19 | 9A3F16FF1AF228E100229577 /* card_shadow.png in Resources */ = {isa = PBXBuildFile; fileRef = 9A3F16FD1AF228E100229577 /* card_shadow.png */; }; 20 | 9A3F17001AF228E100229577 /* card_shine.png in Resources */ = {isa = PBXBuildFile; fileRef = 9A3F16FE1AF228E100229577 /* card_shine.png */; }; 21 | 9AC5E5401ADC009300FE1E0D /* CardStackController.m in Sources */ = {isa = PBXBuildFile; fileRef = 9AC5E53F1ADC009300FE1E0D /* CardStackController.m */; }; 22 | F1A5A8384FE327BFD1A5E2CC /* libPods.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 47DD127F087D98A0D25096AC /* libPods.a */; }; 23 | /* End PBXBuildFile section */ 24 | 25 | /* Begin PBXContainerItemProxy section */ 26 | 146D72AD1AB782920058798C /* PBXContainerItemProxy */ = { 27 | isa = PBXContainerItemProxy; 28 | containerPortal = 146D728B1AB782920058798C /* Project object */; 29 | proxyType = 1; 30 | remoteGlobalIDString = 146D72921AB782920058798C; 31 | remoteInfo = Pod; 32 | }; 33 | /* End PBXContainerItemProxy section */ 34 | 35 | /* Begin PBXFileReference section */ 36 | 1450496D1AB7858300D9745F /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 37 | 1450496E1AB7858300D9745F /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 38 | 145049711AB785A800D9745F /* .travis.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = .travis.yml; path = ../.travis.yml; sourceTree = ""; }; 39 | 145049721AB785A800D9745F /* CONTRIBUTING.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = CONTRIBUTING.md; path = ../CONTRIBUTING.md; sourceTree = ""; }; 40 | 145049731AB785A800D9745F /* LICENSE.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = LICENSE.md; path = ../LICENSE.md; sourceTree = ""; }; 41 | 145049741AB785A800D9745F /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; 42 | 146D72931AB782920058798C /* Pod.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Pod.app; sourceTree = BUILT_PRODUCTS_DIR; }; 43 | 146D72971AB782920058798C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 44 | 146D72981AB782920058798C /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 45 | 146D72A31AB782920058798C /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 46 | 146D72AC1AB782920058798C /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 47 | 146D72B11AB782920058798C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 48 | 146D72B21AB782920058798C /* Tests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Tests.m; sourceTree = ""; }; 49 | 147872E51AE542020091AE1D /* CardView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CardView.h; sourceTree = ""; }; 50 | 147872E61AE542020091AE1D /* CardView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CardView.m; sourceTree = ""; }; 51 | 14946611115390DFF8D31297 /* Pods.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.release.xcconfig; path = "Pods/Target Support Files/Pods/Pods.release.xcconfig"; sourceTree = ""; }; 52 | 31CB67FA653DFCAE806222A3 /* Pods.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.debug.xcconfig; path = "Pods/Target Support Files/Pods/Pods.debug.xcconfig"; sourceTree = ""; }; 53 | 47DD127F087D98A0D25096AC /* libPods.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libPods.a; sourceTree = BUILT_PRODUCTS_DIR; }; 54 | 9A3E19B81B0CDDDB00A0FAF3 /* ResizeableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ResizeableViewController.h; sourceTree = ""; }; 55 | 9A3E19B91B0CDDDB00A0FAF3 /* ResizeableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ResizeableViewController.m; sourceTree = ""; }; 56 | 9A3F16FD1AF228E100229577 /* card_shadow.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = card_shadow.png; sourceTree = ""; }; 57 | 9A3F16FE1AF228E100229577 /* card_shine.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = card_shine.png; sourceTree = ""; }; 58 | 9AC5E53E1ADC009300FE1E0D /* CardStackController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CardStackController.h; sourceTree = ""; }; 59 | 9AC5E53F1ADC009300FE1E0D /* CardStackController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CardStackController.m; sourceTree = ""; }; 60 | /* End PBXFileReference section */ 61 | 62 | /* Begin PBXFrameworksBuildPhase section */ 63 | 146D72901AB782920058798C /* Frameworks */ = { 64 | isa = PBXFrameworksBuildPhase; 65 | buildActionMask = 2147483647; 66 | files = ( 67 | 332C1F8F69719E0E7DE34A23 /* libPods.a in Frameworks */, 68 | ); 69 | runOnlyForDeploymentPostprocessing = 0; 70 | }; 71 | 146D72A91AB782920058798C /* Frameworks */ = { 72 | isa = PBXFrameworksBuildPhase; 73 | buildActionMask = 2147483647; 74 | files = ( 75 | F1A5A8384FE327BFD1A5E2CC /* libPods.a in Frameworks */, 76 | ); 77 | runOnlyForDeploymentPostprocessing = 0; 78 | }; 79 | /* End PBXFrameworksBuildPhase section */ 80 | 81 | /* Begin PBXGroup section */ 82 | 10AC5E1AB4B1CA7A9D954FE2 /* Pods */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | 31CB67FA653DFCAE806222A3 /* Pods.debug.xcconfig */, 86 | 14946611115390DFF8D31297 /* Pods.release.xcconfig */, 87 | ); 88 | name = Pods; 89 | sourceTree = ""; 90 | }; 91 | 1450496C1AB7858300D9745F /* AppDelegate */ = { 92 | isa = PBXGroup; 93 | children = ( 94 | 1450496D1AB7858300D9745F /* AppDelegate.h */, 95 | 1450496E1AB7858300D9745F /* AppDelegate.m */, 96 | 9A3E19B81B0CDDDB00A0FAF3 /* ResizeableViewController.h */, 97 | 9A3E19B91B0CDDDB00A0FAF3 /* ResizeableViewController.m */, 98 | ); 99 | path = AppDelegate; 100 | sourceTree = ""; 101 | }; 102 | 145049701AB7859600D9745F /* Metadata */ = { 103 | isa = PBXGroup; 104 | children = ( 105 | 145049711AB785A800D9745F /* .travis.yml */, 106 | 145049721AB785A800D9745F /* CONTRIBUTING.md */, 107 | 145049731AB785A800D9745F /* LICENSE.md */, 108 | 145049741AB785A800D9745F /* README.md */, 109 | ); 110 | name = Metadata; 111 | sourceTree = ""; 112 | }; 113 | 145049791AB785C700D9745F /* Source */ = { 114 | isa = PBXGroup; 115 | children = ( 116 | 9AC5E53E1ADC009300FE1E0D /* CardStackController.h */, 117 | 9AC5E53F1ADC009300FE1E0D /* CardStackController.m */, 118 | 147872E51AE542020091AE1D /* CardView.h */, 119 | 147872E61AE542020091AE1D /* CardView.m */, 120 | ); 121 | name = Source; 122 | path = ../Source; 123 | sourceTree = ""; 124 | }; 125 | 146D728A1AB782920058798C = { 126 | isa = PBXGroup; 127 | children = ( 128 | 9A3F16FC1AF228E100229577 /* Assets */, 129 | 145049791AB785C700D9745F /* Source */, 130 | 145049701AB7859600D9745F /* Metadata */, 131 | 146D72951AB782920058798C /* Pod */, 132 | 146D72AF1AB782920058798C /* Tests */, 133 | 146D72941AB782920058798C /* Products */, 134 | D0AEA67F93773348378C9B9A /* Frameworks */, 135 | 10AC5E1AB4B1CA7A9D954FE2 /* Pods */, 136 | ); 137 | sourceTree = ""; 138 | }; 139 | 146D72941AB782920058798C /* Products */ = { 140 | isa = PBXGroup; 141 | children = ( 142 | 146D72931AB782920058798C /* Pod.app */, 143 | 146D72AC1AB782920058798C /* Tests.xctest */, 144 | ); 145 | name = Products; 146 | sourceTree = ""; 147 | }; 148 | 146D72951AB782920058798C /* Pod */ = { 149 | isa = PBXGroup; 150 | children = ( 151 | 1450496C1AB7858300D9745F /* AppDelegate */, 152 | 146D72A31AB782920058798C /* Images.xcassets */, 153 | 146D72961AB782920058798C /* Supporting Files */, 154 | ); 155 | path = Pod; 156 | sourceTree = ""; 157 | }; 158 | 146D72961AB782920058798C /* Supporting Files */ = { 159 | isa = PBXGroup; 160 | children = ( 161 | 146D72971AB782920058798C /* Info.plist */, 162 | 146D72981AB782920058798C /* main.m */, 163 | ); 164 | name = "Supporting Files"; 165 | sourceTree = ""; 166 | }; 167 | 146D72AF1AB782920058798C /* Tests */ = { 168 | isa = PBXGroup; 169 | children = ( 170 | 146D72B21AB782920058798C /* Tests.m */, 171 | 146D72B01AB782920058798C /* Supporting Files */, 172 | ); 173 | path = Tests; 174 | sourceTree = ""; 175 | }; 176 | 146D72B01AB782920058798C /* Supporting Files */ = { 177 | isa = PBXGroup; 178 | children = ( 179 | 146D72B11AB782920058798C /* Info.plist */, 180 | ); 181 | name = "Supporting Files"; 182 | sourceTree = ""; 183 | }; 184 | 9A3F16FC1AF228E100229577 /* Assets */ = { 185 | isa = PBXGroup; 186 | children = ( 187 | 9A3F16FD1AF228E100229577 /* card_shadow.png */, 188 | 9A3F16FE1AF228E100229577 /* card_shine.png */, 189 | ); 190 | name = Assets; 191 | path = ../Assets; 192 | sourceTree = ""; 193 | }; 194 | D0AEA67F93773348378C9B9A /* Frameworks */ = { 195 | isa = PBXGroup; 196 | children = ( 197 | 47DD127F087D98A0D25096AC /* libPods.a */, 198 | ); 199 | name = Frameworks; 200 | sourceTree = ""; 201 | }; 202 | /* End PBXGroup section */ 203 | 204 | /* Begin PBXNativeTarget section */ 205 | 146D72921AB782920058798C /* Pod */ = { 206 | isa = PBXNativeTarget; 207 | buildConfigurationList = 146D72B61AB782920058798C /* Build configuration list for PBXNativeTarget "Pod" */; 208 | buildPhases = ( 209 | 33649E002E09CC5DFA0A0AE2 /* Check Pods Manifest.lock */, 210 | 146D728F1AB782920058798C /* Sources */, 211 | 146D72901AB782920058798C /* Frameworks */, 212 | 146D72911AB782920058798C /* Resources */, 213 | AEDB4783CEEF85CD65F47588 /* Copy Pods Resources */, 214 | ); 215 | buildRules = ( 216 | ); 217 | dependencies = ( 218 | ); 219 | name = Pod; 220 | productName = Pod; 221 | productReference = 146D72931AB782920058798C /* Pod.app */; 222 | productType = "com.apple.product-type.application"; 223 | }; 224 | 146D72AB1AB782920058798C /* Tests */ = { 225 | isa = PBXNativeTarget; 226 | buildConfigurationList = 146D72B91AB782920058798C /* Build configuration list for PBXNativeTarget "Tests" */; 227 | buildPhases = ( 228 | 2D5FB1878122BCFCD0B17BEA /* Check Pods Manifest.lock */, 229 | 146D72A81AB782920058798C /* Sources */, 230 | 146D72A91AB782920058798C /* Frameworks */, 231 | 146D72AA1AB782920058798C /* Resources */, 232 | 29D847D1243EB149F1B7213D /* Copy Pods Resources */, 233 | ); 234 | buildRules = ( 235 | ); 236 | dependencies = ( 237 | 146D72AE1AB782920058798C /* PBXTargetDependency */, 238 | ); 239 | name = Tests; 240 | productName = PodTests; 241 | productReference = 146D72AC1AB782920058798C /* Tests.xctest */; 242 | productType = "com.apple.product-type.bundle.unit-test"; 243 | }; 244 | /* End PBXNativeTarget section */ 245 | 246 | /* Begin PBXProject section */ 247 | 146D728B1AB782920058798C /* Project object */ = { 248 | isa = PBXProject; 249 | attributes = { 250 | LastUpgradeCheck = 0620; 251 | ORGANIZATIONNAME = Example; 252 | TargetAttributes = { 253 | 146D72921AB782920058798C = { 254 | CreatedOnToolsVersion = 6.2; 255 | }; 256 | 146D72AB1AB782920058798C = { 257 | CreatedOnToolsVersion = 6.2; 258 | }; 259 | }; 260 | }; 261 | buildConfigurationList = 146D728E1AB782920058798C /* Build configuration list for PBXProject "Pod" */; 262 | compatibilityVersion = "Xcode 3.2"; 263 | developmentRegion = English; 264 | hasScannedForEncodings = 0; 265 | knownRegions = ( 266 | en, 267 | Base, 268 | ); 269 | mainGroup = 146D728A1AB782920058798C; 270 | productRefGroup = 146D72941AB782920058798C /* Products */; 271 | projectDirPath = ""; 272 | projectRoot = ""; 273 | targets = ( 274 | 146D72921AB782920058798C /* Pod */, 275 | 146D72AB1AB782920058798C /* Tests */, 276 | ); 277 | }; 278 | /* End PBXProject section */ 279 | 280 | /* Begin PBXResourcesBuildPhase section */ 281 | 146D72911AB782920058798C /* Resources */ = { 282 | isa = PBXResourcesBuildPhase; 283 | buildActionMask = 2147483647; 284 | files = ( 285 | 9A3F17001AF228E100229577 /* card_shine.png in Resources */, 286 | 146D72A41AB782920058798C /* Images.xcassets in Resources */, 287 | 9A3F16FF1AF228E100229577 /* card_shadow.png in Resources */, 288 | ); 289 | runOnlyForDeploymentPostprocessing = 0; 290 | }; 291 | 146D72AA1AB782920058798C /* Resources */ = { 292 | isa = PBXResourcesBuildPhase; 293 | buildActionMask = 2147483647; 294 | files = ( 295 | ); 296 | runOnlyForDeploymentPostprocessing = 0; 297 | }; 298 | /* End PBXResourcesBuildPhase section */ 299 | 300 | /* Begin PBXShellScriptBuildPhase section */ 301 | 29D847D1243EB149F1B7213D /* Copy Pods Resources */ = { 302 | isa = PBXShellScriptBuildPhase; 303 | buildActionMask = 2147483647; 304 | files = ( 305 | ); 306 | inputPaths = ( 307 | ); 308 | name = "Copy Pods Resources"; 309 | outputPaths = ( 310 | ); 311 | runOnlyForDeploymentPostprocessing = 0; 312 | shellPath = /bin/sh; 313 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods/Pods-resources.sh\"\n"; 314 | showEnvVarsInLog = 0; 315 | }; 316 | 2D5FB1878122BCFCD0B17BEA /* Check Pods Manifest.lock */ = { 317 | isa = PBXShellScriptBuildPhase; 318 | buildActionMask = 2147483647; 319 | files = ( 320 | ); 321 | inputPaths = ( 322 | ); 323 | name = "Check Pods Manifest.lock"; 324 | outputPaths = ( 325 | ); 326 | runOnlyForDeploymentPostprocessing = 0; 327 | shellPath = /bin/sh; 328 | shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; 329 | showEnvVarsInLog = 0; 330 | }; 331 | 33649E002E09CC5DFA0A0AE2 /* Check Pods Manifest.lock */ = { 332 | isa = PBXShellScriptBuildPhase; 333 | buildActionMask = 2147483647; 334 | files = ( 335 | ); 336 | inputPaths = ( 337 | ); 338 | name = "Check Pods Manifest.lock"; 339 | outputPaths = ( 340 | ); 341 | runOnlyForDeploymentPostprocessing = 0; 342 | shellPath = /bin/sh; 343 | shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; 344 | showEnvVarsInLog = 0; 345 | }; 346 | AEDB4783CEEF85CD65F47588 /* Copy Pods Resources */ = { 347 | isa = PBXShellScriptBuildPhase; 348 | buildActionMask = 2147483647; 349 | files = ( 350 | ); 351 | inputPaths = ( 352 | ); 353 | name = "Copy Pods Resources"; 354 | outputPaths = ( 355 | ); 356 | runOnlyForDeploymentPostprocessing = 0; 357 | shellPath = /bin/sh; 358 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods/Pods-resources.sh\"\n"; 359 | showEnvVarsInLog = 0; 360 | }; 361 | /* End PBXShellScriptBuildPhase section */ 362 | 363 | /* Begin PBXSourcesBuildPhase section */ 364 | 146D728F1AB782920058798C /* Sources */ = { 365 | isa = PBXSourcesBuildPhase; 366 | buildActionMask = 2147483647; 367 | files = ( 368 | 147872E71AE542020091AE1D /* CardView.m in Sources */, 369 | 9AC5E5401ADC009300FE1E0D /* CardStackController.m in Sources */, 370 | 146D72991AB782920058798C /* main.m in Sources */, 371 | 1450496F1AB7858300D9745F /* AppDelegate.m in Sources */, 372 | 9A3E19BA1B0CDDDB00A0FAF3 /* ResizeableViewController.m in Sources */, 373 | ); 374 | runOnlyForDeploymentPostprocessing = 0; 375 | }; 376 | 146D72A81AB782920058798C /* Sources */ = { 377 | isa = PBXSourcesBuildPhase; 378 | buildActionMask = 2147483647; 379 | files = ( 380 | 9A3286291AF0C9C700149E6C /* CardView.m in Sources */, 381 | 9A32862A1AF0C9CC00149E6C /* CardStackController.m in Sources */, 382 | 146D72B31AB782920058798C /* Tests.m in Sources */, 383 | ); 384 | runOnlyForDeploymentPostprocessing = 0; 385 | }; 386 | /* End PBXSourcesBuildPhase section */ 387 | 388 | /* Begin PBXTargetDependency section */ 389 | 146D72AE1AB782920058798C /* PBXTargetDependency */ = { 390 | isa = PBXTargetDependency; 391 | target = 146D72921AB782920058798C /* Pod */; 392 | targetProxy = 146D72AD1AB782920058798C /* PBXContainerItemProxy */; 393 | }; 394 | /* End PBXTargetDependency section */ 395 | 396 | /* Begin XCBuildConfiguration section */ 397 | 146D72B41AB782920058798C /* Debug */ = { 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_MODULES = YES; 404 | CLANG_ENABLE_OBJC_ARC = YES; 405 | CLANG_WARN_BOOL_CONVERSION = YES; 406 | CLANG_WARN_CONSTANT_CONVERSION = YES; 407 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 408 | CLANG_WARN_EMPTY_BODY = YES; 409 | CLANG_WARN_ENUM_CONVERSION = YES; 410 | CLANG_WARN_INT_CONVERSION = YES; 411 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 412 | CLANG_WARN_UNREACHABLE_CODE = YES; 413 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 414 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 415 | COPY_PHASE_STRIP = NO; 416 | ENABLE_STRICT_OBJC_MSGSEND = YES; 417 | GCC_C_LANGUAGE_STANDARD = gnu99; 418 | GCC_DYNAMIC_NO_PIC = NO; 419 | GCC_GENERATE_TEST_COVERAGE_FILES = YES; 420 | GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = YES; 421 | GCC_OPTIMIZATION_LEVEL = 0; 422 | GCC_PREPROCESSOR_DEFINITIONS = ( 423 | "DEBUG=1", 424 | "$(inherited)", 425 | ); 426 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 427 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 428 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 429 | GCC_WARN_UNDECLARED_SELECTOR = YES; 430 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 431 | GCC_WARN_UNUSED_FUNCTION = YES; 432 | GCC_WARN_UNUSED_VARIABLE = YES; 433 | IPHONEOS_DEPLOYMENT_TARGET = 8.2; 434 | MTL_ENABLE_DEBUG_INFO = YES; 435 | ONLY_ACTIVE_ARCH = YES; 436 | SDKROOT = iphoneos; 437 | }; 438 | name = Debug; 439 | }; 440 | 146D72B51AB782920058798C /* Release */ = { 441 | isa = XCBuildConfiguration; 442 | buildSettings = { 443 | ALWAYS_SEARCH_USER_PATHS = NO; 444 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 445 | CLANG_CXX_LIBRARY = "libc++"; 446 | CLANG_ENABLE_MODULES = YES; 447 | CLANG_ENABLE_OBJC_ARC = YES; 448 | CLANG_WARN_BOOL_CONVERSION = YES; 449 | CLANG_WARN_CONSTANT_CONVERSION = YES; 450 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 451 | CLANG_WARN_EMPTY_BODY = YES; 452 | CLANG_WARN_ENUM_CONVERSION = YES; 453 | CLANG_WARN_INT_CONVERSION = YES; 454 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 455 | CLANG_WARN_UNREACHABLE_CODE = YES; 456 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 457 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 458 | COPY_PHASE_STRIP = NO; 459 | ENABLE_NS_ASSERTIONS = NO; 460 | ENABLE_STRICT_OBJC_MSGSEND = YES; 461 | GCC_C_LANGUAGE_STANDARD = gnu99; 462 | GCC_GENERATE_TEST_COVERAGE_FILES = YES; 463 | GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = YES; 464 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 465 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 466 | GCC_WARN_UNDECLARED_SELECTOR = YES; 467 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 468 | GCC_WARN_UNUSED_FUNCTION = YES; 469 | GCC_WARN_UNUSED_VARIABLE = YES; 470 | IPHONEOS_DEPLOYMENT_TARGET = 8.2; 471 | MTL_ENABLE_DEBUG_INFO = NO; 472 | SDKROOT = iphoneos; 473 | VALIDATE_PRODUCT = YES; 474 | }; 475 | name = Release; 476 | }; 477 | 146D72B71AB782920058798C /* Debug */ = { 478 | isa = XCBuildConfiguration; 479 | baseConfigurationReference = 31CB67FA653DFCAE806222A3 /* Pods.debug.xcconfig */; 480 | buildSettings = { 481 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 482 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; 483 | INFOPLIST_FILE = Pod/Info.plist; 484 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 485 | PRODUCT_NAME = "$(TARGET_NAME)"; 486 | WARNING_CFLAGS = ( 487 | "-Weverything", 488 | "-Wno-objc-missing-property-synthesis", 489 | ); 490 | }; 491 | name = Debug; 492 | }; 493 | 146D72B81AB782920058798C /* Release */ = { 494 | isa = XCBuildConfiguration; 495 | baseConfigurationReference = 14946611115390DFF8D31297 /* Pods.release.xcconfig */; 496 | buildSettings = { 497 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 498 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; 499 | INFOPLIST_FILE = Pod/Info.plist; 500 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 501 | PRODUCT_NAME = "$(TARGET_NAME)"; 502 | WARNING_CFLAGS = ( 503 | "-Weverything", 504 | "-Wno-objc-missing-property-synthesis", 505 | ); 506 | }; 507 | name = Release; 508 | }; 509 | 146D72BA1AB782920058798C /* Debug */ = { 510 | isa = XCBuildConfiguration; 511 | baseConfigurationReference = 31CB67FA653DFCAE806222A3 /* Pods.debug.xcconfig */; 512 | buildSettings = { 513 | FRAMEWORK_SEARCH_PATHS = ( 514 | "$(SDKROOT)/Developer/Library/Frameworks", 515 | "$(inherited)", 516 | ); 517 | GCC_PREPROCESSOR_DEFINITIONS = ( 518 | "DEBUG=1", 519 | "$(inherited)", 520 | ); 521 | HEADER_SEARCH_PATHS = ( 522 | "$(inherited)", 523 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 524 | "\"${PODS_ROOT}/Headers/Public\"", 525 | "\"${PODS_ROOT}/Headers/Public/Masonry\"", 526 | "\"${PODS_ROOT}/Headers/Public/pop\"", 527 | ); 528 | INFOPLIST_FILE = Tests/Info.plist; 529 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 530 | PRODUCT_NAME = "$(TARGET_NAME)"; 531 | }; 532 | name = Debug; 533 | }; 534 | 146D72BB1AB782920058798C /* Release */ = { 535 | isa = XCBuildConfiguration; 536 | baseConfigurationReference = 14946611115390DFF8D31297 /* Pods.release.xcconfig */; 537 | buildSettings = { 538 | FRAMEWORK_SEARCH_PATHS = ( 539 | "$(SDKROOT)/Developer/Library/Frameworks", 540 | "$(inherited)", 541 | ); 542 | HEADER_SEARCH_PATHS = ( 543 | "$(inherited)", 544 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 545 | "\"${PODS_ROOT}/Headers/Public\"", 546 | "\"${PODS_ROOT}/Headers/Public/Masonry\"", 547 | "\"${PODS_ROOT}/Headers/Public/pop\"", 548 | ); 549 | INFOPLIST_FILE = Tests/Info.plist; 550 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 551 | PRODUCT_NAME = "$(TARGET_NAME)"; 552 | }; 553 | name = Release; 554 | }; 555 | /* End XCBuildConfiguration section */ 556 | 557 | /* Begin XCConfigurationList section */ 558 | 146D728E1AB782920058798C /* Build configuration list for PBXProject "Pod" */ = { 559 | isa = XCConfigurationList; 560 | buildConfigurations = ( 561 | 146D72B41AB782920058798C /* Debug */, 562 | 146D72B51AB782920058798C /* Release */, 563 | ); 564 | defaultConfigurationIsVisible = 0; 565 | defaultConfigurationName = Release; 566 | }; 567 | 146D72B61AB782920058798C /* Build configuration list for PBXNativeTarget "Pod" */ = { 568 | isa = XCConfigurationList; 569 | buildConfigurations = ( 570 | 146D72B71AB782920058798C /* Debug */, 571 | 146D72B81AB782920058798C /* Release */, 572 | ); 573 | defaultConfigurationIsVisible = 0; 574 | defaultConfigurationName = Release; 575 | }; 576 | 146D72B91AB782920058798C /* Build configuration list for PBXNativeTarget "Tests" */ = { 577 | isa = XCConfigurationList; 578 | buildConfigurations = ( 579 | 146D72BA1AB782920058798C /* Debug */, 580 | 146D72BB1AB782920058798C /* Release */, 581 | ); 582 | defaultConfigurationIsVisible = 0; 583 | defaultConfigurationName = Release; 584 | }; 585 | /* End XCConfigurationList section */ 586 | }; 587 | rootObject = 146D728B1AB782920058798C /* Project object */; 588 | } 589 | -------------------------------------------------------------------------------- /Pod/Pod.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Pod/Pod.xcodeproj/xcshareddata/xcschemes/Pod.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 65 | 66 | 75 | 77 | 83 | 84 | 85 | 86 | 87 | 88 | 94 | 96 | 102 | 103 | 104 | 105 | 107 | 108 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /Pod/Pod.xcodeproj/xcshareddata/xcschemes/Tests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 61 | 62 | 68 | 69 | 70 | 71 | 72 | 73 | 79 | 80 | 86 | 87 | 88 | 89 | 91 | 92 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /Pod/Pod.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Pod/Pod/AppDelegate/AppDelegate.h: -------------------------------------------------------------------------------- 1 | @import UIKit; 2 | 3 | @interface AppDelegate : UIResponder 4 | 5 | @property (strong, nonatomic) UIWindow *window; 6 | 7 | @end 8 | 9 | -------------------------------------------------------------------------------- /Pod/Pod/AppDelegate/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | #import "CardStackController.h" 3 | #import "Masonry.h" 4 | #import "ResizeableViewController.h" 5 | 6 | static const NSUInteger ExampleNumberOfInitialCards = 4; 7 | static const CGFloat ExampleButtonWidth = 320.0f; 8 | static const CGFloat ExampleButtonHeight = 44.0f; 9 | static const CGFloat ExampleTopMargin = 64.0f; 10 | static const CGFloat ExampleMargin = 10.0f; 11 | static const CGFloat ExampleSearchViewControllerHeight = 100.0f; 12 | 13 | @interface AppDelegate () 14 | 15 | @property (nonatomic) CardStackController *cardStackController; 16 | @property (nonatomic) NSMutableArray *viewControllers; 17 | @property (nonatomic) NSUInteger numberOfCardsCreated; 18 | 19 | @property (nonatomic) UIViewController *searchViewController; 20 | 21 | @end 22 | 23 | @implementation AppDelegate 24 | 25 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 26 | self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; 27 | 28 | [[UIApplication sharedApplication] setStatusBarHidden:YES]; 29 | 30 | self.viewControllers = [NSMutableArray new]; 31 | for (NSUInteger i = 0; i < ExampleNumberOfInitialCards; i++) { 32 | [self.viewControllers addObject:[self createNewTestViewControllerWithTag:i]]; 33 | } 34 | 35 | self.cardStackController = [CardStackController new]; 36 | self.cardStackController.delegate = self; 37 | self.cardStackController.dataSource = self; 38 | self.cardStackController.titleBarBackgroundColor = [UIColor orangeColor]; 39 | self.cardStackController.viewControllers = self.viewControllers; 40 | for (NSUInteger i = 0; i < self.cardStackController.cards.count; i++) { 41 | CardView *card = [self.cardStackController.cards objectAtIndex:i]; 42 | card.title = [NSString stringWithFormat:@"#%ld", (long)i + 1]; 43 | } 44 | 45 | self.searchViewController = [[UIViewController alloc] init]; 46 | self.searchViewController.view.frame = CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, ExampleSearchViewControllerHeight); 47 | self.searchViewController.view.backgroundColor = [UIColor redColor]; 48 | 49 | UILabel *searchLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, ExampleButtonHeight)]; 50 | searchLabel.text = @"Search view controller here"; 51 | searchLabel.textColor = [UIColor whiteColor]; 52 | searchLabel.font = [UIFont systemFontOfSize:16.0f]; 53 | searchLabel.textAlignment = NSTextAlignmentCenter; 54 | [self.searchViewController.view addSubview:searchLabel]; 55 | [searchLabel mas_makeConstraints:^(MASConstraintMaker *make) { 56 | make.center.equalTo(self.searchViewController.view); 57 | }]; 58 | 59 | self.cardStackController.searchViewController = self.searchViewController; 60 | 61 | self.window.rootViewController = self.cardStackController; 62 | self.window.backgroundColor = [UIColor clearColor]; 63 | [self.window makeKeyAndVisible]; 64 | 65 | return YES; 66 | } 67 | 68 | #pragma mark - CardStackControllerDataSource 69 | 70 | - (UIColor *)cardStackController:(CardStackController *)cardStackController titleBarDecorationColorForCardAtIndex:(NSUInteger)index { 71 | return [UIColor colorWithRed:1 green:0 blue:0 alpha:(CGFloat)index / (CGFloat)cardStackController.cards.count]; 72 | } 73 | 74 | - (UIImage *)cardStackController:(CardStackController *)cardStackController titleBarShadowImageForCardAtIndex:(NSUInteger)index { 75 | return [UIImage imageNamed:@"card_shadow.png"]; 76 | } 77 | 78 | - (UIImage *)cardStackController:(CardStackController *)cardStackController titleBarShineImageForCardAtIndex:(NSUInteger)index { 79 | return [UIImage imageNamed:@"card_shine.png"]; 80 | } 81 | 82 | #pragma mark - Other methods 83 | 84 | - (UIViewController *)createNewTestViewControllerWithTag:(NSInteger)tag { 85 | UIViewController *viewController = [ResizeableViewController new]; 86 | 87 | viewController.view.backgroundColor = [UIColor whiteColor]; 88 | viewController.view.tag = tag; 89 | 90 | UIButton *insertAboveButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, ExampleButtonWidth, ExampleButtonHeight)]; 91 | [insertAboveButton setTitleColor:[UIColor orangeColor] forState:UIControlStateNormal]; 92 | [insertAboveButton setTitle:@"Insert card above" forState:UIControlStateNormal]; 93 | [insertAboveButton addTarget:self action:@selector(insertAboveAction:) forControlEvents:UIControlEventTouchUpInside]; 94 | [viewController.view addSubview:insertAboveButton]; 95 | [insertAboveButton mas_makeConstraints:^(MASConstraintMaker *make) { 96 | make.top.equalTo(viewController.view.mas_top).with.offset(ExampleTopMargin); 97 | make.centerX.equalTo(viewController.view.mas_centerX); 98 | }]; 99 | 100 | UIButton *insertBelowButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, ExampleButtonWidth, ExampleButtonHeight)]; 101 | [insertBelowButton setTitleColor:[UIColor orangeColor] forState:UIControlStateNormal]; 102 | [insertBelowButton setTitle:@"Insert card below" forState:UIControlStateNormal]; 103 | [insertBelowButton addTarget:self action:@selector(insertBelowAction:) forControlEvents:UIControlEventTouchUpInside]; 104 | [viewController.view addSubview:insertBelowButton]; 105 | [insertBelowButton mas_makeConstraints:^(MASConstraintMaker *make) { 106 | make.top.equalTo(insertAboveButton.mas_bottom).with.offset(ExampleMargin); 107 | make.centerX.equalTo(viewController.view.mas_centerX); 108 | }]; 109 | 110 | UIButton *removeButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, ExampleButtonWidth, ExampleButtonHeight)]; 111 | [removeButton setTitle:@"Remove card" forState:UIControlStateNormal]; 112 | [removeButton setTitleColor:[UIColor orangeColor] forState:UIControlStateNormal]; 113 | [removeButton addTarget:self action:@selector(removeAction:) forControlEvents:UIControlEventTouchUpInside]; 114 | [viewController.view addSubview:removeButton]; 115 | [removeButton mas_makeConstraints:^(MASConstraintMaker *make) { 116 | make.top.equalTo(insertBelowButton.mas_bottom).with.offset(ExampleMargin); 117 | make.centerX.equalTo(viewController.view.mas_centerX); 118 | }]; 119 | 120 | UILabel *openLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, ExampleButtonWidth, ExampleButtonHeight)]; 121 | openLabel.text = @"Pull down on title bar to open stack"; 122 | openLabel.textAlignment = NSTextAlignmentCenter; 123 | openLabel.font = [UIFont systemFontOfSize:13.0f]; 124 | openLabel.textColor = [UIColor darkGrayColor]; 125 | [viewController.view addSubview:openLabel]; 126 | [openLabel mas_makeConstraints:^(MASConstraintMaker *make) { 127 | make.top.equalTo(removeButton.mas_bottom).with.offset(3 * ExampleMargin); 128 | make.centerX.equalTo(viewController.view.mas_centerX); 129 | }]; 130 | 131 | UILabel *tapLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, ExampleButtonWidth, ExampleButtonHeight)]; 132 | tapLabel.text = @"Tap on title bar to select"; 133 | tapLabel.textAlignment = NSTextAlignmentCenter; 134 | tapLabel.font = [UIFont systemFontOfSize:13.0f]; 135 | tapLabel.textColor = [UIColor darkGrayColor]; 136 | [viewController.view addSubview:tapLabel]; 137 | [tapLabel mas_makeConstraints:^(MASConstraintMaker *make) { 138 | make.top.equalTo(openLabel.mas_bottom).with.offset(2 * ExampleMargin); 139 | make.centerX.equalTo(viewController.view.mas_centerX); 140 | }]; 141 | 142 | self.numberOfCardsCreated = self.numberOfCardsCreated + 1; 143 | 144 | return viewController; 145 | } 146 | 147 | - (void)insertAboveAction:(UIButton *)button { 148 | NSInteger index = button.superview.tag; 149 | UIViewController *aboveViewController = [self.viewControllers objectAtIndex:index]; 150 | UIViewController *viewController = [self createNewTestViewControllerWithTag:self.viewControllers.count]; 151 | [self.viewControllers insertObject:viewController atIndex:index]; 152 | NSString *title = [NSString stringWithFormat:@"#%ld", (long)self.numberOfCardsCreated]; 153 | [self.cardStackController insertCardWithViewController:viewController 154 | withTitle:title 155 | aboveViewController:aboveViewController 156 | makeCurrent:NO 157 | animated:YES 158 | withCompletion:^{ 159 | for (NSUInteger i = 0; i < self.viewControllers.count; i++) { 160 | UIViewController *viewController = [self.viewControllers objectAtIndex:i]; 161 | viewController.view.tag = i; 162 | } 163 | }]; 164 | } 165 | 166 | - (void)insertBelowAction:(UIButton *)button { 167 | NSInteger index = button.superview.tag; 168 | UIViewController *belowViewController = [self.viewControllers objectAtIndex:index]; 169 | UIViewController *viewController = [self createNewTestViewControllerWithTag:self.viewControllers.count]; 170 | [self.viewControllers insertObject:viewController atIndex:index + 1]; 171 | NSString *title = [NSString stringWithFormat:@"#%ld", (long)self.numberOfCardsCreated]; 172 | [self.cardStackController insertCardWithViewController:viewController 173 | withTitle:title 174 | belowViewController:belowViewController 175 | makeCurrent:YES 176 | animated:YES 177 | withCompletion:^{ 178 | for (NSUInteger i = 0; i < self.viewControllers.count; i++) { 179 | UIViewController *viewController = [self.viewControllers objectAtIndex:i]; 180 | viewController.view.tag = i; 181 | } 182 | }]; 183 | } 184 | 185 | - (void)removeAction:(UIButton *)button { 186 | NSInteger index = button.superview.tag; 187 | [self.cardStackController removeCardAtIndex:index 188 | animated:YES 189 | withCompletion:^{ 190 | for (NSUInteger i = 0; i < self.viewControllers.count; i++) { 191 | UIViewController *viewController = [self.viewControllers objectAtIndex:i]; 192 | viewController.view.tag = i; 193 | } 194 | }]; 195 | } 196 | 197 | @end 198 | -------------------------------------------------------------------------------- /Pod/Pod/AppDelegate/ResizeableViewController.h: -------------------------------------------------------------------------------- 1 | @import UIKit; 2 | #import "CardStackController.h" 3 | 4 | @interface ResizeableViewController : UIViewController 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /Pod/Pod/AppDelegate/ResizeableViewController.m: -------------------------------------------------------------------------------- 1 | #import "ResizeableViewController.h" 2 | 3 | @implementation ResizeableViewController 4 | 5 | - (void)setContentHeight:(CGFloat)contentHeight { 6 | CGRect frame = self.view.frame; 7 | frame.size.height = contentHeight; 8 | self.view.frame = frame; 9 | } 10 | 11 | @end 12 | -------------------------------------------------------------------------------- /Pod/Pod/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Pod/Pod/Images.xcassets/LaunchImage.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "orientation" : "portrait", 5 | "idiom" : "iphone", 6 | "minimum-system-version" : "7.0", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "orientation" : "portrait", 11 | "idiom" : "iphone", 12 | "minimum-system-version" : "7.0", 13 | "subtype" : "retina4", 14 | "scale" : "2x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Pod/Pod/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | com.example.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | 33 | UIViewControllerBasedStatusBarAppearance 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /Pod/Pod/main.m: -------------------------------------------------------------------------------- 1 | @import UIKit; 2 | #import "AppDelegate.h" 3 | 4 | int main(int argc, char * argv[]) { 5 | @autoreleasepool { 6 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Pod/Podfile: -------------------------------------------------------------------------------- 1 | source 'https://github.com/CocoaPods/Specs.git' 2 | 3 | platform :ios, '7.0' 4 | 5 | link_with 'Pod', 'Tests' 6 | 7 | pod 'Masonry', '~> 0.6' 8 | pod 'pop' 9 | -------------------------------------------------------------------------------- /Pod/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Masonry (0.6.1) 3 | - pop (1.0.7) 4 | 5 | DEPENDENCIES: 6 | - Masonry (~> 0.6) 7 | - pop 8 | 9 | SPEC CHECKSUMS: 10 | Masonry: 4972309f2f134de9dd312f4dc4a21359b50e6caa 11 | pop: 628ffc631644601567ee8bfaaaea493ebd7d0923 12 | 13 | COCOAPODS: 0.37.0 14 | -------------------------------------------------------------------------------- /Pod/Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | com.example.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Pod/Tests/Tests.m: -------------------------------------------------------------------------------- 1 | @import UIKit; 2 | @import XCTest; 3 | #import "CardStackController.h" 4 | 5 | @interface PodTests : XCTestCase 6 | 7 | @end 8 | 9 | @implementation PodTests 10 | 11 | - (void)testIndexForViewController { 12 | CardStackController *cardStackController = [CardStackController new]; 13 | 14 | UIViewController *viewController1 = [UIViewController new]; 15 | UIViewController *viewController2 = [UIViewController new]; 16 | UIViewController *viewController3 = [UIViewController new]; 17 | cardStackController.viewControllers = @[viewController1, viewController2]; 18 | 19 | XCTAssertEqual([cardStackController indexForViewController:viewController1], 0); 20 | XCTAssertEqual([cardStackController indexForViewController:viewController2], 1); 21 | XCTAssertEqual([cardStackController indexForViewController:viewController3], NSNotFound); 22 | } 23 | 24 | @end 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CardStack 2 | 3 | [![CI Status](http://img.shields.io/travis/hyperoslo/CardStack.svg?style=flat)](https://travis-ci.org/hyperoslo/CardStack) 4 | [![Version](https://img.shields.io/cocoapods/v/CardStack.svg?style=flat)](http://cocoadocs.org/docsets/CardStack) 5 | [![License](https://img.shields.io/cocoapods/l/CardStack.svg?style=flat)](http://cocoadocs.org/docsets/CardStack) 6 | [![Platform](https://img.shields.io/cocoapods/p/CardStack.svg?style=flat)](http://cocoadocs.org/docsets/CardStack) 7 | 8 | ## Screenshot 9 | 10 | ![](Images/screenshot.png?raw=true) 11 | 12 | ## Usage 13 | 14 | ```objc 15 | 16 | ``` 17 | 18 | ## Installation 19 | 20 | **CardStack** is available through [CocoaPods](http://cocoapods.org). To install 21 | it, simply add the following line to your Podfile: 22 | 23 | ```ruby 24 | pod 'CardStack' 25 | ``` 26 | 27 | ## Author 28 | 29 | Hyper Interaktiv AS, ios@hyper.no 30 | 31 | ## License 32 | 33 | **CardStack** is available under the MIT license. See the LICENSE file for more info. 34 | -------------------------------------------------------------------------------- /Source/CardStackController.h: -------------------------------------------------------------------------------- 1 | @import UIKit; 2 | #import "CardView.h" 3 | 4 | @protocol CardStackControllerDelegate; 5 | @protocol CardStackControllerDataSource; 6 | 7 | @interface CardStackController : UIViewController 8 | 9 | @property (weak, nonatomic) id delegate; 10 | @property (weak, nonatomic) id dataSource; 11 | 12 | @property (nonatomic) NSArray *cards; 13 | @property (nonatomic) NSArray *viewControllers; 14 | @property (nonatomic) NSUInteger currentCardIndex; 15 | @property (nonatomic) UIColor *titleBarBackgroundColor; 16 | @property (nonatomic) UIColor *titleColor; 17 | @property (nonatomic) UIFont *titleFont; 18 | @property (nonatomic) CGFloat titleBarHeight; 19 | @property (nonatomic) CGFloat titleLabelVerticalOffset; 20 | 21 | @property (nonatomic) UIViewController *searchViewController; 22 | @property (nonatomic) BOOL isSeachViewControllerHidden; 23 | 24 | - (void)insertCardWithViewController:(UIViewController *)viewController 25 | withTitle:(NSString *)title 26 | aboveViewController:(UIViewController *)aboveViewController 27 | makeCurrent:(BOOL)makeCurrent 28 | animated:(BOOL)animated 29 | withCompletion:(void(^)())completion; 30 | 31 | - (void)insertCardWithViewController:(UIViewController *)viewController 32 | withTitle:(NSString *)title 33 | belowViewController:(UIViewController *)belowViewController 34 | makeCurrent:(BOOL)makeCurrent 35 | animated:(BOOL)animated 36 | withCompletion:(void(^)())completion; 37 | 38 | - (void)removeCardAtIndex:(NSUInteger)index 39 | animated:(BOOL)animated 40 | withCompletion:(void(^)())completion; 41 | 42 | - (void)setIsSeachViewControllerHidden:(BOOL)isSeachViewControllerHidden 43 | animated:(BOOL)animated 44 | withCompletion:(void(^)())completion; 45 | 46 | - (void)setCurrentCardIndex:(NSUInteger)currentCardIndex animated:(BOOL)animated; 47 | 48 | - (CardView *)cardViewForViewController:(UIViewController *)viewController; 49 | - (NSUInteger)indexForViewController:(UIViewController *)viewController; 50 | 51 | @end 52 | 53 | @protocol CardStackControllerDelegate 54 | 55 | @optional 56 | - (void)cardStackControllerWillOpen:(CardStackController *)cardStackController; 57 | - (void)cardStackControllerWillClose:(CardStackController *)cardStackController; 58 | - (void)cardStackControllerDidOpen:(CardStackController *)cardStackController; 59 | - (void)cardStackControllerDidClose:(CardStackController *)cardStackController; 60 | - (void)cardStackControllerDidUndockCards:(CardStackController *)cardStackController; 61 | - (void)cardStackControllerDidCloseSearch:(CardStackController *)cardStackController; 62 | 63 | @end 64 | 65 | @protocol CardStackControllerDataSource 66 | 67 | @optional 68 | - (UIColor *)cardStackController:(CardStackController *)cardStackController titleBarDecorationColorForCardAtIndex:(NSUInteger)index; 69 | - (UIImage *)cardStackController:(CardStackController *)cardStackController titleBarShadowImageForCardAtIndex:(NSUInteger)index; 70 | - (UIImage *)cardStackController:(CardStackController *)cardStackController titleBarShineImageForCardAtIndex:(NSUInteger)index; 71 | 72 | @end 73 | 74 | @protocol CardStackResizeableViewController 75 | 76 | - (void)setContentHeight:(CGFloat)contentHeight; 77 | 78 | @end 79 | -------------------------------------------------------------------------------- /Source/CardStackController.m: -------------------------------------------------------------------------------- 1 | #import "CardStackController.h" 2 | #import "CardView.h" 3 | #import "POP.h" 4 | 5 | static const CGFloat CardStackTopMargin = 10.0f; 6 | static const CGFloat CardStackDepthOffset = 0.04f; 7 | static const CGFloat CardStackOpenIfLargeThanPercent = 0.8f; 8 | static const CGFloat CardStackVerticalVelocityLimitWhenMakingCardCurrent = 100.0f; 9 | static const CGFloat CardStackOffsetToAvoidAreaBelowTheTitleToBecomeVisible = 1.0f; 10 | static const CGFloat CardStackMaximumVisibleTitleBarProportion = 1.3f; 11 | static const CGFloat CardStackMinimumSearchViewControllerHeightPropotion = 0.5f; 12 | static const CGFloat CardStackTitleBarHeightProportionWhenSearchViewControllerIsVisible = 0.2f; 13 | static const CGFloat CardStackTitleBarHeight = 44.0f; 14 | static const CGFloat CardStackDefaultSpringBounciness = 8.0f; 15 | 16 | @interface CardStackController () 17 | 18 | @property (nonatomic) BOOL isAnimating; 19 | @property (nonatomic) BOOL isOpen; 20 | @property (nonatomic) CGRect originalCardFrame; 21 | @property (nonatomic) CGFloat maximumTotalTitleBarHeightBeforeSearchAppears; 22 | @property (nonatomic) POPSpringAnimation *searchControllerOpeningAnimation; 23 | 24 | @end 25 | 26 | @implementation CardStackController 27 | 28 | @synthesize titleBarBackgroundColor = _titleBarBackgroundColor; 29 | @synthesize titleFont = _titleFont; 30 | 31 | - (instancetype)init { 32 | self = [super init]; 33 | if (!self) return nil; 34 | 35 | _isSeachViewControllerHidden = YES; 36 | _titleBarHeight = CardStackTitleBarHeight; 37 | 38 | return self; 39 | } 40 | 41 | #pragma mark - Setters 42 | 43 | - (void)setCards:(NSArray *)cards { 44 | _cards = cards; 45 | 46 | for (NSUInteger i = 0; i < cards.count; i++) { 47 | CardView *card = [cards objectAtIndex:i]; 48 | card.tag = i; 49 | } 50 | } 51 | 52 | - (void)setViewControllers:(NSArray *)viewControllers { 53 | _viewControllers = [viewControllers copy]; 54 | 55 | [self.cards makeObjectsPerformSelector:@selector(removeFromSuperview)]; 56 | 57 | self.cards = [CardView cardsWithViewControllers:viewControllers]; 58 | for (CardView *card in self.cards) { 59 | card.delegate = self; 60 | card.titleFont = self.titleFont; 61 | card.titleBarHeight = self.titleBarHeight; 62 | [self.view addSubview:card]; 63 | } 64 | 65 | self.currentCardIndex = self.cards.count - 1; 66 | 67 | // make sure cards' title bar background colors have the depth effect 68 | [self updateCardTitleBarBackgroundColors]; 69 | } 70 | 71 | - (void)setCurrentCardIndex:(NSUInteger)currentCardIndex { 72 | [self setCurrentCardIndex:currentCardIndex animated:NO]; 73 | } 74 | 75 | - (void)setCurrentCardIndex:(NSUInteger)currentCardIndex animated:(BOOL)animated { 76 | _currentCardIndex = currentCardIndex; 77 | 78 | [self updateCardScales]; 79 | [self updateCardLocationsAnimated:animated]; 80 | } 81 | 82 | - (void)setTitleBarBackgroundColor:(UIColor *)titleBarBackgroundColor { 83 | _titleBarBackgroundColor = titleBarBackgroundColor; 84 | [self updateCardTitleBarBackgroundColors]; 85 | } 86 | 87 | - (void)setTitleFont:(UIFont *)titleFont { 88 | _titleFont = titleFont; 89 | for (CardView *card in self.cards) { 90 | card.titleFont = titleFont; 91 | } 92 | } 93 | 94 | - (void)setTitleBarHeight:(CGFloat)titleBarHeight { 95 | _titleBarHeight = titleBarHeight; 96 | for (CardView *card in self.cards) { 97 | card.titleBarHeight = titleBarHeight; 98 | } 99 | } 100 | 101 | - (void)setTitleLabelVerticalOffset:(CGFloat)titleLabelVerticalOffset { 102 | _titleLabelVerticalOffset = titleLabelVerticalOffset; 103 | for (CardView *card in self.cards) { 104 | card.titleLabelVerticalOffset = titleLabelVerticalOffset; 105 | } 106 | } 107 | 108 | - (void)setSearchViewController:(UIViewController *)searchViewController { 109 | [_searchViewController.view removeFromSuperview]; 110 | 111 | _searchViewController = searchViewController; 112 | [self.view addSubview:searchViewController.view]; 113 | 114 | CGRect frame = searchViewController.view.frame; 115 | frame.origin.y = -searchViewController.view.frame.size.height; 116 | searchViewController.view.frame = frame; 117 | } 118 | 119 | - (void)setIsSeachViewControllerHidden:(BOOL)isSeachViewControllerHidden { 120 | [self setIsSeachViewControllerHidden:isSeachViewControllerHidden 121 | animated:NO 122 | withCompletion:nil]; 123 | } 124 | 125 | - (void)setIsSeachViewControllerHidden:(BOOL)isSeachViewControllerHidden 126 | animated:(BOOL)animated 127 | withCompletion:(void(^)())completion { 128 | if (!isSeachViewControllerHidden && !self.searchViewController) { 129 | return; 130 | } 131 | 132 | _isSeachViewControllerHidden = isSeachViewControllerHidden; 133 | if (isSeachViewControllerHidden) { 134 | self.isOpen = NO; 135 | } 136 | 137 | POPSpringAnimation *springAnimation = [POPSpringAnimation animationWithPropertyNamed:kPOPViewFrame]; 138 | CGRect frame = self.searchViewController.view.frame; 139 | frame.origin.y = isSeachViewControllerHidden ? -self.searchViewController.view.frame.size.height : 0; 140 | springAnimation.toValue = [NSValue valueWithCGRect:frame]; 141 | springAnimation.springBounciness = CardStackDefaultSpringBounciness; 142 | springAnimation.completionBlock = ^(POPAnimation *anim, BOOL finished) { 143 | if (completion) { 144 | completion(); 145 | } 146 | if (isSeachViewControllerHidden && finished) { 147 | if ([self.delegate respondsToSelector:@selector(cardStackControllerDidCloseSearch:)]) { 148 | [self.delegate cardStackControllerDidCloseSearch:self]; 149 | } 150 | } 151 | }; 152 | [self.searchViewController.view pop_addAnimation:springAnimation forKey:@"frame"]; 153 | 154 | [self updateCardLocationsAnimated:animated]; 155 | } 156 | 157 | #pragma mark - Getters 158 | 159 | - (UIColor *)titleBarBackgroundColor { 160 | if (_titleBarBackgroundColor) return _titleBarBackgroundColor; 161 | 162 | _titleBarBackgroundColor = [UIColor orangeColor]; 163 | 164 | return _titleBarBackgroundColor; 165 | } 166 | 167 | - (UIColor *)titleColor { 168 | if (_titleColor) return _titleColor; 169 | 170 | _titleColor = [UIColor whiteColor]; 171 | 172 | return _titleColor; 173 | } 174 | 175 | - (UIFont *)titleFont { 176 | if (_titleFont) return _titleFont; 177 | 178 | _titleFont = [UIFont boldSystemFontOfSize:18.0f]; 179 | 180 | return _titleFont; 181 | } 182 | 183 | #pragma mark - CardDelegate 184 | 185 | - (void)cardTitleTapped:(CardView *)card { 186 | if (card.tag != self.currentCardIndex) { 187 | self.isOpen = NO; 188 | if (card.tag == self.cards.count - 1) { 189 | [self undockCardsWithVerticalVelocity:0]; 190 | } else { 191 | [self setCurrentCardIndex:card.tag 192 | animated:YES]; 193 | } 194 | } 195 | } 196 | 197 | - (void)cardTitlePanDidStart:(CardView *)card { 198 | self.originalCardFrame = card.frame; 199 | } 200 | 201 | - (void)card:(CardView *)card titlePannedByDelta:(CGPoint)delta { 202 | self.maximumTotalTitleBarHeightBeforeSearchAppears = 0; 203 | for (NSUInteger index = 0; index < self.currentCardIndex; index++) { 204 | CardView *card = [self.cards objectAtIndex:index]; 205 | self.maximumTotalTitleBarHeightBeforeSearchAppears += (card.titleBarHeight * card.scale - CardStackOffsetToAvoidAreaBelowTheTitleToBecomeVisible) * CardStackMaximumVisibleTitleBarProportion; 206 | } 207 | 208 | if ((card.tag != self.currentCardIndex && 209 | card.tag != self.cards.count - 1) || 210 | self.cards.count == 1) { 211 | return; 212 | } 213 | 214 | CGFloat y = self.originalCardFrame.origin.y + delta.y; 215 | if (y >= 0.0f) { 216 | CGRect frame = self.originalCardFrame; 217 | frame.origin.y = y; 218 | card.frame = frame; 219 | [self updateCardLocationsWhileOpening]; 220 | 221 | BOOL shouldConsiderOpeningSearchController = (self.searchViewController && 222 | self.isSeachViewControllerHidden && 223 | card.tag == self.currentCardIndex); 224 | if (shouldConsiderOpeningSearchController) { 225 | CGFloat topOfPreviousCard = CardStackTopMargin; 226 | if (self.currentCardIndex > 0) { 227 | CardView *previousCard = [self.cards objectAtIndex:self.currentCardIndex - 1]; 228 | topOfPreviousCard = previousCard.frame.origin.y; 229 | } 230 | 231 | BOOL shouldRevealSearchController = (y > self.maximumTotalTitleBarHeightBeforeSearchAppears); 232 | if (shouldRevealSearchController) { 233 | CGRect frame = self.searchViewController.view.frame; 234 | frame.origin.y = ((y - self.maximumTotalTitleBarHeightBeforeSearchAppears) - frame.size.height); 235 | if (frame.origin.y <= 0) { 236 | self.searchViewController.view.frame = frame; 237 | } 238 | } 239 | } 240 | } 241 | } 242 | 243 | - (void)cardTitlePanDidFinish:(CardView *)card withVelocity:(CGPoint)velocity { 244 | if ((card.tag != self.currentCardIndex && 245 | card.tag != self.cards.count - 1) || 246 | self.cards.count == 1) { 247 | return; 248 | } 249 | 250 | if (card.tag == self.currentCardIndex) { 251 | if (velocity.y < 0.0f) { 252 | self.isOpen = NO; 253 | 254 | BOOL shouldCloseSearchController = (self.searchViewController && 255 | !self.isSeachViewControllerHidden && 256 | card.tag == self.currentCardIndex); 257 | if (shouldCloseSearchController) { 258 | _isSeachViewControllerHidden = YES; 259 | POPSpringAnimation *springAnimation = [POPSpringAnimation animationWithPropertyNamed:kPOPViewFrame]; 260 | CGRect frame = self.searchViewController.view.frame; 261 | frame.origin.y = -self.searchViewController.view.frame.size.height; 262 | springAnimation.toValue = [NSValue valueWithCGRect:frame]; 263 | springAnimation.springBounciness = CardStackDefaultSpringBounciness; 264 | springAnimation.completionBlock = ^(POPAnimation *anim, BOOL finished) { 265 | if (finished) { 266 | if ([self.delegate respondsToSelector:@selector(cardStackControllerDidCloseSearch:)]) { 267 | [self.delegate cardStackControllerDidCloseSearch:self]; 268 | } 269 | } 270 | }; 271 | [self.searchViewController.view pop_addAnimation:springAnimation forKey:@"frame"]; 272 | } 273 | } else { 274 | CGFloat heightAboveCurrentCardWhenOpen = CardStackTopMargin; 275 | if (self.currentCardIndex > 0) { 276 | for (NSUInteger i = 0; i < self.currentCardIndex - 1; i++) { 277 | CardView *card = [self.cards objectAtIndex:i]; 278 | heightAboveCurrentCardWhenOpen += card.titleBarHeight * card.scale; 279 | } 280 | } 281 | self.isOpen = (card.frame.origin.y > heightAboveCurrentCardWhenOpen * CardStackOpenIfLargeThanPercent); 282 | 283 | if (self.searchViewController) { 284 | CGFloat heightOfVisiblePortionOfSearchViewController = (self.searchViewController.view.frame.origin.y + self.searchViewController.view.frame.size.height); 285 | _isSeachViewControllerHidden = (heightOfVisiblePortionOfSearchViewController < self.searchViewController.view.frame.size.height * CardStackMinimumSearchViewControllerHeightPropotion); 286 | } 287 | } 288 | [self updateCardLocationsAnimatedWithVerticalVelocity:velocity.y]; 289 | [self updateSearchViewControllerLocationAnimatedWithVerticalVelocity:velocity.y]; 290 | } else if (card.tag == self.cards.count - 1) { 291 | if (velocity.y < 0.0f && 292 | fabs(velocity.y) > CardStackVerticalVelocityLimitWhenMakingCardCurrent) { 293 | [self undockCardsWithVerticalVelocity:velocity.y]; 294 | } else { 295 | [self updateCardLocationsAnimated:YES]; 296 | } 297 | } 298 | } 299 | 300 | #pragma mark - Other methods 301 | 302 | - (CardView *)cardViewForViewController:(UIViewController *)viewController { 303 | CardView *card; 304 | 305 | for (NSUInteger index = 0; index < self.viewControllers.count; index++) { 306 | if ([[self.viewControllers objectAtIndex:index] isEqual:viewController]) { 307 | card = [self.cards objectAtIndex:index]; 308 | break; 309 | } 310 | } 311 | 312 | return card; 313 | } 314 | 315 | - (NSUInteger)indexForViewController:(UIViewController *)viewController { 316 | __block NSInteger foundIndex = NSNotFound; 317 | [self.viewControllers enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 318 | *stop = [obj isEqual:viewController]; 319 | if (*stop) { 320 | foundIndex = idx; 321 | } 322 | }]; 323 | 324 | return foundIndex; 325 | } 326 | 327 | - (void)insertCardWithViewController:(UIViewController *)viewController 328 | withTitle:(NSString *)title 329 | aboveViewController:(UIViewController *)aboveViewController 330 | makeCurrent:(BOOL)makeCurrent 331 | animated:(BOOL)animated 332 | withCompletion:(void(^)())completion { 333 | NSUInteger index = 0; 334 | for (UIViewController *v in self.viewControllers) { 335 | if ([v isEqual:aboveViewController]) { 336 | break; 337 | } 338 | ++index; 339 | } 340 | if (index == self.viewControllers.count) { 341 | return; 342 | } 343 | 344 | NSMutableArray *mutableViewControllers = [self.viewControllers mutableCopy]; 345 | [mutableViewControllers insertObject:viewController atIndex:index]; 346 | 347 | // don't user setter to avoid full rebuild 348 | _viewControllers = [mutableViewControllers copy]; 349 | 350 | CardView *aboveCard = [self.cards objectAtIndex:index]; 351 | CardView *card = [CardView cardWithViewController:viewController]; 352 | card.delegate = self; 353 | card.title = title; 354 | card.titleFont = self.titleFont; 355 | card.titleBarBackgroundColor = self.titleBarBackgroundColor; 356 | card.titleBarHeight = self.titleBarHeight; 357 | NSMutableArray *mutableCards = [self.cards mutableCopy]; 358 | [mutableCards insertObject:card atIndex:index]; 359 | self.cards = [mutableCards copy]; 360 | [self.view insertSubview:card belowSubview:aboveCard]; 361 | 362 | // this will make the new card appear from beneath the old card (if the stack is open) 363 | card.frame = aboveCard.frame; 364 | 365 | if (animated) { 366 | [UIView animateWithDuration:0.5 animations:^{ 367 | [self updateCardScales]; 368 | [self updateCardLocations]; 369 | [self updateCardTitleBarBackgroundColors]; 370 | if (!makeCurrent) { 371 | self.currentCardIndex = index + 1; 372 | } 373 | } completion:^(BOOL finished) { 374 | if (completion) { 375 | completion(); 376 | } 377 | }]; 378 | } else { 379 | [self updateCardScales]; 380 | [self updateCardLocations]; 381 | [self updateCardTitleBarBackgroundColors]; 382 | if (!makeCurrent) { 383 | self.currentCardIndex = index + 1; 384 | } 385 | if (completion) { 386 | completion(); 387 | } 388 | } 389 | } 390 | 391 | - (void)insertCardWithViewController:(UIViewController *)viewController 392 | withTitle:(NSString *)title 393 | belowViewController:(UIViewController *)belowViewController 394 | makeCurrent:(BOOL)makeCurrent 395 | animated:(BOOL)animated 396 | withCompletion:(void(^)())completion { 397 | NSUInteger index = 0; 398 | for (UIViewController *v in self.viewControllers) { 399 | if ([v isEqual:belowViewController]) { 400 | break; 401 | } 402 | ++index; 403 | } 404 | if (index == self.viewControllers.count) { 405 | return; 406 | } 407 | 408 | NSMutableArray *mutableViewControllers = [self.viewControllers mutableCopy]; 409 | [mutableViewControllers insertObject:viewController atIndex:index + 1]; 410 | 411 | // don't user setter to avoid full rebuild 412 | _viewControllers = [mutableViewControllers copy]; 413 | 414 | CardView *belowCard = [self.cards objectAtIndex:index]; 415 | CardView *card = [CardView cardWithViewController:viewController]; 416 | card.delegate = self; 417 | card.title = title; 418 | card.titleFont = self.titleFont; 419 | card.titleBarBackgroundColor = self.titleBarBackgroundColor; 420 | card.titleBarHeight = self.titleBarHeight; 421 | NSMutableArray *mutableCards = [self.cards mutableCopy]; 422 | [mutableCards insertObject:card atIndex:index + 1]; 423 | self.cards = [mutableCards copy]; 424 | [self.view insertSubview:card aboveSubview:belowCard]; 425 | BOOL cardAnimationShouldStartOutsideOfTheScreen = (index == self.currentCardIndex && makeCurrent && animated); 426 | if (cardAnimationShouldStartOutsideOfTheScreen) { 427 | CGRect frame = card.frame; 428 | frame.origin.y = self.view.bounds.size.height; 429 | card.frame = frame; 430 | } 431 | 432 | if (animated) { 433 | [UIView animateWithDuration:0.5 animations:^{ 434 | [self updateCardScales]; 435 | [self updateCardLocations]; 436 | [self updateCardTitleBarBackgroundColors]; 437 | if (makeCurrent) { 438 | self.currentCardIndex = index + 1; 439 | } 440 | } completion:^(BOOL finished) { 441 | if (completion) { 442 | completion(); 443 | } 444 | }]; 445 | } else { 446 | [self updateCardScales]; 447 | [self updateCardLocations]; 448 | [self updateCardTitleBarBackgroundColors]; 449 | if (makeCurrent) { 450 | self.currentCardIndex = index + 1; 451 | } 452 | if (completion) { 453 | completion(); 454 | } 455 | } 456 | } 457 | 458 | - (void)removeCardAtIndex:(NSUInteger)index 459 | animated:(BOOL)animated 460 | withCompletion:(void(^)())completion { 461 | if (self.cards.count < 2 || index > self.cards.count - 1) { 462 | return; 463 | } 464 | 465 | BOOL shouldAvoidUnwantedAnimationIfTopmostCardIsRemoved = (!self.isOpen); 466 | if (shouldAvoidUnwantedAnimationIfTopmostCardIsRemoved) { 467 | animated = NO; 468 | } 469 | 470 | NSMutableArray *mutableViewControllers = [self.viewControllers mutableCopy]; 471 | [mutableViewControllers removeObjectAtIndex:index]; 472 | 473 | // don't user setter to avoid full rebuild 474 | _viewControllers = [mutableViewControllers copy]; 475 | 476 | __block CardView *card = [self.cards objectAtIndex:index]; 477 | NSMutableArray *mutableCards = [self.cards mutableCopy]; 478 | [mutableCards removeObjectAtIndex:index]; 479 | self.cards = [mutableCards copy]; 480 | 481 | if (animated) { 482 | [UIView animateWithDuration:0.5 animations:^{ 483 | if (self.cards.count == 1 && self.isOpen) { 484 | self.isOpen = NO; 485 | } 486 | [self updateCardScales]; 487 | [self updateCardLocations]; 488 | [self updateCardTitleBarBackgroundColors]; 489 | if (self.currentCardIndex > 0 && self.currentCardIndex > self.cards.count - 1) { 490 | self.currentCardIndex = self.cards.count - 1; 491 | } 492 | 493 | CGRect frame = card.frame; 494 | frame.origin.x = frame.origin.x + self.view.bounds.size.width; 495 | card.frame = frame; 496 | } completion:^(BOOL finished) { 497 | // removal from superview is diferred to make sure a proper removal animation is visible 498 | [card removeFromSuperview]; 499 | 500 | if (completion) { 501 | completion(); 502 | } 503 | }]; 504 | } else { 505 | if (self.cards.count == 1 && self.isOpen) { 506 | self.isOpen = NO; 507 | } 508 | [self updateCardScales]; 509 | [self updateCardLocations]; 510 | [self updateCardTitleBarBackgroundColors]; 511 | if (self.currentCardIndex > 0 && self.currentCardIndex > self.cards.count - 1) { 512 | self.currentCardIndex = self.cards.count - 1; 513 | } 514 | [card removeFromSuperview]; 515 | if (completion) { 516 | completion(); 517 | } 518 | } 519 | } 520 | 521 | - (void)updateCardScales { 522 | NSUInteger index = 0; 523 | for (CardView *card in self.cards) { 524 | CGFloat scale = 1.0f; 525 | if (index < self.currentCardIndex) { 526 | NSInteger relativeIndex = index - self.currentCardIndex; 527 | scale = 1.0 + relativeIndex * CardStackDepthOffset; 528 | } 529 | card.scale = scale; 530 | ++index; 531 | } 532 | } 533 | 534 | - (void)updateCardTitleBarBackgroundColors { 535 | for (NSUInteger i = 0; i < self.cards.count; i++) { 536 | CardView *card = [self.cards objectAtIndex:i]; 537 | card.titleBarBackgroundColor = self.titleBarBackgroundColor; 538 | if ([self.dataSource respondsToSelector:@selector(cardStackController:titleBarDecorationColorForCardAtIndex:)]) { 539 | card.titleBarDecorationColor = [self.dataSource cardStackController:self titleBarDecorationColorForCardAtIndex:i]; 540 | } 541 | if ([self.dataSource respondsToSelector:@selector(cardStackController:titleBarShadowImageForCardAtIndex:)]) { 542 | card.titleBarShadowImage = [self.dataSource cardStackController:self titleBarShadowImageForCardAtIndex:i]; 543 | } 544 | if ([self.dataSource respondsToSelector:@selector(cardStackController:titleBarShineImageForCardAtIndex:)]) { 545 | card.titleBarShineImage = [self.dataSource cardStackController:self titleBarShineImageForCardAtIndex:i]; 546 | } 547 | } 548 | } 549 | 550 | - (void)updateCardLocations { 551 | [self updateCardLocationsAnimated:NO]; 552 | } 553 | 554 | - (void)updateCardLocationsAnimated:(BOOL)animated { 555 | if (animated) { 556 | [self updateCardLocationsAnimatedWithVerticalVelocity:0.0f]; 557 | } else { 558 | for (NSUInteger i = 0; i < self.cards.count; i++) { 559 | CardView *card = [self.cards objectAtIndex:i]; 560 | card.frame = [self frameForCardAtIndex:i]; 561 | } 562 | } 563 | } 564 | 565 | - (void)updateCardLocationsAnimatedWithVerticalVelocity:(CGFloat)verticalVelocity { 566 | for (NSUInteger i = 0; i < self.cards.count; i++) { 567 | CardView *card = [self.cards objectAtIndex:i]; 568 | POPSpringAnimation *springAnimation = [POPSpringAnimation animationWithPropertyNamed:kPOPViewFrame]; 569 | CGRect frame = [self frameForCardAtIndex:i]; 570 | springAnimation.toValue = [NSValue valueWithCGRect:frame]; 571 | springAnimation.springBounciness = CardStackDefaultSpringBounciness; 572 | BOOL shouldScaleDownVelocityForUpperCardsAvoidingSpringEffectForCardsNearToTheTop = (verticalVelocity > 0.0f && i <= self.currentCardIndex); 573 | if (shouldScaleDownVelocityForUpperCardsAvoidingSpringEffectForCardsNearToTheTop) { 574 | CGFloat springVelocity = (verticalVelocity * card.scale) * ((CGFloat)i / (CGFloat)(self.currentCardIndex + 1)); 575 | if (self.searchViewController && !self.isSeachViewControllerHidden) { 576 | springVelocity *= CardStackTitleBarHeightProportionWhenSearchViewControllerIsVisible; 577 | } 578 | springAnimation.velocity = [NSValue valueWithCGRect:CGRectMake(0, springVelocity, 0, 0)]; 579 | } 580 | [card pop_addAnimation:springAnimation forKey:@"frame"]; 581 | } 582 | } 583 | 584 | - (void)updateCardLocationsWhileOpening { 585 | if (self.currentCardIndex > 0) { 586 | CGFloat startY = CardStackTopMargin; 587 | if (self.searchViewController) { 588 | startY += self.searchViewController.view.frame.origin.y + self.searchViewController.view.frame.size.height; 589 | } 590 | CardView *currentCard = [self.cards objectAtIndex:self.currentCardIndex]; 591 | CGFloat endY = currentCard.frame.origin.y; 592 | CGFloat currentY = startY; 593 | CGFloat incrementY = (endY - startY) / self.currentCardIndex; 594 | for (NSUInteger i = 0; i < self.currentCardIndex; i++) { 595 | currentCard = [self.cards objectAtIndex:i]; 596 | CGRect frame = currentCard.frame; 597 | frame.origin.y = currentY; 598 | currentCard.frame = frame; 599 | currentY = currentY + incrementY; 600 | } 601 | } 602 | } 603 | 604 | - (void)updateSearchViewControllerLocationAnimatedWithVerticalVelocity:(CGFloat)verticalVelocity { 605 | if (self.searchViewController) { 606 | POPSpringAnimation *springAnimation = [POPSpringAnimation animationWithPropertyNamed:kPOPViewFrame]; 607 | CGRect frame = self.searchViewController.view.frame; 608 | frame.origin.y = (self.isSeachViewControllerHidden ? -self.searchViewController.view.frame.size.height : 0); 609 | springAnimation.toValue = [NSValue valueWithCGRect:frame]; 610 | springAnimation.springBounciness = CardStackDefaultSpringBounciness; 611 | springAnimation.completionBlock = ^(POPAnimation *anim, BOOL finished) { 612 | if (self.isSeachViewControllerHidden && finished) { 613 | if ([self.delegate respondsToSelector:@selector(cardStackControllerDidCloseSearch:)]) { 614 | [self.delegate cardStackControllerDidCloseSearch:self]; 615 | } 616 | } 617 | }; 618 | [self.searchViewController.view pop_addAnimation:springAnimation forKey:@"frame"]; 619 | } 620 | } 621 | 622 | - (CGRect)frameForCardAtIndex:(NSUInteger)index { 623 | CGRect frame; 624 | 625 | // Note: Cards at the bottom but behind the last card will be positioned 626 | // outside of the visible area, so their title bar won't show up when the 627 | // last card is being moved. 628 | BOOL shouldCardRemainInvisibleEvenIfLastCardIsMoved = (index > self.currentCardIndex && index < self.cards.count - 1); 629 | if (index <= self.currentCardIndex) { 630 | if (self.isOpen) { 631 | CGFloat previousTitleBarHeights = CardStackTopMargin; 632 | if (self.searchViewController && !self.isSeachViewControllerHidden) { 633 | previousTitleBarHeights += self.searchViewController.view.frame.size.height; 634 | } 635 | for (NSUInteger i = 0; i < index; i++) { 636 | CardView *card = [self.cards objectAtIndex:i]; 637 | CGFloat titleBarHeight = (card.titleBarHeight * card.scale - CardStackOffsetToAvoidAreaBelowTheTitleToBecomeVisible); 638 | if (self.searchViewController && !self.isSeachViewControllerHidden) { 639 | titleBarHeight *= CardStackTitleBarHeightProportionWhenSearchViewControllerIsVisible; 640 | } 641 | previousTitleBarHeights += titleBarHeight; 642 | } 643 | 644 | CardView *card = [self.cards objectAtIndex:index]; 645 | frame = card.frame; 646 | frame.origin.y = previousTitleBarHeights; 647 | } else { 648 | CardView *card = [self.cards objectAtIndex:index]; 649 | frame = card.frame; 650 | frame.origin.y = 0; 651 | if (self.searchViewController && !self.isSeachViewControllerHidden) { 652 | frame.origin.y += self.searchViewController.view.frame.size.height; 653 | } 654 | } 655 | } else if (shouldCardRemainInvisibleEvenIfLastCardIsMoved) { 656 | CardView *card = [self.cards objectAtIndex:index]; 657 | frame = card.frame; 658 | frame.origin.y = self.view.bounds.size.height; 659 | } else { 660 | CardView *card = [self.cards objectAtIndex:index]; 661 | frame = card.frame; 662 | frame.origin.y = self.view.bounds.size.height - card.titleBarHeight; 663 | } 664 | 665 | CGFloat contentHeight = self.view.bounds.size.height; 666 | if (index == self.currentCardIndex) { 667 | CardView *lastCard = [self.cards lastObject]; 668 | BOOL isCurrentCardTheLast = (self.currentCardIndex == self.cards.count - 1); 669 | contentHeight = self.view.bounds.size.height - (frame.origin.y + (isCurrentCardTheLast ? 0 : lastCard.titleBarHeight)); 670 | } 671 | UIViewController *viewController = [self.viewControllers objectAtIndex:index]; 672 | if ([viewController respondsToSelector:@selector(setContentHeight:)]) { 673 | id resizeableViewController = (id)viewController; 674 | [resizeableViewController setContentHeight:contentHeight]; 675 | } 676 | 677 | return frame; 678 | } 679 | 680 | - (void)moveCard:(CardView *)card 681 | toFrame:(CGRect)frame 682 | springBounciness:(CGFloat)bounciness 683 | velocity:(CGPoint)velocity 684 | withCompletion:(void(^)())completion { 685 | POPSpringAnimation *springAnimation = [POPSpringAnimation animationWithPropertyNamed:kPOPViewFrame]; 686 | springAnimation.toValue = [NSValue valueWithCGRect:frame]; 687 | springAnimation.springBounciness = bounciness; 688 | springAnimation.velocity = [NSValue valueWithCGRect:CGRectMake(velocity.x, velocity.y, 0, 0)]; 689 | springAnimation.completionBlock = ^(POPAnimation *anim, BOOL finished) { 690 | if (completion) { 691 | completion(); 692 | } 693 | }; 694 | [card pop_addAnimation:springAnimation forKey:@"frame"]; 695 | } 696 | 697 | - (void)undockCardsWithVerticalVelocity:(CGFloat)verticalVelocity { 698 | CardView *card = [self.cards lastObject]; 699 | CGRect frame = [self frameForCardAtIndex:card.tag]; 700 | frame.origin.y = 0; 701 | [self moveCard:card toFrame:frame springBounciness:8.0f velocity:CGPointMake(0, verticalVelocity) withCompletion:^{ 702 | self.isOpen = NO; 703 | self.currentCardIndex = self.cards.count - 1; 704 | [self updateCardScales]; 705 | [self updateCardLocations]; 706 | if ([self.delegate respondsToSelector:@selector(cardStackControllerDidUndockCards:)]) { 707 | [self.delegate cardStackControllerDidUndockCards:self]; 708 | } 709 | }]; 710 | } 711 | 712 | @end 713 | -------------------------------------------------------------------------------- /Source/CardView.h: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | @import UIKit; 3 | 4 | @protocol CardViewDelegate; 5 | 6 | @interface CardView : UIView 7 | 8 | @property (weak, nonatomic) id delegate; 9 | 10 | @property (nonatomic) CGFloat scale; 11 | @property (nonatomic) UIColor *titleBarBackgroundColor; 12 | @property (nonatomic) UIColor *titleBarDecorationColor; 13 | @property (nonatomic) UIImage *titleBarShadowImage; 14 | @property (nonatomic) UIImage *titleBarShineImage; 15 | @property (nonatomic) UIColor *titleColor; 16 | @property (nonatomic) UIFont *titleFont; 17 | @property (nonatomic) NSString *title; 18 | @property (nonatomic) UIPanGestureRecognizer *panRecognizer; 19 | @property (nonatomic) CGFloat titleBarHeight; 20 | @property (nonatomic) CGFloat titleLabelVerticalOffset; 21 | 22 | + (CardView *)cardWithViewController:(UIViewController *)viewController; 23 | + (NSArray *)cardsWithViewControllers:(NSArray *)viewControllers; 24 | 25 | @end 26 | 27 | @protocol CardViewDelegate 28 | 29 | @optional 30 | - (void)cardTitleTapped:(CardView *)card; 31 | - (void)cardTitlePanDidStart:(CardView *)card; 32 | - (void)card:(CardView *)card titlePannedByDelta:(CGPoint)delta; 33 | - (void)cardTitlePanDidFinish:(CardView *)card withVelocity:(CGPoint)velocity; 34 | 35 | @end 36 | -------------------------------------------------------------------------------- /Source/CardView.m: -------------------------------------------------------------------------------- 1 | #import "CardView.h" 2 | #import "Masonry.h" 3 | 4 | static const CGFloat CardTitleBarHeight = 44.0f; 5 | static const CGFloat CardCornerRadius = 4.0f; 6 | static const CGFloat CardShadowImageViewVerticalOffset = -13.0f; 7 | static const CGFloat CardStackTitleBarShineImageHeight = 1.0f; 8 | 9 | @interface CardView () 10 | 11 | @property (nonatomic) UIViewController *viewController; 12 | @property (nonatomic) UIView *contentView; 13 | @property (nonatomic) UIView *titleBarView; 14 | @property (nonatomic) UIView *titleBarDecorationView; 15 | @property (nonatomic) UIImageView *titleBarShadowImageView; 16 | @property (nonatomic) UIImageView *titleBarShineImageView; 17 | @property (nonatomic) UILabel *titleLabel; 18 | @property (nonatomic) UITapGestureRecognizer *tapRecognizer; 19 | 20 | @property (nonatomic) MASConstraint *titleBarHeightConstraint; 21 | @property (nonatomic) MASConstraint *titleLabelTopConstraint; 22 | 23 | @end 24 | 25 | @implementation CardView 26 | 27 | @synthesize titleColor = _titleColor; 28 | @synthesize titleFont = _titleFont; 29 | @synthesize titleBarDecorationColor = _titleBarDecorationColor; 30 | 31 | + (CardView *)cardWithViewController:(UIViewController *)viewController { 32 | CardView *card = [[CardView alloc] initWithFrame:[UIScreen mainScreen].bounds]; 33 | card.viewController = viewController; 34 | return card; 35 | } 36 | 37 | + (NSArray *)cardsWithViewControllers:(NSArray *)viewControllers { 38 | NSMutableArray *cards = [NSMutableArray array]; 39 | 40 | for (UIViewController *viewController in viewControllers) { 41 | [cards addObject:[CardView cardWithViewController:viewController]]; 42 | } 43 | 44 | return [NSArray arrayWithArray:cards]; 45 | } 46 | 47 | - (instancetype)initWithFrame:(CGRect)frame { 48 | self = [super initWithFrame:frame]; 49 | if (!self) return nil; 50 | 51 | self.titleBarHeight = CardTitleBarHeight; 52 | 53 | [self addSubview:self.titleBarShadowImageView]; 54 | [self addSubview:self.contentView]; 55 | [self.contentView addSubview:self.titleBarView]; 56 | [self.titleBarView addSubview:self.titleBarDecorationView]; 57 | [self.titleBarView addSubview:self.titleBarShineImageView]; 58 | [self.titleBarView addSubview:self.titleLabel]; 59 | 60 | [self.titleBarView mas_makeConstraints:^(MASConstraintMaker *make) { 61 | make.left.equalTo(self.contentView.mas_left); 62 | make.right.equalTo(self.contentView.mas_right); 63 | make.top.equalTo(self.contentView.mas_top); 64 | self.titleBarHeightConstraint = make.height.equalTo(@(self.titleBarHeight)); 65 | }]; 66 | 67 | [self.titleBarDecorationView mas_makeConstraints:^(MASConstraintMaker *make) { 68 | make.center.equalTo(self.titleBarView); 69 | make.size.equalTo(self.titleBarView); 70 | }]; 71 | 72 | [self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) { 73 | self.titleLabelTopConstraint = make.top.equalTo(self.titleBarView.mas_top).with.offset(self.titleLabelVerticalOffset); 74 | make.centerX.equalTo(self.titleBarView.mas_centerX); 75 | make.width.equalTo(self.titleBarView); 76 | make.height.equalTo(self.titleBarView); 77 | }]; 78 | 79 | // using shadows drops frame rate noticeably even on an iPhone 6 80 | // self.layer.shadowColor = [[UIColor blackColor] CGColor]; 81 | // self.layer.shadowOpacity = 0.5; 82 | 83 | return self; 84 | } 85 | 86 | #pragma mark - Getters 87 | 88 | - (UIColor *)titleBarBackgroundColor { 89 | return self.titleBarView.backgroundColor; 90 | } 91 | 92 | - (UIColor *)titleBarDecorationColor { 93 | if (_titleBarDecorationColor) return _titleBarDecorationColor; 94 | 95 | _titleBarDecorationColor = [UIColor clearColor]; 96 | 97 | return _titleBarDecorationColor; 98 | } 99 | 100 | - (UIColor *)titleColor { 101 | if (_titleColor) return _titleColor; 102 | 103 | _titleColor = [UIColor whiteColor]; 104 | 105 | return _titleColor; 106 | } 107 | 108 | - (UIFont *)titleFont { 109 | if (_titleFont) return _titleFont; 110 | 111 | _titleFont = [UIFont boldSystemFontOfSize:18.0f]; 112 | 113 | return _titleFont; 114 | } 115 | 116 | - (UIView *)contentView { 117 | if (_contentView) return _contentView; 118 | 119 | _contentView = [[UIView alloc] initWithFrame:self.bounds]; 120 | _contentView.layer.cornerRadius = CardCornerRadius; 121 | _contentView.clipsToBounds = YES; 122 | 123 | return _contentView; 124 | } 125 | 126 | - (UIView *)titleBarView { 127 | if (_titleBarView) return _titleBarView; 128 | 129 | _titleBarView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.frame.size.width, self.titleBarHeight)]; 130 | _titleBarView.backgroundColor = self.titleBarBackgroundColor; 131 | _titleBarView.userInteractionEnabled = YES; 132 | _titleBarView.layer.cornerRadius = CardCornerRadius; 133 | _titleBarView.clipsToBounds = YES; 134 | [_titleBarView addGestureRecognizer:self.tapRecognizer]; 135 | [_titleBarView addGestureRecognizer:self.panRecognizer]; 136 | 137 | return _titleBarView; 138 | } 139 | 140 | - (UIView *)titleBarDecorationView { 141 | if (_titleBarDecorationView) return _titleBarDecorationView; 142 | 143 | _titleBarDecorationView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.frame.size.width, self.titleBarHeight)]; 144 | _titleBarDecorationView.backgroundColor = self.titleBarDecorationColor; 145 | 146 | return _titleBarDecorationView; 147 | } 148 | 149 | - (UIImageView *)titleBarShadowImageView { 150 | if (_titleBarShadowImageView) return _titleBarShadowImageView; 151 | 152 | _titleBarShadowImageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, CardShadowImageViewVerticalOffset, self.bounds.size.width, self.titleBarHeight)]; 153 | 154 | return _titleBarShadowImageView; 155 | } 156 | 157 | - (UIImageView *)titleBarShineImageView { 158 | if (_titleBarShineImageView) return _titleBarShineImageView; 159 | 160 | _titleBarShineImageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, self.bounds.size.width, CardStackTitleBarShineImageHeight)]; 161 | 162 | return _titleBarShineImageView; 163 | } 164 | 165 | - (UILabel *)titleLabel { 166 | if (_titleLabel) return _titleLabel; 167 | 168 | _titleLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, self.titleBarView.bounds.size.width, self.titleBarHeight)]; 169 | _titleLabel.textAlignment = NSTextAlignmentCenter; 170 | _titleLabel.textColor = self.titleColor; 171 | _titleLabel.font = self.titleFont; 172 | 173 | return _titleLabel; 174 | } 175 | 176 | - (UITapGestureRecognizer *)tapRecognizer { 177 | if (_tapRecognizer) return _tapRecognizer; 178 | 179 | _tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapAction:)]; 180 | 181 | return _tapRecognizer; 182 | } 183 | 184 | - (UIPanGestureRecognizer *)panRecognizer { 185 | if (_panRecognizer) return _panRecognizer; 186 | 187 | _panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panAction:)]; 188 | 189 | return _panRecognizer; 190 | } 191 | 192 | #pragma mark - Setters 193 | 194 | - (void)setScale:(CGFloat)scale { 195 | _scale = scale; 196 | self.layer.transform = CATransform3DMakeScale(self.scale, self.scale, 1.0); 197 | } 198 | 199 | - (void)setTitleBarBackgroundColor:(UIColor *)titleBarBackgroundColor { 200 | self.titleBarView.backgroundColor = titleBarBackgroundColor; 201 | } 202 | 203 | - (void)setTitleBarDecorationColor:(UIColor *)titleBarDecorationColor { 204 | _titleBarDecorationColor = titleBarDecorationColor; 205 | self.titleBarDecorationView.backgroundColor = titleBarDecorationColor; 206 | 207 | if (titleBarDecorationColor) { 208 | CGFloat alpha; 209 | [titleBarDecorationColor getRed:nil green:nil blue:nil alpha:&alpha]; 210 | self.titleBarShineImageView.alpha = alpha; 211 | } 212 | } 213 | 214 | - (void)setTitleBarShadowImage:(UIImage *)titleBarShadowImage { 215 | _titleBarShadowImage = titleBarShadowImage; 216 | self.titleBarShadowImageView.image = titleBarShadowImage; 217 | } 218 | 219 | - (void)setTitleBarShineImage:(UIImage *)titleBarShineImage { 220 | _titleBarShineImage = titleBarShineImage; 221 | self.titleBarShineImageView.image = titleBarShineImage; 222 | } 223 | 224 | - (void)setTitleColor:(UIColor *)titleColor { 225 | _titleColor = titleColor; 226 | self.titleLabel.textColor = titleColor; 227 | } 228 | 229 | - (void)setTitleFont:(UIFont *)titleFont { 230 | _titleFont = titleFont; 231 | self.titleLabel.font = titleFont; 232 | } 233 | 234 | - (void)setTitle:(NSString *)title { 235 | _title = title; 236 | self.titleLabel.text = title; 237 | } 238 | 239 | - (void)setTitleBarHeight:(CGFloat)titleBarHeight { 240 | _titleBarHeight = titleBarHeight; 241 | 242 | if (self.titleBarHeightConstraint) { 243 | self.titleBarHeightConstraint.offset(titleBarHeight); 244 | } 245 | } 246 | 247 | - (void)setTitleLabelVerticalOffset:(CGFloat)titleLabelVerticalOffset { 248 | _titleLabelVerticalOffset = titleLabelVerticalOffset; 249 | 250 | if (self.titleLabelTopConstraint) { 251 | self.titleLabelTopConstraint.offset(titleLabelVerticalOffset); 252 | } 253 | } 254 | 255 | - (void)setViewController:(UIViewController *)viewController { 256 | _viewController = viewController; 257 | [self.contentView addSubview:viewController.view]; 258 | [self.contentView bringSubviewToFront:self.titleBarView]; 259 | } 260 | 261 | #pragma mark - Gesture recognizers 262 | 263 | - (void)tapAction:(UITapGestureRecognizer *)tapRecognizer { 264 | if ([self.delegate respondsToSelector:@selector(cardTitleTapped:)]) { 265 | [self.delegate cardTitleTapped:self]; 266 | } 267 | } 268 | 269 | - (void)panAction:(UIPanGestureRecognizer *)panRecognizer { 270 | static CGPoint originalPoint; 271 | 272 | switch (panRecognizer.state) { 273 | case UIGestureRecognizerStateBegan: 274 | originalPoint = [panRecognizer locationInView:self.superview]; 275 | if ([self.delegate respondsToSelector:@selector(cardTitlePanDidStart:)]) { 276 | [self.delegate cardTitlePanDidStart:self]; 277 | } 278 | break; 279 | case UIGestureRecognizerStateChanged: { 280 | CGPoint point = [panRecognizer locationInView:self.superview]; 281 | CGPoint delta = CGPointMake(point.x - originalPoint.x, point.y - originalPoint.y); 282 | if ([self.delegate respondsToSelector:@selector(card:titlePannedByDelta:)]) { 283 | [self.delegate card:self titlePannedByDelta:delta]; 284 | } 285 | break; 286 | } 287 | case UIGestureRecognizerStateCancelled: 288 | case UIGestureRecognizerStateEnded: 289 | if ([self.delegate respondsToSelector:@selector(cardTitlePanDidFinish:withVelocity:)]) { 290 | [self.delegate cardTitlePanDidFinish:self withVelocity:[panRecognizer velocityInView:self.superview]]; 291 | } 292 | break; 293 | case UIGestureRecognizerStatePossible: 294 | case UIGestureRecognizerStateFailed: 295 | break; 296 | } 297 | } 298 | 299 | #pragma mark - Other methods 300 | 301 | - (NSString *)description { 302 | return [NSString stringWithFormat:@"%@ (%@)", self.title, self.viewController]; 303 | } 304 | 305 | @end 306 | --------------------------------------------------------------------------------