├── PhotoStackViewTest.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcuserdata │ │ └── cyz.xcuserdatad │ │ └── UserInterfaceState.xcuserstate └── xcuserdata │ └── cyz.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ ├── PhotoStackViewTest.xcscheme │ └── xcschememanagement.plist ├── PhotoStackViewTest ├── AppDelegate.swift ├── Base.lproj │ ├── LaunchScreen.xib │ └── Main.storyboard ├── Images.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Info.plist ├── PhotoBorder.png ├── PhotoBorder@2x.png ├── PhotoStackView.swift ├── QQ头像 │ ├── github_1.jpeg │ ├── github_2.jpeg │ ├── github_3.jpeg │ ├── github_4.jpeg │ ├── github_5.jpeg │ └── 头像.jpg └── ViewController.swift ├── PhotoStackViewTestTests ├── Info.plist └── PhotoStackViewTestTests.swift └── README.md /PhotoStackViewTest.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 651A963C1B1DA95800D87D14 /* github_1.jpeg in Resources */ = {isa = PBXBuildFile; fileRef = 651A96361B1DA95800D87D14 /* github_1.jpeg */; }; 11 | 651A963D1B1DA95800D87D14 /* github_2.jpeg in Resources */ = {isa = PBXBuildFile; fileRef = 651A96371B1DA95800D87D14 /* github_2.jpeg */; }; 12 | 651A963E1B1DA95800D87D14 /* github_3.jpeg in Resources */ = {isa = PBXBuildFile; fileRef = 651A96381B1DA95800D87D14 /* github_3.jpeg */; }; 13 | 651A963F1B1DA95800D87D14 /* github_4.jpeg in Resources */ = {isa = PBXBuildFile; fileRef = 651A96391B1DA95800D87D14 /* github_4.jpeg */; }; 14 | 651A96401B1DA95800D87D14 /* github_5.jpeg in Resources */ = {isa = PBXBuildFile; fileRef = 651A963A1B1DA95800D87D14 /* github_5.jpeg */; }; 15 | 651A96411B1DA95800D87D14 /* 头像.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 651A963B1B1DA95800D87D14 /* 头像.jpg */; }; 16 | 6552DB971B1C830A00AE50F8 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6552DB961B1C830A00AE50F8 /* AppDelegate.swift */; }; 17 | 6552DB991B1C830A00AE50F8 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6552DB981B1C830A00AE50F8 /* ViewController.swift */; }; 18 | 6552DB9C1B1C830A00AE50F8 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6552DB9A1B1C830A00AE50F8 /* Main.storyboard */; }; 19 | 6552DB9E1B1C830A00AE50F8 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6552DB9D1B1C830A00AE50F8 /* Images.xcassets */; }; 20 | 6552DBA11B1C830A00AE50F8 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6552DB9F1B1C830A00AE50F8 /* LaunchScreen.xib */; }; 21 | 6552DBAD1B1C830A00AE50F8 /* PhotoStackViewTestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6552DBAC1B1C830A00AE50F8 /* PhotoStackViewTestTests.swift */; }; 22 | 6552DBB71B1C831900AE50F8 /* PhotoStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6552DBB61B1C831900AE50F8 /* PhotoStackView.swift */; }; 23 | 6552DBBA1B1C834D00AE50F8 /* PhotoBorder.png in Resources */ = {isa = PBXBuildFile; fileRef = 6552DBB81B1C834D00AE50F8 /* PhotoBorder.png */; }; 24 | 6552DBBB1B1C834D00AE50F8 /* PhotoBorder@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 6552DBB91B1C834D00AE50F8 /* PhotoBorder@2x.png */; }; 25 | /* End PBXBuildFile section */ 26 | 27 | /* Begin PBXContainerItemProxy section */ 28 | 6552DBA71B1C830A00AE50F8 /* PBXContainerItemProxy */ = { 29 | isa = PBXContainerItemProxy; 30 | containerPortal = 6552DB891B1C830A00AE50F8 /* Project object */; 31 | proxyType = 1; 32 | remoteGlobalIDString = 6552DB901B1C830A00AE50F8; 33 | remoteInfo = PhotoStackViewTest; 34 | }; 35 | /* End PBXContainerItemProxy section */ 36 | 37 | /* Begin PBXFileReference section */ 38 | 651A96361B1DA95800D87D14 /* github_1.jpeg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = github_1.jpeg; sourceTree = ""; }; 39 | 651A96371B1DA95800D87D14 /* github_2.jpeg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = github_2.jpeg; sourceTree = ""; }; 40 | 651A96381B1DA95800D87D14 /* github_3.jpeg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = github_3.jpeg; sourceTree = ""; }; 41 | 651A96391B1DA95800D87D14 /* github_4.jpeg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = github_4.jpeg; sourceTree = ""; }; 42 | 651A963A1B1DA95800D87D14 /* github_5.jpeg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = github_5.jpeg; sourceTree = ""; }; 43 | 651A963B1B1DA95800D87D14 /* 头像.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "头像.jpg"; sourceTree = ""; }; 44 | 6552DB911B1C830A00AE50F8 /* PhotoStackViewTest.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PhotoStackViewTest.app; sourceTree = BUILT_PRODUCTS_DIR; }; 45 | 6552DB951B1C830A00AE50F8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 46 | 6552DB961B1C830A00AE50F8 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 47 | 6552DB981B1C830A00AE50F8 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 48 | 6552DB9B1B1C830A00AE50F8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 49 | 6552DB9D1B1C830A00AE50F8 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 50 | 6552DBA01B1C830A00AE50F8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 51 | 6552DBA61B1C830A00AE50F8 /* PhotoStackViewTestTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PhotoStackViewTestTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 52 | 6552DBAB1B1C830A00AE50F8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 53 | 6552DBAC1B1C830A00AE50F8 /* PhotoStackViewTestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoStackViewTestTests.swift; sourceTree = ""; }; 54 | 6552DBB61B1C831900AE50F8 /* PhotoStackView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoStackView.swift; sourceTree = ""; }; 55 | 6552DBB81B1C834D00AE50F8 /* PhotoBorder.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = PhotoBorder.png; sourceTree = ""; }; 56 | 6552DBB91B1C834D00AE50F8 /* PhotoBorder@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "PhotoBorder@2x.png"; sourceTree = ""; }; 57 | /* End PBXFileReference section */ 58 | 59 | /* Begin PBXFrameworksBuildPhase section */ 60 | 6552DB8E1B1C830A00AE50F8 /* Frameworks */ = { 61 | isa = PBXFrameworksBuildPhase; 62 | buildActionMask = 2147483647; 63 | files = ( 64 | ); 65 | runOnlyForDeploymentPostprocessing = 0; 66 | }; 67 | 6552DBA31B1C830A00AE50F8 /* Frameworks */ = { 68 | isa = PBXFrameworksBuildPhase; 69 | buildActionMask = 2147483647; 70 | files = ( 71 | ); 72 | runOnlyForDeploymentPostprocessing = 0; 73 | }; 74 | /* End PBXFrameworksBuildPhase section */ 75 | 76 | /* Begin PBXGroup section */ 77 | 651A96351B1DA95800D87D14 /* images */ = { 78 | isa = PBXGroup; 79 | children = ( 80 | 651A96361B1DA95800D87D14 /* github_1.jpeg */, 81 | 651A96371B1DA95800D87D14 /* github_2.jpeg */, 82 | 651A96381B1DA95800D87D14 /* github_3.jpeg */, 83 | 651A96391B1DA95800D87D14 /* github_4.jpeg */, 84 | 651A963A1B1DA95800D87D14 /* github_5.jpeg */, 85 | 651A963B1B1DA95800D87D14 /* 头像.jpg */, 86 | ); 87 | name = images; 88 | path = "QQ头像"; 89 | sourceTree = ""; 90 | }; 91 | 6552DB881B1C830A00AE50F8 = { 92 | isa = PBXGroup; 93 | children = ( 94 | 6552DB931B1C830A00AE50F8 /* PhotoStackViewTest */, 95 | 6552DBA91B1C830A00AE50F8 /* PhotoStackViewTestTests */, 96 | 6552DB921B1C830A00AE50F8 /* Products */, 97 | ); 98 | sourceTree = ""; 99 | }; 100 | 6552DB921B1C830A00AE50F8 /* Products */ = { 101 | isa = PBXGroup; 102 | children = ( 103 | 6552DB911B1C830A00AE50F8 /* PhotoStackViewTest.app */, 104 | 6552DBA61B1C830A00AE50F8 /* PhotoStackViewTestTests.xctest */, 105 | ); 106 | name = Products; 107 | sourceTree = ""; 108 | }; 109 | 6552DB931B1C830A00AE50F8 /* PhotoStackViewTest */ = { 110 | isa = PBXGroup; 111 | children = ( 112 | 6552DB961B1C830A00AE50F8 /* AppDelegate.swift */, 113 | 6552DB981B1C830A00AE50F8 /* ViewController.swift */, 114 | 6552DBBC1B1C835100AE50F8 /* PhotoStackView */, 115 | 6552DB9A1B1C830A00AE50F8 /* Main.storyboard */, 116 | 651A96351B1DA95800D87D14 /* images */, 117 | 6552DB9D1B1C830A00AE50F8 /* Images.xcassets */, 118 | 6552DB9F1B1C830A00AE50F8 /* LaunchScreen.xib */, 119 | 6552DB941B1C830A00AE50F8 /* Supporting Files */, 120 | ); 121 | path = PhotoStackViewTest; 122 | sourceTree = ""; 123 | }; 124 | 6552DB941B1C830A00AE50F8 /* Supporting Files */ = { 125 | isa = PBXGroup; 126 | children = ( 127 | 6552DB951B1C830A00AE50F8 /* Info.plist */, 128 | ); 129 | name = "Supporting Files"; 130 | sourceTree = ""; 131 | }; 132 | 6552DBA91B1C830A00AE50F8 /* PhotoStackViewTestTests */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | 6552DBAC1B1C830A00AE50F8 /* PhotoStackViewTestTests.swift */, 136 | 6552DBAA1B1C830A00AE50F8 /* Supporting Files */, 137 | ); 138 | path = PhotoStackViewTestTests; 139 | sourceTree = ""; 140 | }; 141 | 6552DBAA1B1C830A00AE50F8 /* Supporting Files */ = { 142 | isa = PBXGroup; 143 | children = ( 144 | 6552DBAB1B1C830A00AE50F8 /* Info.plist */, 145 | ); 146 | name = "Supporting Files"; 147 | sourceTree = ""; 148 | }; 149 | 6552DBBC1B1C835100AE50F8 /* PhotoStackView */ = { 150 | isa = PBXGroup; 151 | children = ( 152 | 6552DBB61B1C831900AE50F8 /* PhotoStackView.swift */, 153 | 6552DBB81B1C834D00AE50F8 /* PhotoBorder.png */, 154 | 6552DBB91B1C834D00AE50F8 /* PhotoBorder@2x.png */, 155 | ); 156 | name = PhotoStackView; 157 | sourceTree = ""; 158 | }; 159 | /* End PBXGroup section */ 160 | 161 | /* Begin PBXNativeTarget section */ 162 | 6552DB901B1C830A00AE50F8 /* PhotoStackViewTest */ = { 163 | isa = PBXNativeTarget; 164 | buildConfigurationList = 6552DBB01B1C830A00AE50F8 /* Build configuration list for PBXNativeTarget "PhotoStackViewTest" */; 165 | buildPhases = ( 166 | 6552DB8D1B1C830A00AE50F8 /* Sources */, 167 | 6552DB8E1B1C830A00AE50F8 /* Frameworks */, 168 | 6552DB8F1B1C830A00AE50F8 /* Resources */, 169 | ); 170 | buildRules = ( 171 | ); 172 | dependencies = ( 173 | ); 174 | name = PhotoStackViewTest; 175 | productName = PhotoStackViewTest; 176 | productReference = 6552DB911B1C830A00AE50F8 /* PhotoStackViewTest.app */; 177 | productType = "com.apple.product-type.application"; 178 | }; 179 | 6552DBA51B1C830A00AE50F8 /* PhotoStackViewTestTests */ = { 180 | isa = PBXNativeTarget; 181 | buildConfigurationList = 6552DBB31B1C830A00AE50F8 /* Build configuration list for PBXNativeTarget "PhotoStackViewTestTests" */; 182 | buildPhases = ( 183 | 6552DBA21B1C830A00AE50F8 /* Sources */, 184 | 6552DBA31B1C830A00AE50F8 /* Frameworks */, 185 | 6552DBA41B1C830A00AE50F8 /* Resources */, 186 | ); 187 | buildRules = ( 188 | ); 189 | dependencies = ( 190 | 6552DBA81B1C830A00AE50F8 /* PBXTargetDependency */, 191 | ); 192 | name = PhotoStackViewTestTests; 193 | productName = PhotoStackViewTestTests; 194 | productReference = 6552DBA61B1C830A00AE50F8 /* PhotoStackViewTestTests.xctest */; 195 | productType = "com.apple.product-type.bundle.unit-test"; 196 | }; 197 | /* End PBXNativeTarget section */ 198 | 199 | /* Begin PBXProject section */ 200 | 6552DB891B1C830A00AE50F8 /* Project object */ = { 201 | isa = PBXProject; 202 | attributes = { 203 | LastUpgradeCheck = 0630; 204 | ORGANIZATIONNAME = "Chen Yizhuo"; 205 | TargetAttributes = { 206 | 6552DB901B1C830A00AE50F8 = { 207 | CreatedOnToolsVersion = 6.3.1; 208 | }; 209 | 6552DBA51B1C830A00AE50F8 = { 210 | CreatedOnToolsVersion = 6.3.1; 211 | TestTargetID = 6552DB901B1C830A00AE50F8; 212 | }; 213 | }; 214 | }; 215 | buildConfigurationList = 6552DB8C1B1C830A00AE50F8 /* Build configuration list for PBXProject "PhotoStackViewTest" */; 216 | compatibilityVersion = "Xcode 3.2"; 217 | developmentRegion = English; 218 | hasScannedForEncodings = 0; 219 | knownRegions = ( 220 | en, 221 | Base, 222 | ); 223 | mainGroup = 6552DB881B1C830A00AE50F8; 224 | productRefGroup = 6552DB921B1C830A00AE50F8 /* Products */; 225 | projectDirPath = ""; 226 | projectRoot = ""; 227 | targets = ( 228 | 6552DB901B1C830A00AE50F8 /* PhotoStackViewTest */, 229 | 6552DBA51B1C830A00AE50F8 /* PhotoStackViewTestTests */, 230 | ); 231 | }; 232 | /* End PBXProject section */ 233 | 234 | /* Begin PBXResourcesBuildPhase section */ 235 | 6552DB8F1B1C830A00AE50F8 /* Resources */ = { 236 | isa = PBXResourcesBuildPhase; 237 | buildActionMask = 2147483647; 238 | files = ( 239 | 6552DB9C1B1C830A00AE50F8 /* Main.storyboard in Resources */, 240 | 6552DBA11B1C830A00AE50F8 /* LaunchScreen.xib in Resources */, 241 | 651A963F1B1DA95800D87D14 /* github_4.jpeg in Resources */, 242 | 651A963C1B1DA95800D87D14 /* github_1.jpeg in Resources */, 243 | 6552DBBA1B1C834D00AE50F8 /* PhotoBorder.png in Resources */, 244 | 6552DB9E1B1C830A00AE50F8 /* Images.xcassets in Resources */, 245 | 651A96411B1DA95800D87D14 /* 头像.jpg in Resources */, 246 | 6552DBBB1B1C834D00AE50F8 /* PhotoBorder@2x.png in Resources */, 247 | 651A963E1B1DA95800D87D14 /* github_3.jpeg in Resources */, 248 | 651A96401B1DA95800D87D14 /* github_5.jpeg in Resources */, 249 | 651A963D1B1DA95800D87D14 /* github_2.jpeg in Resources */, 250 | ); 251 | runOnlyForDeploymentPostprocessing = 0; 252 | }; 253 | 6552DBA41B1C830A00AE50F8 /* Resources */ = { 254 | isa = PBXResourcesBuildPhase; 255 | buildActionMask = 2147483647; 256 | files = ( 257 | ); 258 | runOnlyForDeploymentPostprocessing = 0; 259 | }; 260 | /* End PBXResourcesBuildPhase section */ 261 | 262 | /* Begin PBXSourcesBuildPhase section */ 263 | 6552DB8D1B1C830A00AE50F8 /* Sources */ = { 264 | isa = PBXSourcesBuildPhase; 265 | buildActionMask = 2147483647; 266 | files = ( 267 | 6552DBB71B1C831900AE50F8 /* PhotoStackView.swift in Sources */, 268 | 6552DB991B1C830A00AE50F8 /* ViewController.swift in Sources */, 269 | 6552DB971B1C830A00AE50F8 /* AppDelegate.swift in Sources */, 270 | ); 271 | runOnlyForDeploymentPostprocessing = 0; 272 | }; 273 | 6552DBA21B1C830A00AE50F8 /* Sources */ = { 274 | isa = PBXSourcesBuildPhase; 275 | buildActionMask = 2147483647; 276 | files = ( 277 | 6552DBAD1B1C830A00AE50F8 /* PhotoStackViewTestTests.swift in Sources */, 278 | ); 279 | runOnlyForDeploymentPostprocessing = 0; 280 | }; 281 | /* End PBXSourcesBuildPhase section */ 282 | 283 | /* Begin PBXTargetDependency section */ 284 | 6552DBA81B1C830A00AE50F8 /* PBXTargetDependency */ = { 285 | isa = PBXTargetDependency; 286 | target = 6552DB901B1C830A00AE50F8 /* PhotoStackViewTest */; 287 | targetProxy = 6552DBA71B1C830A00AE50F8 /* PBXContainerItemProxy */; 288 | }; 289 | /* End PBXTargetDependency section */ 290 | 291 | /* Begin PBXVariantGroup section */ 292 | 6552DB9A1B1C830A00AE50F8 /* Main.storyboard */ = { 293 | isa = PBXVariantGroup; 294 | children = ( 295 | 6552DB9B1B1C830A00AE50F8 /* Base */, 296 | ); 297 | name = Main.storyboard; 298 | sourceTree = ""; 299 | }; 300 | 6552DB9F1B1C830A00AE50F8 /* LaunchScreen.xib */ = { 301 | isa = PBXVariantGroup; 302 | children = ( 303 | 6552DBA01B1C830A00AE50F8 /* Base */, 304 | ); 305 | name = LaunchScreen.xib; 306 | sourceTree = ""; 307 | }; 308 | /* End PBXVariantGroup section */ 309 | 310 | /* Begin XCBuildConfiguration section */ 311 | 6552DBAE1B1C830A00AE50F8 /* Debug */ = { 312 | isa = XCBuildConfiguration; 313 | buildSettings = { 314 | ALWAYS_SEARCH_USER_PATHS = NO; 315 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 316 | CLANG_CXX_LIBRARY = "libc++"; 317 | CLANG_ENABLE_MODULES = YES; 318 | CLANG_ENABLE_OBJC_ARC = YES; 319 | CLANG_WARN_BOOL_CONVERSION = YES; 320 | CLANG_WARN_CONSTANT_CONVERSION = YES; 321 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 322 | CLANG_WARN_EMPTY_BODY = YES; 323 | CLANG_WARN_ENUM_CONVERSION = YES; 324 | CLANG_WARN_INT_CONVERSION = YES; 325 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 326 | CLANG_WARN_UNREACHABLE_CODE = YES; 327 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 328 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 329 | COPY_PHASE_STRIP = NO; 330 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 331 | ENABLE_STRICT_OBJC_MSGSEND = YES; 332 | GCC_C_LANGUAGE_STANDARD = gnu99; 333 | GCC_DYNAMIC_NO_PIC = NO; 334 | GCC_NO_COMMON_BLOCKS = YES; 335 | GCC_OPTIMIZATION_LEVEL = 0; 336 | GCC_PREPROCESSOR_DEFINITIONS = ( 337 | "DEBUG=1", 338 | "$(inherited)", 339 | ); 340 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 341 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 342 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 343 | GCC_WARN_UNDECLARED_SELECTOR = YES; 344 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 345 | GCC_WARN_UNUSED_FUNCTION = YES; 346 | GCC_WARN_UNUSED_VARIABLE = YES; 347 | IPHONEOS_DEPLOYMENT_TARGET = 8.3; 348 | MTL_ENABLE_DEBUG_INFO = YES; 349 | ONLY_ACTIVE_ARCH = YES; 350 | SDKROOT = iphoneos; 351 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 352 | }; 353 | name = Debug; 354 | }; 355 | 6552DBAF1B1C830A00AE50F8 /* Release */ = { 356 | isa = XCBuildConfiguration; 357 | buildSettings = { 358 | ALWAYS_SEARCH_USER_PATHS = NO; 359 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 360 | CLANG_CXX_LIBRARY = "libc++"; 361 | CLANG_ENABLE_MODULES = YES; 362 | CLANG_ENABLE_OBJC_ARC = YES; 363 | CLANG_WARN_BOOL_CONVERSION = YES; 364 | CLANG_WARN_CONSTANT_CONVERSION = YES; 365 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 366 | CLANG_WARN_EMPTY_BODY = YES; 367 | CLANG_WARN_ENUM_CONVERSION = YES; 368 | CLANG_WARN_INT_CONVERSION = YES; 369 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 370 | CLANG_WARN_UNREACHABLE_CODE = YES; 371 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 372 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 373 | COPY_PHASE_STRIP = NO; 374 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 375 | ENABLE_NS_ASSERTIONS = NO; 376 | ENABLE_STRICT_OBJC_MSGSEND = YES; 377 | GCC_C_LANGUAGE_STANDARD = gnu99; 378 | GCC_NO_COMMON_BLOCKS = YES; 379 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 380 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 381 | GCC_WARN_UNDECLARED_SELECTOR = YES; 382 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 383 | GCC_WARN_UNUSED_FUNCTION = YES; 384 | GCC_WARN_UNUSED_VARIABLE = YES; 385 | IPHONEOS_DEPLOYMENT_TARGET = 8.3; 386 | MTL_ENABLE_DEBUG_INFO = NO; 387 | SDKROOT = iphoneos; 388 | VALIDATE_PRODUCT = YES; 389 | }; 390 | name = Release; 391 | }; 392 | 6552DBB11B1C830A00AE50F8 /* Debug */ = { 393 | isa = XCBuildConfiguration; 394 | buildSettings = { 395 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 396 | INFOPLIST_FILE = PhotoStackViewTest/Info.plist; 397 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 398 | PRODUCT_NAME = "$(TARGET_NAME)"; 399 | }; 400 | name = Debug; 401 | }; 402 | 6552DBB21B1C830A00AE50F8 /* Release */ = { 403 | isa = XCBuildConfiguration; 404 | buildSettings = { 405 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 406 | INFOPLIST_FILE = PhotoStackViewTest/Info.plist; 407 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 408 | PRODUCT_NAME = "$(TARGET_NAME)"; 409 | }; 410 | name = Release; 411 | }; 412 | 6552DBB41B1C830A00AE50F8 /* Debug */ = { 413 | isa = XCBuildConfiguration; 414 | buildSettings = { 415 | BUNDLE_LOADER = "$(TEST_HOST)"; 416 | FRAMEWORK_SEARCH_PATHS = ( 417 | "$(SDKROOT)/Developer/Library/Frameworks", 418 | "$(inherited)", 419 | ); 420 | GCC_PREPROCESSOR_DEFINITIONS = ( 421 | "DEBUG=1", 422 | "$(inherited)", 423 | ); 424 | INFOPLIST_FILE = PhotoStackViewTestTests/Info.plist; 425 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 426 | PRODUCT_NAME = "$(TARGET_NAME)"; 427 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PhotoStackViewTest.app/PhotoStackViewTest"; 428 | }; 429 | name = Debug; 430 | }; 431 | 6552DBB51B1C830A00AE50F8 /* Release */ = { 432 | isa = XCBuildConfiguration; 433 | buildSettings = { 434 | BUNDLE_LOADER = "$(TEST_HOST)"; 435 | FRAMEWORK_SEARCH_PATHS = ( 436 | "$(SDKROOT)/Developer/Library/Frameworks", 437 | "$(inherited)", 438 | ); 439 | INFOPLIST_FILE = PhotoStackViewTestTests/Info.plist; 440 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 441 | PRODUCT_NAME = "$(TARGET_NAME)"; 442 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PhotoStackViewTest.app/PhotoStackViewTest"; 443 | }; 444 | name = Release; 445 | }; 446 | /* End XCBuildConfiguration section */ 447 | 448 | /* Begin XCConfigurationList section */ 449 | 6552DB8C1B1C830A00AE50F8 /* Build configuration list for PBXProject "PhotoStackViewTest" */ = { 450 | isa = XCConfigurationList; 451 | buildConfigurations = ( 452 | 6552DBAE1B1C830A00AE50F8 /* Debug */, 453 | 6552DBAF1B1C830A00AE50F8 /* Release */, 454 | ); 455 | defaultConfigurationIsVisible = 0; 456 | defaultConfigurationName = Release; 457 | }; 458 | 6552DBB01B1C830A00AE50F8 /* Build configuration list for PBXNativeTarget "PhotoStackViewTest" */ = { 459 | isa = XCConfigurationList; 460 | buildConfigurations = ( 461 | 6552DBB11B1C830A00AE50F8 /* Debug */, 462 | 6552DBB21B1C830A00AE50F8 /* Release */, 463 | ); 464 | defaultConfigurationIsVisible = 0; 465 | defaultConfigurationName = Release; 466 | }; 467 | 6552DBB31B1C830A00AE50F8 /* Build configuration list for PBXNativeTarget "PhotoStackViewTestTests" */ = { 468 | isa = XCConfigurationList; 469 | buildConfigurations = ( 470 | 6552DBB41B1C830A00AE50F8 /* Debug */, 471 | 6552DBB51B1C830A00AE50F8 /* Release */, 472 | ); 473 | defaultConfigurationIsVisible = 0; 474 | defaultConfigurationName = Release; 475 | }; 476 | /* End XCConfigurationList section */ 477 | }; 478 | rootObject = 6552DB891B1C830A00AE50F8 /* Project object */; 479 | } 480 | -------------------------------------------------------------------------------- /PhotoStackViewTest.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /PhotoStackViewTest.xcodeproj/project.xcworkspace/xcuserdata/cyz.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ijoyc/PhotoStackView-Swift/4cc29b53b0645b04b78c03d1ee6bece8f126b0c8/PhotoStackViewTest.xcodeproj/project.xcworkspace/xcuserdata/cyz.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /PhotoStackViewTest.xcodeproj/xcuserdata/cyz.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /PhotoStackViewTest.xcodeproj/xcuserdata/cyz.xcuserdatad/xcschemes/PhotoStackViewTest.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 | -------------------------------------------------------------------------------- /PhotoStackViewTest.xcodeproj/xcuserdata/cyz.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | PhotoStackViewTest.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 6552DB901B1C830A00AE50F8 16 | 17 | primary 18 | 19 | 20 | 6552DBA51B1C830A00AE50F8 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /PhotoStackViewTest/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // PhotoStackViewTest 4 | // 5 | // Created by Chen Yizhuo on 15/6/1. 6 | // Copyright (c) 2015年 Chen Yizhuo. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(application: UIApplication) { 33 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /PhotoStackViewTest/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /PhotoStackViewTest/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /PhotoStackViewTest/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 | } -------------------------------------------------------------------------------- /PhotoStackViewTest/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | com.chenyizhuo.$(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 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /PhotoStackViewTest/PhotoBorder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ijoyc/PhotoStackView-Swift/4cc29b53b0645b04b78c03d1ee6bece8f126b0c8/PhotoStackViewTest/PhotoBorder.png -------------------------------------------------------------------------------- /PhotoStackViewTest/PhotoBorder@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ijoyc/PhotoStackView-Swift/4cc29b53b0645b04b78c03d1ee6bece8f126b0c8/PhotoStackViewTest/PhotoBorder@2x.png -------------------------------------------------------------------------------- /PhotoStackViewTest/PhotoStackView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoStackView.swift 3 | // PhotoStackViewTest 4 | // 5 | // Created by Chen Yizhuo on 15/6/1. 6 | // Copyright (c) 2015年 Chen Yizhuo. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @objc protocol PhotoStackViewDataSource: NSObjectProtocol { 12 | func numberOfPhotosInStackView(stackView: PhotoStackView) -> Int 13 | func stackView(stackView: PhotoStackView, imageAtIndex index: Int) -> UIImage 14 | 15 | optional func stackView(stackView: PhotoStackView, sizeOfPhotoAtIndex index: Int) -> CGSize 16 | } 17 | 18 | @objc protocol PhotoStackViewDelegate: NSObjectProtocol { 19 | optional func stackView(stackView: PhotoStackView, didSelectPhotoAtIndex index: Int) 20 | optional func stackView(stackView: PhotoStackView, willBeginDraggingPhotoAtIndex index: Int) 21 | optional func stackView(stackView: PhotoStackView, didRevealPhotoAtIndex index: Int) 22 | optional func stackView(stackView: PhotoStackView, willFlickAwayPhotoFromIndex from: Int, toIndex to: Int) 23 | } 24 | 25 | enum PhotoStackViewFlipDirection: Int { 26 | case Left = 1, Right, Up, Down 27 | } 28 | 29 | /** 30 | * Supporting events: TouchDown, TouchCancel, TouchDragInside, TouchUpInside 31 | */ 32 | class PhotoStackView: UIControl { 33 | 34 | //MARK: computed property 35 | 36 | var s_borderImage: UIImage? 37 | /// the border image of every photo 38 | var borderImage: UIImage? { 39 | set { 40 | if s_borderImage == newValue { 41 | return 42 | } 43 | 44 | s_borderImage = newValue 45 | reloadData() 46 | } 47 | get { 48 | if s_borderImage == nil { 49 | return UIImage(named: "PhotoBorder.png") 50 | } else { 51 | return s_borderImage 52 | } 53 | } 54 | } 55 | 56 | var s_borderWidth: CGFloat = 0.0 57 | /// the width of border image view, default is 5.0 58 | var borderWidth: CGFloat { 59 | set { 60 | if s_borderWidth == newValue { 61 | return 62 | } 63 | 64 | s_borderWidth = newValue 65 | reloadData() 66 | } 67 | get { 68 | if showBorder { 69 | return s_borderWidth 70 | } else { 71 | return 0 72 | } 73 | } 74 | } 75 | 76 | var s_showBorder = false 77 | /// decide whether show border image view or not, default is true 78 | var showBorder: Bool { 79 | set { 80 | if s_showBorder == newValue { 81 | return 82 | } 83 | 84 | s_showBorder = newValue 85 | reloadData() 86 | } 87 | get { 88 | return s_showBorder 89 | } 90 | } 91 | 92 | var s_rotationOffset: CGFloat = 0.0 93 | /// the scope of offset of rotation on every photo except the first one. default is 4.0. 94 | /// ie, 4.0 means rotate iamge with degree between (-4.0, 4.0) 95 | var rotationOffset: CGFloat { 96 | set { 97 | if s_rotationOffset == newValue { 98 | return 99 | } 100 | 101 | s_rotationOffset = newValue 102 | reloadData() 103 | } 104 | get { 105 | return s_rotationOffset 106 | } 107 | } 108 | 109 | var s_highlightColor: UIColor? 110 | var highlightColor: UIColor? { 111 | set { 112 | s_highlightColor = newValue 113 | } 114 | get { 115 | if s_highlightColor == nil { 116 | return UIColor(red: 0, green: 0, blue: 0, alpha: 0.15) 117 | } else { 118 | return s_highlightColor 119 | } 120 | } 121 | } 122 | 123 | var delegate: PhotoStackViewDelegate? 124 | 125 | var s_dataSource: PhotoStackViewDataSource? 126 | var dataSource: PhotoStackViewDataSource? { 127 | set { 128 | s_dataSource = newValue 129 | reloadData() 130 | } 131 | get { 132 | return s_dataSource 133 | } 134 | } 135 | 136 | var s_photoImages: [UIView]? 137 | var photoImages: [UIView]? { 138 | set { 139 | //remove all subview and prepare to re-add all images from data source 140 | for view in subviews { 141 | view.removeFromSuperview() 142 | } 143 | 144 | if let images = newValue { 145 | for view in images { 146 | //keep the original transfrom for the existing images 147 | if let index = find(images, view), count = s_photoImages?.count where index < count { 148 | let existingView = s_photoImages![index] 149 | view.transform = existingView.transform 150 | } else { 151 | makeCrooked(view, animated: false) 152 | } 153 | 154 | insertSubview(view, atIndex: 0) 155 | } 156 | } 157 | 158 | s_photoImages = newValue 159 | } 160 | get { 161 | return s_photoImages 162 | } 163 | } 164 | 165 | override var highlighted: Bool { 166 | didSet { 167 | let photo = self.topPhoto()?.subviews.last as! UIImageView 168 | if highlighted { 169 | let view = UIView(frame: self.bounds) 170 | view.backgroundColor = self.highlightColor 171 | photo.addSubview(view) 172 | photo.bringSubviewToFront(view) 173 | } else { 174 | photo.subviews.last?.removeFromSuperview() 175 | } 176 | } 177 | } 178 | 179 | override var bounds: CGRect { 180 | didSet { 181 | if CGRectEqualToRect(oldValue, self.bounds) { 182 | return 183 | } 184 | 185 | reloadData() 186 | } 187 | } 188 | 189 | override var frame: CGRect { 190 | didSet { 191 | if CGRectEqualToRect(oldValue, self.frame) { 192 | return 193 | } 194 | 195 | reloadData() 196 | } 197 | } 198 | 199 | //MARK: Set up 200 | 201 | override init(frame: CGRect) { 202 | super.init(frame: frame) 203 | setup() 204 | } 205 | 206 | required init(coder aDecoder: NSCoder) { 207 | super.init(coder: aDecoder) 208 | setup() 209 | } 210 | 211 | func setup() { 212 | //default value 213 | borderWidth = 5.0 214 | showBorder = true 215 | rotationOffset = 4.0 216 | 217 | let panGR = UIPanGestureRecognizer(target: self, action: Selector("handlePan:")) 218 | addGestureRecognizer(panGR) 219 | 220 | let tapGR = UITapGestureRecognizer(target: self, action: Selector("handleTap:")) 221 | addGestureRecognizer(tapGR) 222 | 223 | reloadData() 224 | } 225 | 226 | override func sendActionsForControlEvents(controlEvents: UIControlEvents) { 227 | super.sendActionsForControlEvents(controlEvents) 228 | highlighted = (controlEvents == .TouchDown) 229 | } 230 | 231 | //MARK: Public Methods 232 | 233 | /** 234 | use this method to reload photo stack view when data has changed 235 | */ 236 | func reloadData() { 237 | if dataSource == nil { 238 | photoImages = nil 239 | return 240 | } 241 | 242 | if let number = dataSource?.numberOfPhotosInStackView(self) { 243 | var images = [UIView]() 244 | let border = borderImage?.resizableImageWithCapInsets(UIEdgeInsets(top: borderWidth, left: borderWidth, bottom: borderWidth, right: borderWidth)) 245 | let topIndex = indexOfTopPhoto() 246 | 247 | for i in 0.. Int { 300 | if let images = photoImages, let photo = topPhoto() { 301 | if let index = find(images, photo) { 302 | return index 303 | } 304 | } 305 | return 0 306 | } 307 | 308 | /** 309 | automatically flip to next photo with a given direction 310 | 311 | :param: direction the direction of flipping 312 | */ 313 | func flipToNextPhotoWithDirection(direction: PhotoStackViewFlipDirection) { 314 | var xVelocity: CGFloat = 0 315 | var yVelocity: CGFloat = 0 316 | switch direction { 317 | case .Up: 318 | yVelocity = 400 319 | case .Down: 320 | yVelocity = -400 321 | case .Left: 322 | xVelocity = 400 323 | case .Right: 324 | xVelocity = -400 325 | } 326 | 327 | if let photo = topPhoto() { 328 | flickAway(photo, withVelocity: CGPoint(x: xVelocity, y: yVelocity)) 329 | } 330 | } 331 | 332 | /** 333 | get the top photo on photo stack 334 | 335 | :returns: current first photo 336 | */ 337 | func topPhoto() -> UIView? { 338 | if subviews.count == 0 { 339 | return nil 340 | } 341 | return subviews[subviews.count - 1] as? UIView 342 | } 343 | 344 | /** 345 | jump to photo at index 346 | */ 347 | func goToImageAtIndex(index: Int) { 348 | if let photos = photoImages { 349 | for view in photos { 350 | if let idx = find(photos, view) where idx < index { 351 | sendSubviewToBack(view) 352 | } 353 | } 354 | } 355 | makeStraight(topPhoto()!, animated: false) 356 | } 357 | 358 | func showAllPhotos() { 359 | let screenBounds = UIScreen.mainScreen().bounds 360 | let maskView = UIView(frame: screenBounds) 361 | maskView.backgroundColor = UIColor.blackColor() 362 | maskView.alpha = 0 363 | UIApplication.sharedApplication().keyWindow?.addSubview(maskView) 364 | 365 | UIView.animateWithDuration(0.1, delay: 0.0, options: nil, animations: { () -> Void in 366 | maskView.alpha = 1.0 367 | }) { (_) -> Void in 368 | 369 | } 370 | 371 | 372 | let column = 3 373 | let imageWidth = 80 374 | let padding = (Int(screenBounds.width) - column * imageWidth) / (column + 1) 375 | 376 | if let photos = photoImages { 377 | for view in photos { 378 | 379 | //set the initial location 380 | view.removeFromSuperview() 381 | maskView.addSubview(view) 382 | view.frame = frame 383 | 384 | if let index = find(photos, view) { 385 | UIView.animateWithDuration(0.1, delay: NSTimeInterval(Double(index) * 0.1), options: nil, animations: { () -> Void in 386 | view.frame = CGRect(x: padding + (index % column) * (imageWidth + padding), y: padding + (index / column) * (padding + imageWidth), width: imageWidth, height: imageWidth) 387 | }, completion: { (finished) -> Void in 388 | 389 | }) 390 | } 391 | } 392 | } 393 | 394 | let tapGR = UITapGestureRecognizer(target: self, action: Selector("removeMaskView:")) 395 | maskView.addGestureRecognizer(tapGR) 396 | 397 | } 398 | 399 | //MARK: Animations 400 | 401 | func returnToCenter(view: UIView) { 402 | UIView.animateWithDuration(0.2, animations: { () -> Void in 403 | view.center = CGPoint(x: CGRectGetMidX(self.bounds), y: CGRectGetMidY(self.bounds)) 404 | }) 405 | } 406 | 407 | func flickAway(view: UIView, withVelocity velocity: CGPoint) { 408 | if let del = delegate where del.respondsToSelector(Selector("stackView:willFlickAwayPhotoFromIndex:toIndex:")) { 409 | let from = indexOfTopPhoto() 410 | var to = from + 1 411 | if let number = dataSource?.numberOfPhotosInStackView(self) where to >= number { 412 | to = 0 413 | } 414 | 415 | del.stackView!(self, willFlickAwayPhotoFromIndex: from, toIndex: to) 416 | } 417 | 418 | let width = CGRectGetWidth(bounds) 419 | let height = CGRectGetHeight(bounds) 420 | 421 | var xPosition: CGFloat = CGRectGetMidX(bounds) 422 | var yPosition: CGFloat = CGRectGetMidY(bounds) 423 | 424 | if velocity.x > 0 { 425 | xPosition = CGRectGetMidX(bounds) + width 426 | } else if velocity.x < 0 { 427 | xPosition = CGRectGetMidX(bounds) - width 428 | } 429 | if velocity.y > 0 { 430 | yPosition = CGRectGetMidY(bounds) + height 431 | } else if velocity.y < 0 { 432 | yPosition = CGRectGetMidY(bounds) - height 433 | } 434 | 435 | UIView.animateWithDuration(0.1, animations: { () -> Void in 436 | view.center = CGPoint(x: xPosition, y: yPosition) 437 | }) { (finished) -> Void in 438 | self.makeCrooked(view, animated: true) 439 | self.sendSubviewToBack(view) 440 | self.makeStraight(self.topPhoto()!, animated: true) 441 | self .returnToCenter(view) 442 | 443 | if let del = self.delegate where del.respondsToSelector("stackView:didRevealPhotoAtIndex:") { 444 | del.stackView!(self, didRevealPhotoAtIndex:self.indexOfTopPhoto()) 445 | } 446 | } 447 | } 448 | 449 | func rotate(degree: Int, onView view: UIView, animated: Bool) { 450 | let radian = CGFloat(degree) * CGFloat(M_PI) / 180 451 | 452 | if animated { 453 | UIView.animateWithDuration(0.2, animations: { () -> Void in 454 | view.transform = CGAffineTransformMakeRotation(radian) 455 | }) 456 | } else { 457 | view.transform = CGAffineTransformMakeRotation(radian) 458 | } 459 | } 460 | 461 | func makeCrooked(view: UIView, animated: Bool) { 462 | let min = Int(-rotationOffset) 463 | let max = Int(rotationOffset) 464 | 465 | let scope = UInt32(max - min - 1) 466 | let randomDegree = Int(arc4random_uniform(scope)) 467 | let degree: Int = min + randomDegree 468 | 469 | rotate(degree, onView: view, animated: animated) 470 | } 471 | 472 | func makeStraight(view: UIView, animated: Bool) { 473 | rotate(0, onView: view, animated: animated) 474 | } 475 | 476 | //MARK: Gesture Recognizer 477 | 478 | func handlePan(recognizer: UIPanGestureRecognizer) { 479 | if let topPhoto = self.topPhoto() { 480 | let velocity = recognizer.velocityInView(recognizer.view) 481 | let translation = recognizer.translationInView(recognizer.view!) 482 | 483 | if recognizer.state == .Began { 484 | sendActionsForControlEvents(.TouchCancel) 485 | 486 | if let del = delegate where del.respondsToSelector(Selector("stackView:willBeginDraggingPhotoAtIndex")) { 487 | del.stackView!(self, willBeginDraggingPhotoAtIndex: self.indexOfTopPhoto()) 488 | } 489 | } else if recognizer.state == .Changed { 490 | topPhoto.center = CGPoint(x: topPhoto.center.x + translation.x, y: topPhoto.center.y + translation.y) 491 | recognizer.setTranslation(CGPoint.zeroPoint, inView: recognizer.view) 492 | } else if recognizer.state == .Ended || recognizer.state == .Cancelled { 493 | if abs(velocity.x) > 200 { 494 | flickAway(topPhoto, withVelocity: velocity) 495 | } else { 496 | returnToCenter(topPhoto) 497 | } 498 | } 499 | 500 | } 501 | 502 | } 503 | 504 | func handleTap(recognizer: UIGestureRecognizer) { 505 | sendActionsForControlEvents(.TouchUpInside) 506 | 507 | if let del = delegate where del.respondsToSelector(Selector("stackView:didSelectPhotoAtIndex:")) { 508 | del.stackView!(self, didSelectPhotoAtIndex: self.indexOfTopPhoto()) 509 | } 510 | } 511 | 512 | func removeMaskView(recognizer: UITapGestureRecognizer) { 513 | let maskView = recognizer.view! 514 | for i in stride(from: maskView.subviews.count - 1, through: 0, by: -1) { 515 | let photo = maskView.subviews[i] as? UIView 516 | UIView.animateWithDuration(0.25, animations: { () -> Void in 517 | photo?.frame = self.frame 518 | }, completion: { (_) -> Void in 519 | 520 | }) 521 | } 522 | 523 | UIView.animateWithDuration(0.25, animations: { () -> Void in 524 | maskView.alpha = 0.0 525 | }) { (_) -> Void in 526 | maskView.removeFromSuperview() 527 | self.reloadData() 528 | } 529 | } 530 | 531 | //MARK: Touch Methods 532 | 533 | override func touchesBegan(touches: Set, withEvent event: UIEvent) { 534 | super.touchesBegan(touches, withEvent: event) 535 | 536 | sendActionsForControlEvents(.TouchDown) 537 | } 538 | 539 | override func touchesMoved(touches: Set, withEvent event: UIEvent) { 540 | super.touchesMoved(touches, withEvent: event) 541 | 542 | sendActionsForControlEvents(.TouchDragInside) 543 | } 544 | 545 | override func touchesEnded(touches: Set, withEvent event: UIEvent) { 546 | super.touchesEnded(touches, withEvent: event) 547 | 548 | sendActionsForControlEvents(.TouchCancel) 549 | } 550 | } 551 | 552 | -------------------------------------------------------------------------------- /PhotoStackViewTest/QQ头像/github_1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ijoyc/PhotoStackView-Swift/4cc29b53b0645b04b78c03d1ee6bece8f126b0c8/PhotoStackViewTest/QQ头像/github_1.jpeg -------------------------------------------------------------------------------- /PhotoStackViewTest/QQ头像/github_2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ijoyc/PhotoStackView-Swift/4cc29b53b0645b04b78c03d1ee6bece8f126b0c8/PhotoStackViewTest/QQ头像/github_2.jpeg -------------------------------------------------------------------------------- /PhotoStackViewTest/QQ头像/github_3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ijoyc/PhotoStackView-Swift/4cc29b53b0645b04b78c03d1ee6bece8f126b0c8/PhotoStackViewTest/QQ头像/github_3.jpeg -------------------------------------------------------------------------------- /PhotoStackViewTest/QQ头像/github_4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ijoyc/PhotoStackView-Swift/4cc29b53b0645b04b78c03d1ee6bece8f126b0c8/PhotoStackViewTest/QQ头像/github_4.jpeg -------------------------------------------------------------------------------- /PhotoStackViewTest/QQ头像/github_5.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ijoyc/PhotoStackView-Swift/4cc29b53b0645b04b78c03d1ee6bece8f126b0c8/PhotoStackViewTest/QQ头像/github_5.jpeg -------------------------------------------------------------------------------- /PhotoStackViewTest/QQ头像/头像.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ijoyc/PhotoStackView-Swift/4cc29b53b0645b04b78c03d1ee6bece8f126b0c8/PhotoStackViewTest/QQ头像/头像.jpg -------------------------------------------------------------------------------- /PhotoStackViewTest/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // PhotoStackViewTest 4 | // 5 | // Created by Chen Yizhuo on 15/6/1. 6 | // Copyright (c) 2015年 Chen Yizhuo. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController, PhotoStackViewDataSource { 12 | 13 | var photos = [UIImage(named: "github_1.jpeg"), UIImage(named: "github_2.jpeg"), UIImage(named: "github_3.jpeg")] 14 | 15 | let ps = PhotoStackView() 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | 20 | let btn: UIButton = UIButton.buttonWithType(.System) as! UIButton 21 | btn.frame = CGRect(x: 100, y: 200, width: 100, height: 40) 22 | btn.setTitle("添加图片", forState: .Normal) 23 | btn.addTarget(self, action: Selector("addImage"), forControlEvents: .TouchUpInside) 24 | view.addSubview(btn) 25 | 26 | let deleteBtn = UIButton.buttonWithType(.System) as! UIButton 27 | deleteBtn.frame = CGRect(x: 100, y: 250, width: 100, height: 40) 28 | deleteBtn.setTitle("删除图片", forState: .Normal) 29 | deleteBtn.addTarget(self, action: Selector("deleteImage"), forControlEvents: .TouchUpInside) 30 | view.addSubview(deleteBtn) 31 | 32 | let moveBtn = UIButton.buttonWithType(.System) as! UIButton 33 | moveBtn.frame = CGRect(x: 100, y: 300, width: 100, height: 40) 34 | moveBtn.setTitle("下一张", forState: .Normal) 35 | moveBtn.addTarget(self, action: Selector("nextPhoto"), forControlEvents: .TouchUpInside) 36 | view.addSubview(moveBtn) 37 | 38 | let showBtn = UIButton.buttonWithType(.System) as! UIButton 39 | showBtn.frame = CGRect(x: 100, y: 350, width: 100, height: 40) 40 | showBtn.setTitle("显示全部", forState: .Normal) 41 | showBtn.addTarget(self, action: Selector("showAll"), forControlEvents: .TouchUpInside) 42 | view.addSubview(showBtn) 43 | 44 | ps.delegate = self 45 | ps.dataSource = self 46 | ps.frame = CGRect(x: 120, y: 50, width: 80, height: 80) 47 | view.addSubview(ps) 48 | 49 | ps.addTarget(self, action: Selector("touchDown"), forControlEvents: .TouchDown) 50 | } 51 | 52 | override func didReceiveMemoryWarning() { 53 | super.didReceiveMemoryWarning() 54 | // Dispose of any resources that can be recreated. 55 | } 56 | 57 | } 58 | 59 | /** 60 | * actions 61 | */ 62 | extension ViewController { 63 | func touchDown() { 64 | println("touchDown") 65 | } 66 | 67 | func addImage() { 68 | let num = arc4random() % 5 + 1 69 | photos.append(UIImage(named: "github_\(num).jpeg")) 70 | ps.reloadData() 71 | } 72 | 73 | func deleteImage() { 74 | if photos.count == 1 { 75 | let alert = UIAlertView(title: "Tips", message: "Only one photo left. Failed to delete image", delegate: nil, cancelButtonTitle: nil, otherButtonTitles: "OK") 76 | alert.show() 77 | return 78 | } 79 | 80 | photos.removeLast() 81 | ps.reloadData() 82 | } 83 | 84 | func nextPhoto() { 85 | let random = Int(arc4random() % 4 + 1) 86 | let direction = PhotoStackViewFlipDirection(rawValue: random) 87 | ps.flipToNextPhotoWithDirection(direction!) 88 | } 89 | 90 | func showAll() { 91 | ps.showAllPhotos() 92 | } 93 | } 94 | 95 | /** 96 | * delegate & dataSource 97 | */ 98 | extension ViewController: PhotoStackViewDataSource, PhotoStackViewDelegate { 99 | func numberOfPhotosInStackView(stackView: PhotoStackView) -> Int { 100 | return photos.count 101 | } 102 | 103 | func stackView(stackView: PhotoStackView, imageAtIndex index: Int) -> UIImage { 104 | return photos[index]! 105 | } 106 | 107 | func stackView(stackView: PhotoStackView, sizeOfPhotoAtIndex index: Int) -> CGSize { 108 | return CGSizeMake(80, 80) 109 | } 110 | 111 | func stackView(stackView: PhotoStackView, willFlickAwayPhotoFromIndex from: Int, toIndex to: Int) { 112 | println("from \(from) to \(to)") 113 | } 114 | } 115 | 116 | -------------------------------------------------------------------------------- /PhotoStackViewTestTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | com.chenyizhuo.$(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 | -------------------------------------------------------------------------------- /PhotoStackViewTestTests/PhotoStackViewTestTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoStackViewTestTests.swift 3 | // PhotoStackViewTestTests 4 | // 5 | // Created by Chen Yizhuo on 15/6/1. 6 | // Copyright (c) 2015年 Chen Yizhuo. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import XCTest 11 | 12 | class PhotoStackViewTestTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | XCTAssert(true, "Pass") 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measureBlock() { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #PhotoStackView with swift 2 | ============== 3 | 4 | ###Statement 5 | 6 | I studied from an open-sourced code whose name is also "PhotoStackView". But I can't find it now. It is written by Objective-C. Here I rewrite it with swift and fix some small bugs. Also I add some function on it. 7 | 8 | ###Usage 9 | 10 | Just like UITableViewDelegate, you can init PhotoStackView and set the delegate and dataSource of it. Then you must implement the dataSource method `func numberOfPhotosInStackView(stackView: PhotoStackView) -> Int` and `func stackView(stackView: PhotoStackView, imageAtIndex index: Int) -> UIImage` to provide data for this view. Alternatively, you can implement `optional func stackView(stackView: PhotoStackView, sizeOfPhotoAtIndex index: Int) -> CGSize` to specify the size for each image. 11 | 12 | To response to some event on this control, you can either implement these optional delegate method: 13 | ```swift 14 | @objc protocol PhotoStackViewDelegate: NSObjectProtocol { 15 | optional func stackView(stackView: PhotoStackView, didSelectPhotoAtIndex index: Int) 16 | optional func stackView(stackView: PhotoStackView, willBeginDraggingPhotoAtIndex index: Int) 17 | optional func stackView(stackView: PhotoStackView, didRevealPhotoAtIndex index: Int) 18 | optional func stackView(stackView: PhotoStackView, willFlickAwayPhotoFromIndex from: Int, toIndex to: Int) 19 | } 20 | ``` 21 | or use `addTarget:action:forControlEvents:` to add action listener. All control events it support are TouchDown, TouchCancel, TouchDragInside and TouchUpInside. 22 | 23 | ###Example 24 | 25 | ```swift 26 | //declare some properties 27 | var photos = [UIImage(named: "github_1.jpeg"), UIImage(named: "github_2.jpeg"), UIImage(named: "github_3.jpeg")] 28 | 29 | let ps = PhotoStackView() 30 | 31 | //viewDidLoad() 32 | ps.delegate = self 33 | ps.dataSource = self 34 | ps.frame = CGRect(x: 120, y: 50, width: 80, height: 80) 35 | view.addSubview(ps) 36 | 37 | ps.addTarget(self, action: Selector("touchDown"), forControlEvents: .TouchDown) 38 | 39 | //touchDown action listener 40 | func touchDown() { 41 | println("touchDown") 42 | } 43 | 44 | //data source 45 | func numberOfPhotosInStackView(stackView: PhotoStackView) -> Int { 46 | return photos.count 47 | } 48 | 49 | func stackView(stackView: PhotoStackView, imageAtIndex index: Int) -> UIImage { 50 | return photos[index]! 51 | } 52 | 53 | func stackView(stackView: PhotoStackView, sizeOfPhotoAtIndex index: Int) -> CGSize { 54 | return CGSizeMake(80, 80) 55 | } 56 | 57 | //delegate 58 | func stackView(stackView: PhotoStackView, willFlickAwayPhotoFromIndex from: Int, toIndex to: Int) { 59 | println("from \(from) to \(to)") 60 | } 61 | ``` 62 | 63 | ###Images 64 | 65 | Move, add and delete. 66 | ![stackView1](http://img.blog.csdn.net/20150602220650277) 67 | 68 | Show next page 69 | ![stackView2](http://img.blog.csdn.net/20150602220720978) 70 | 71 | Show all photos 72 | ![stackView3](http://img.blog.csdn.net/20150602221021702) 73 | --------------------------------------------------------------------------------