├── .gitignore ├── Classes ├── Extension │ └── UICollectionViewControllerExtension.swift ├── SDENavigationControllerDelegate.swift ├── SDEPopPinchInteractionController.swift └── SDEPushAndPopAnimationController.swift ├── Demo ├── SDECollectionViewAlbumTransition.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata └── SDECollectionViewAlbumTransition │ ├── AppDelegate.swift │ ├── Base.lproj │ └── LaunchScreen.storyboard │ ├── Default-568h@2x.png │ ├── Info.plist │ ├── Main.storyboard │ ├── NSTimeIntervalOutputExtension.swift │ ├── PHFetchResultExtension.swift │ ├── SDEAdaptiveAssetCell.swift │ ├── SDEAlbumViewController.swift │ ├── SDEGalleriesViewController.swift │ ├── video_call.png │ └── video_call@2x.png ├── Figures ├── AlbumTransition.gif ├── Config1.png └── Config2.png └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | ######################### 2 | # .gitignore file for Xcode4 and Xcode5 Source projects 3 | # 4 | # Apple bugs, waiting for Apple to fix/respond: 5 | # 6 | # 15564624 - what does the xccheckout file in Xcode5 do? Where's the documentation? 7 | # 8 | # Version 2.6 9 | # For latest version, see: http://stackoverflow.com/questions/49478/git-ignore-file-for-xcode-projects 10 | # 11 | # 2015 updates: 12 | # - Fixed typo in "xccheckout" line - thanks to @lyck for pointing it out! 13 | # - Fixed the .idea optional ignore. Thanks to @hashier for pointing this out 14 | # - Finally added "xccheckout" to the ignore. Apple still refuses to answer support requests about this, but in practice it seems you should ignore it. 15 | # - minor tweaks from Jona and Coeur (slightly more precise xc* filtering/names) 16 | # 2014 updates: 17 | # - appended non-standard items DISABLED by default (uncomment if you use those tools) 18 | # - removed the edit that an SO.com moderator made without bothering to ask me 19 | # - researched CocoaPods .lock more carefully, thanks to Gokhan Celiker 20 | # 2013 updates: 21 | # - fixed the broken "save personal Schemes" 22 | # - added line-by-line explanations for EVERYTHING (some were missing) 23 | # 24 | # NB: if you are storing "built" products, this WILL NOT WORK, 25 | # and you should use a different .gitignore (or none at all) 26 | # This file is for SOURCE projects, where there are many extra 27 | # files that we want to exclude 28 | # 29 | ######################### 30 | 31 | ##### 32 | # OS X temporary files that should never be committed 33 | # 34 | # c.f. http://www.westwind.com/reference/os-x/invisibles.html 35 | 36 | .DS_Store 37 | 38 | # c.f. http://www.westwind.com/reference/os-x/invisibles.html 39 | 40 | .Trashes 41 | 42 | # c.f. http://www.westwind.com/reference/os-x/invisibles.html 43 | 44 | *.swp 45 | 46 | # 47 | # *.lock - this is used and abused by many editors for many different things. 48 | # For the main ones I use (e.g. Eclipse), it should be excluded 49 | # from source-control, but YMMV. 50 | # (lock files are usually local-only file-synchronization on the local FS that should NOT go in git) 51 | # c.f. the "OPTIONAL" section at bottom though, for tool-specific variations! 52 | # 53 | # In particular, if you're using CocoaPods, you'll want to comment-out this line: 54 | *.lock 55 | 56 | 57 | # 58 | # profile - REMOVED temporarily (on double-checking, I can't find it in OS X docs?) 59 | #profile 60 | 61 | 62 | #### 63 | # Xcode temporary files that should never be committed 64 | # 65 | # NB: NIB/XIB files still exist even on Storyboard projects, so we want this... 66 | 67 | *~.nib 68 | 69 | 70 | #### 71 | # Xcode build files - 72 | # 73 | # NB: slash on the end, so we only remove the FOLDER, not any files that were badly named "DerivedData" 74 | 75 | DerivedData/ 76 | 77 | # NB: slash on the end, so we only remove the FOLDER, not any files that were badly named "build" 78 | 79 | build/ 80 | 81 | 82 | ##### 83 | # Xcode private settings (window sizes, bookmarks, breakpoints, custom executables, smart groups) 84 | # 85 | # This is complicated: 86 | # 87 | # SOMETIMES you need to put this file in version control. 88 | # Apple designed it poorly - if you use "custom executables", they are 89 | # saved in this file. 90 | # 99% of projects do NOT use those, so they do NOT want to version control this file. 91 | # ..but if you're in the 1%, comment out the line "*.pbxuser" 92 | 93 | # .pbxuser: http://lists.apple.com/archives/xcode-users/2004/Jan/msg00193.html 94 | 95 | *.pbxuser 96 | 97 | # .mode1v3: http://lists.apple.com/archives/xcode-users/2007/Oct/msg00465.html 98 | 99 | *.mode1v3 100 | 101 | # .mode2v3: http://lists.apple.com/archives/xcode-users/2007/Oct/msg00465.html 102 | 103 | *.mode2v3 104 | 105 | # .perspectivev3: http://stackoverflow.com/questions/5223297/xcode-projects-what-is-a-perspectivev3-file 106 | 107 | *.perspectivev3 108 | 109 | # NB: also, whitelist the default ones, some projects need to use these 110 | !default.pbxuser 111 | !default.mode1v3 112 | !default.mode2v3 113 | !default.perspectivev3 114 | 115 | 116 | #### 117 | # Xcode 4 - semi-personal settings 118 | # 119 | # Apple Shared data that Apple put in the wrong folder 120 | # c.f. http://stackoverflow.com/a/19260712/153422 121 | # FROM ANSWER: Apple says "don't ignore it" 122 | # FROM COMMENTS: Apple is wrong; Apple code is too buggy to trust; there are no known negative side-effects to ignoring Apple's unofficial advice and instead doing the thing that actively fixes bugs in Xcode 123 | # Up to you, but ... current advice: ignore it. 124 | *.xccheckout 125 | 126 | # 127 | # 128 | # OPTION 1: --------------------------------- 129 | # throw away ALL personal settings (including custom schemes! 130 | # - unless they are "shared") 131 | # As per build/ and DerivedData/, this ought to have a trailing slash 132 | # 133 | # NB: this is exclusive with OPTION 2 below 134 | xcuserdata/ 135 | 136 | # OPTION 2: --------------------------------- 137 | # get rid of ALL personal settings, but KEEP SOME OF THEM 138 | # - NB: you must manually uncomment the bits you want to keep 139 | # 140 | # NB: this *requires* git v1.8.2 or above; you may need to upgrade to latest OS X, 141 | # or manually install git over the top of the OS X version 142 | # NB: this is exclusive with OPTION 1 above 143 | # 144 | #xcuserdata/**/* 145 | 146 | # (requires option 2 above): Personal Schemes 147 | # 148 | #!xcuserdata/**/xcschemes/* 149 | 150 | #### 151 | # XCode 4 workspaces - more detailed 152 | # 153 | # Workspaces are important! They are a core feature of Xcode - don't exclude them :) 154 | # 155 | # Workspace layout is quite spammy. For reference: 156 | # 157 | # /(root)/ 158 | # /(project-name).xcodeproj/ 159 | # project.pbxproj 160 | # /project.xcworkspace/ 161 | # contents.xcworkspacedata 162 | # /xcuserdata/ 163 | # /(your name)/xcuserdatad/ 164 | # UserInterfaceState.xcuserstate 165 | # /xcshareddata/ 166 | # /xcschemes/ 167 | # (shared scheme name).xcscheme 168 | # /xcuserdata/ 169 | # /(your name)/xcuserdatad/ 170 | # (private scheme).xcscheme 171 | # xcschememanagement.plist 172 | # 173 | # 174 | 175 | #### 176 | # Xcode 4 - Deprecated classes 177 | # 178 | # Allegedly, if you manually "deprecate" your classes, they get moved here. 179 | # 180 | # We're using source-control, so this is a "feature" that we do not want! 181 | 182 | *.moved-aside 183 | 184 | #### 185 | # OPTIONAL: Some well-known tools that people use side-by-side with Xcode / iOS development 186 | # 187 | # NB: I'd rather not include these here, but gitignore's design is weak and doesn't allow 188 | # modular gitignore: you have to put EVERYTHING in one file. 189 | # 190 | # COCOAPODS: 191 | # 192 | # c.f. http://guides.cocoapods.org/using/using-cocoapods.html#what-is-a-podfilelock 193 | # c.f. http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 194 | # 195 | #!Podfile.lock 196 | # 197 | # RUBY: 198 | # 199 | # c.f. http://yehudakatz.com/2010/12/16/clarifying-the-roles-of-the-gemspec-and-gemfile/ 200 | # 201 | #!Gemfile.lock 202 | # 203 | # IDEA: 204 | # 205 | # c.f. https://www.jetbrains.com/objc/help/managing-projects-under-version-control.html?search=workspace.xml 206 | # 207 | #.idea/workspace.xml 208 | # 209 | # TEXTMATE: 210 | # 211 | # -- UNVERIFIED: c.f. http://stackoverflow.com/a/50283/153422 212 | # 213 | #tm_build_errors 214 | 215 | #### 216 | # UNKNOWN: recommended by others, but I can't discover what these files are 217 | # -------------------------------------------------------------------------------- /Classes/Extension/UICollectionViewControllerExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UICollectionViewExtension.swift 3 | // CKWaveCollectionViewTransition 4 | // 5 | // Created by Salvation on 7/21/15. 6 | // Modified by seedante. 7 | // Copyright (c) 2015 CezaryKopacz. All rights reserved. 8 | // 9 | 10 | import UIKit 11 | import ObjectiveC 12 | 13 | private var selectedIndexPathAssociationKey: UInt8 = 0 14 | private var coverRectInSuperviewKey: UInt8 = 1 15 | 16 | extension UICollectionViewController { 17 | 18 | var selectedIndexPath: NSIndexPath! { 19 | get { 20 | return objc_getAssociatedObject(self, &selectedIndexPathAssociationKey) as? NSIndexPath 21 | } 22 | set(newValue) { 23 | objc_setAssociatedObject(self, &selectedIndexPathAssociationKey, newValue, .OBJC_ASSOCIATION_RETAIN) 24 | } 25 | } 26 | 27 | var coverRectInSuperview: CGRect! { 28 | get { 29 | let value = objc_getAssociatedObject(self, &coverRectInSuperviewKey) as? NSValue 30 | return value?.CGRectValue() 31 | } 32 | 33 | set(newValue){ 34 | let value = NSValue(CGRect: newValue) 35 | objc_setAssociatedObject(self, &coverRectInSuperviewKey, value, .OBJC_ASSOCIATION_RETAIN) 36 | } 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /Classes/SDENavigationControllerDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SDENavigationDelegate.swift 3 | // SDEAlbumTransition 4 | // 5 | // Created by seedante on 15/10/15. 6 | // Copyright © 2015年 seedante. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | 12 | class SDENavigationControllerDelegate: NSObject, UINavigationControllerDelegate { 13 | 14 | var interactive = false 15 | private (set) var animationController: UIViewControllerAnimatedTransitioning! 16 | private (set) var interactionController: UIPercentDrivenInteractiveTransition? 17 | 18 | //MARK: UINavigationControllerDelegate 19 | func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { 20 | 21 | animationController = SDEPushAndPopAnimationController(operation: operation) 22 | if operation == .Push{ 23 | if let toCollectionVC = toVC as? UICollectionViewController{ 24 | interactionController = SDEPopPinchInteractionController(toVC: toCollectionVC, holder: self) 25 | } 26 | } 27 | 28 | return animationController 29 | } 30 | 31 | //If you want a interaction transition, you must implement this method. 32 | func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { 33 | 34 | // interactive can only update in gesture action 35 | if interactive{ 36 | return interactionController 37 | } 38 | 39 | return nil 40 | } 41 | 42 | } 43 | 44 | -------------------------------------------------------------------------------- /Classes/SDEPopPinchInteractionController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SDEPushAndPopInteractionController.swift 3 | // SDECollectionViewAlbumTransition 4 | // 5 | // Created by seedante on 15/10/19. 6 | // Copyright © 2015年 seedante. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SDEPopPinchInteractionController: UIPercentDrivenInteractiveTransition { 12 | private var pinchGesture = UIPinchGestureRecognizer() 13 | private var topVC: UICollectionViewController 14 | private unowned var holder: SDENavigationControllerDelegate 15 | 16 | init(toVC topVC: UICollectionViewController, holder: SDENavigationControllerDelegate) { 17 | self.topVC = topVC 18 | self.holder = holder 19 | super.init() 20 | addPinchGestureOnView(self.topVC.view) 21 | } 22 | 23 | private func addPinchGestureOnView(view: UIView){ 24 | pinchGesture.addTarget(self, action: "sde_handlePinch:") 25 | view.addGestureRecognizer(pinchGesture) 26 | } 27 | 28 | internal func sde_handlePinch(gesture: UIPinchGestureRecognizer){ 29 | switch gesture.state{ 30 | case .Began: 31 | if gesture.scale < 1.0{ 32 | holder.interactive = true 33 | topVC.navigationController?.popViewControllerAnimated(true) 34 | } 35 | case .Changed: 36 | if gesture.scale < 1.0{ 37 | let progress = 1.0 - gesture.scale 38 | self.updateInteractiveTransition(progress) 39 | } 40 | case .Ended: 41 | if gesture.scale < 1.0{ 42 | let progress = 1.0 - gesture.scale 43 | if progress > 0.4{ 44 | self.finishInteractiveTransition() 45 | }else{ 46 | self.cancelInteractiveTransition() 47 | } 48 | holder.interactive = false 49 | } 50 | default: 51 | holder.interactive = false 52 | } 53 | } 54 | 55 | deinit{ 56 | pinchGesture.view?.removeGestureRecognizer(pinchGesture) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Classes/SDEPushAndPopAnimationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SDETransitionAnimator.swift 3 | // CustomCollectionViewTransition 4 | // 5 | // Created by seedante on 15/7/11. 6 | // Copyright © 2015年 seedante. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SDEPushAndPopAnimationController: NSObject, UIViewControllerAnimatedTransitioning { 12 | 13 | let coverEdgeInSets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) 14 | let coverViewBackgroundColor = UIColor.darkGrayColor() 15 | let horizontalCount = 5 16 | var verticalCount = 1 //>= 1 17 | var horizontalGap: CGFloat = 0 18 | var verticalGap: CGFloat = 0 19 | 20 | private let kAnimationDuration: Double = 0.8 21 | private let kCellAnimationSmallDelta: Double = 0.01 22 | private let kCellAnimationBigDelta: Double = 0.03 23 | 24 | private var operation: UINavigationControllerOperation 25 | 26 | init(operation: UINavigationControllerOperation){ 27 | self.operation = operation 28 | super.init() 29 | } 30 | 31 | //MARK: Protocol Method 32 | func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval { 33 | return kAnimationDuration 34 | } 35 | 36 | func animateTransition(transitionContext: UIViewControllerContextTransitioning) { 37 | 38 | let containerView = transitionContext.containerView() 39 | let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) as? UICollectionViewController 40 | let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) as? UICollectionViewController 41 | let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey) 42 | let toView = transitionContext.viewForKey(UITransitionContextToViewKey) 43 | let duration = transitionDuration(transitionContext) 44 | 45 | switch operation{ 46 | case .Push: 47 | let selectedCell = fromVC?.collectionView?.cellForItemAtIndexPath(fromVC!.selectedIndexPath) 48 | selectedCell?.hidden = true 49 | 50 | let layoutAttributes = fromVC!.collectionView?.layoutAttributesForItemAtIndexPath(fromVC!.selectedIndexPath) 51 | let areaRect = fromVC!.collectionView?.convertRect(layoutAttributes!.frame, toView: fromVC!.collectionView?.superview) 52 | toVC!.coverRectInSuperview = areaRect! 53 | 54 | //key code, the most important code here. without this line, you can't get visibleCells from UICollectionView. 55 | //And, there are other ways, Just make view redraw. 56 | toVC?.view.layoutIfNeeded() 57 | setupVisibleCellsBeforePushToVC(toVC!) 58 | containerView?.addSubview(toView!) 59 | 60 | let fakeCoverView = createAndSetupFakeCoverView(fromVC!, toVC: toVC!) 61 | 62 | UIView.setAnimationCurve(UIViewAnimationCurve.EaseOut) 63 | let options: UIViewKeyframeAnimationOptions = [.BeginFromCurrentState, .OverrideInheritedDuration, .CalculationModeCubic, .CalculationModeCubicPaced] 64 | UIView.animateKeyframesWithDuration(duration, delay: 0, options: options, animations: { 65 | 66 | self.addkeyFrameAnimationForBackgroundColorInPush(fromVC!, toVC: toVC!) 67 | self.addKeyFrameAnimationInPushForFakeCoverView(fakeCoverView) 68 | self.addKeyFrameAnimationOnVisibleCellsInPushToVC(toVC!) 69 | 70 | }, completion: { finished in 71 | 72 | let isCancelled = transitionContext.transitionWasCancelled() 73 | if isCancelled{ 74 | selectedCell?.hidden = false 75 | } 76 | transitionContext.completeTransition(!isCancelled) 77 | }) 78 | 79 | case .Pop: 80 | containerView?.insertSubview(toView!, belowSubview: fromView!) 81 | 82 | let coverView = fromView?.viewWithTag(1000) 83 | UIView.setAnimationCurve(UIViewAnimationCurve.EaseInOut) 84 | UIView.animateKeyframesWithDuration(duration, delay: 0, options: UIViewKeyframeAnimationOptions(), animations: { 85 | self.addkeyFrameAnimationForBackgroundColorInPop(fromVC!) 86 | self.addKeyFrameAnimationInPopForFakeCoverView(coverView) 87 | self.addKeyFrameAnimationOnVisibleCellsInPopFromVC(fromVC!) 88 | 89 | }, completion: { finished in 90 | 91 | let isCancelled = transitionContext.transitionWasCancelled() 92 | if !isCancelled{ 93 | let selectedCell = toVC?.collectionView?.cellForItemAtIndexPath(toVC!.selectedIndexPath) 94 | selectedCell?.hidden = false 95 | } 96 | transitionContext.completeTransition(!isCancelled) 97 | }) 98 | 99 | default:break 100 | } 101 | } 102 | 103 | //MARK: Push Transition Helper Method 104 | private func createAndSetupFakeCoverView(fromVC: UICollectionViewController, toVC: UICollectionViewController) -> UIView?{ 105 | let selectedCell = fromVC.collectionView?.cellForItemAtIndexPath(fromVC.selectedIndexPath) 106 | let snapshotCellView = selectedCell!.snapshotViewAfterScreenUpdates(false) 107 | snapshotCellView.tag = 10 108 | 109 | let coverContainerView = UIView(frame: snapshotCellView.frame) 110 | coverContainerView.backgroundColor = coverViewBackgroundColor 111 | coverContainerView.addSubview(snapshotCellView) 112 | coverContainerView.tag = 1000 113 | 114 | toVC.view.addSubview(coverContainerView) 115 | coverContainerView.frame = toVC.coverRectInSuperview 116 | 117 | let frame = coverContainerView.frame 118 | coverContainerView.layer.anchorPoint = CGPointMake(0, 0.5) 119 | coverContainerView.frame = frame 120 | 121 | return coverContainerView 122 | } 123 | 124 | private func addKeyFrameAnimationInPushForFakeCoverView(coverView: UIView?){ 125 | UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 0.5, animations: { 126 | var flipLeftTransform = CATransform3DIdentity 127 | flipLeftTransform.m34 = -1.0 / 500.0 128 | flipLeftTransform = CATransform3DRotate(flipLeftTransform, CGFloat(-M_PI), 0.0, 1.0, 0.0) 129 | coverView?.layer.transform = flipLeftTransform 130 | }) 131 | 132 | UIView.addKeyframeWithRelativeStartTime(0.45, relativeDuration: 0.05, animations: { 133 | coverView?.alpha = 0 134 | }) 135 | 136 | //fix Transparent Background In Flip Animation 137 | let snapshotView = coverView?.viewWithTag(10) 138 | UIView.addKeyframeWithRelativeStartTime(0.25, relativeDuration: 0.01, animations: { 139 | snapshotView?.alpha = 0 140 | }) 141 | 142 | } 143 | 144 | private func addkeyFrameAnimationForBackgroundColorInPush(fromVC: UICollectionViewController, toVC: UICollectionViewController){ 145 | UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 1.0, animations: { 146 | let toCollectionViewBackgroundColor = fromVC.collectionView?.backgroundColor 147 | toVC.collectionView?.backgroundColor = toCollectionViewBackgroundColor 148 | }) 149 | } 150 | 151 | 152 | private func addKeyFrameAnimationOnVisibleCellsInPushToVC(toVC: UICollectionViewController){ 153 | let collectionView = toVC.collectionView! 154 | for cell in collectionView.visibleCells(){ 155 | let indexPath = collectionView.indexPathForCell(cell)! 156 | let layoutAttributes = collectionView.layoutAttributesForItemAtIndexPath(indexPath) 157 | var column = indexPath.row / horizontalCount 158 | let row = indexPath.row % horizontalCount 159 | if (column + 1) * (row + 1) > horizontalCount * verticalCount{ 160 | column = ((column + 1) * (row + 1)) % (horizontalCount * verticalCount) / horizontalCount 161 | } 162 | 163 | let columns = verticalCount 164 | 165 | let relativeStartTime = (self.kCellAnimationBigDelta * Double(indexPath.row % columns)) 166 | let relativeDuration = 0.5 - (self.kCellAnimationSmallDelta * Double(indexPath.row)) 167 | 168 | UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 0.7, animations: { 169 | cell.alpha = 1 170 | }) 171 | 172 | UIView.addKeyframeWithRelativeStartTime(0.5 + relativeStartTime, relativeDuration: relativeDuration, animations: { 173 | cell.transform = CGAffineTransformScale(CGAffineTransformIdentity, 1, 1) 174 | }) 175 | 176 | UIView.addKeyframeWithRelativeStartTime(0.5 + relativeStartTime, relativeDuration: relativeDuration, animations: { 177 | cell.center = layoutAttributes!.center 178 | }) 179 | 180 | 181 | } 182 | 183 | } 184 | 185 | 186 | private func setupVisibleCellsBeforePushToVC(toVC:UICollectionViewController){ 187 | if toVC.collectionView?.visibleCells().count > 0{ 188 | 189 | let areaRect = toVC.collectionView!.convertRect(toVC.coverRectInSuperview, fromView: toVC.collectionView!.superview) 190 | let cellsAreaRect = UIEdgeInsetsInsetRect(areaRect, coverEdgeInSets) 191 | 192 | let cellWidth = (cellsAreaRect.width - CGFloat(horizontalCount - 1) * horizontalGap) / CGFloat(horizontalCount) 193 | let cellHeight = cellWidth 194 | verticalCount = Int((cellsAreaRect.height + verticalGap) / (cellHeight + verticalGap)) 195 | 196 | for cell in toVC.collectionView!.visibleCells(){ 197 | let indexPath = toVC.collectionView!.indexPathForCell(cell) 198 | var column = indexPath!.row / horizontalCount 199 | let row = indexPath!.row % horizontalCount 200 | if (column + 1) * (row + 1) > horizontalCount * verticalCount{ 201 | column = ((column + 1) * (row + 1)) % (horizontalCount * verticalCount) / horizontalCount 202 | } 203 | 204 | let centerY: CGFloat = cellsAreaRect.origin.y + cellHeight / 2 + CGFloat(column) * (cellHeight + verticalGap) 205 | let centerX = cellsAreaRect.origin.x + cellWidth / 2 + CGFloat(row) * (cellWidth + horizontalGap) 206 | cell.center = CGPoint(x: centerX, y: centerY) 207 | 208 | let widthScale = cellWidth / cell.frame.width 209 | let heightScale = cellHeight / cell.frame.height 210 | cell.transform = CGAffineTransformScale(CGAffineTransformIdentity, widthScale, heightScale) 211 | 212 | cell.alpha = 0 213 | cell.layer.zPosition = CGFloat(100 - indexPath!.row) 214 | } 215 | } 216 | } 217 | 218 | //MARK: Pop Transition Helper Method 219 | private func addkeyFrameAnimationForBackgroundColorInPop(fromVC: UICollectionViewController){ 220 | UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 0.5, animations: { 221 | fromVC.collectionView?.backgroundColor = UIColor.clearColor() 222 | }) 223 | } 224 | 225 | private func addKeyFrameAnimationInPopForFakeCoverView(coverView: UIView?){ 226 | UIView.addKeyframeWithRelativeStartTime(0.5, relativeDuration: 0.1, animations: { 227 | coverView?.alpha = 1 228 | }) 229 | 230 | 231 | UIView.addKeyframeWithRelativeStartTime(0.5, relativeDuration: 0.5, animations: { 232 | coverView?.layer.transform = CATransform3DIdentity 233 | }) 234 | 235 | //fix Transparent Background In Flip Animation 236 | let snapshotView = coverView?.viewWithTag(10) 237 | UIView.addKeyframeWithRelativeStartTime(0.75, relativeDuration: 0.01, animations: { 238 | snapshotView?.alpha = 1 239 | }) 240 | } 241 | 242 | private func addKeyFrameAnimationOnVisibleCellsInPopFromVC(fromVC: UICollectionViewController) { 243 | 244 | let visibleCellIndexPaths = fromVC.collectionView?.indexPathsForVisibleItems() 245 | if visibleCellIndexPaths?.count > 0{ 246 | //find minimal indexpath's row 247 | let rows = visibleCellIndexPaths!.map({$0.row}) 248 | let minimalRow = rows.reduce(Int.max, combine: { $0 < $1 ? $0 : $1 }) 249 | 250 | let collectionView = fromVC.collectionView! 251 | let areaRect = collectionView.convertRect(fromVC.coverRectInSuperview, fromView: fromVC.collectionView!.superview) 252 | let cellsAreaRect = UIEdgeInsetsInsetRect(areaRect, coverEdgeInSets) 253 | 254 | let cellWidth = (cellsAreaRect.width - CGFloat(horizontalCount - 1) * horizontalGap) / CGFloat(horizontalCount) 255 | let cellHeight = cellWidth 256 | verticalCount = Int((cellsAreaRect.height + verticalGap) / (cellHeight + verticalGap)) 257 | 258 | for indexPath in visibleCellIndexPaths!{ 259 | let relativeRow = indexPath.row - minimalRow 260 | var column = relativeRow / horizontalCount 261 | let row = relativeRow % horizontalCount 262 | if (column + 1) * (row + 1) > horizontalCount * verticalCount{ 263 | column = ((column + 1) * (row + 1)) % (horizontalCount * verticalCount) / horizontalCount 264 | } 265 | 266 | let centerY: CGFloat = cellsAreaRect.origin.y + cellHeight / 2 + CGFloat(column) * (cellHeight + verticalGap) 267 | let centerX = cellsAreaRect.origin.x + cellWidth / 2 + CGFloat(row) * (cellWidth + horizontalGap) 268 | 269 | let cell = collectionView.cellForItemAtIndexPath(indexPath)! 270 | let widthScale = cellWidth / cell.frame.width 271 | let heightScale = cellHeight / cell.frame.height 272 | 273 | let relativeStartTime = (self.kCellAnimationBigDelta * Double(relativeRow % verticalCount)) 274 | let relativeDuration = 0.5 - (self.kCellAnimationSmallDelta * Double(relativeRow)) 275 | 276 | UIView.addKeyframeWithRelativeStartTime(relativeStartTime, relativeDuration: relativeDuration, animations: { 277 | cell.center = CGPoint(x: centerX, y: centerY) 278 | cell.transform = CGAffineTransformScale(CGAffineTransformIdentity, widthScale, heightScale) 279 | }) 280 | } 281 | } 282 | } 283 | 284 | 285 | 286 | } 287 | -------------------------------------------------------------------------------- /Demo/SDECollectionViewAlbumTransition.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | FD62436B1C5578A5006FFBCF /* Base.lproj in Resources */ = {isa = PBXBuildFile; fileRef = FD62436A1C5578A5006FFBCF /* Base.lproj */; }; 11 | FD83B6221BD529C100C4326E /* SDEPopPinchInteractionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B6211BD529C100C4326E /* SDEPopPinchInteractionController.swift */; }; 12 | FDB179B41BD00B610029A9AB /* UICollectionViewControllerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB179B01BD00B610029A9AB /* UICollectionViewControllerExtension.swift */; }; 13 | FDBB4B4F1B5BDFBE00B6D674 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBB4B4E1B5BDFBE00B6D674 /* AppDelegate.swift */; }; 14 | FDBB4B591B5BDFBE00B6D674 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FDBB4B571B5BDFBE00B6D674 /* LaunchScreen.storyboard */; }; 15 | FDBB4B681B5BE02400B6D674 /* SDEAlbumViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBB4B601B5BE02400B6D674 /* SDEAlbumViewController.swift */; }; 16 | FDBB4B691B5BE02400B6D674 /* SDEGalleriesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBB4B611B5BE02400B6D674 /* SDEGalleriesViewController.swift */; }; 17 | FDBB4B6A1B5BE02400B6D674 /* SDEPushAndPopAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBB4B621B5BE02400B6D674 /* SDEPushAndPopAnimationController.swift */; }; 18 | FDBB4B6C1B5BE02400B6D674 /* SDEAdaptiveAssetCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBB4B641B5BE02400B6D674 /* SDEAdaptiveAssetCell.swift */; }; 19 | FDBB4B6D1B5BE02400B6D674 /* NSTimeIntervalOutputExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBB4B651B5BE02400B6D674 /* NSTimeIntervalOutputExtension.swift */; }; 20 | FDBB4B6E1B5BE02400B6D674 /* video_call.png in Resources */ = {isa = PBXBuildFile; fileRef = FDBB4B661B5BE02400B6D674 /* video_call.png */; }; 21 | FDBB4B6F1B5BE02400B6D674 /* video_call@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = FDBB4B671B5BE02400B6D674 /* video_call@2x.png */; }; 22 | FDBB4B711B5BE08800B6D674 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FDBB4B701B5BE08800B6D674 /* Main.storyboard */; }; 23 | FDBB4B731B5BE20800B6D674 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = FDBB4B721B5BE20800B6D674 /* Default-568h@2x.png */; }; 24 | FDE437511BCF86FC001A6BF8 /* SDENavigationControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE437501BCF86FC001A6BF8 /* SDENavigationControllerDelegate.swift */; }; 25 | FDE437541BCF9703001A6BF8 /* PHFetchResultExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE437531BCF9703001A6BF8 /* PHFetchResultExtension.swift */; }; 26 | /* End PBXBuildFile section */ 27 | 28 | /* Begin PBXFileReference section */ 29 | FD62436A1C5578A5006FFBCF /* Base.lproj */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Base.lproj; sourceTree = ""; }; 30 | FD83B6211BD529C100C4326E /* SDEPopPinchInteractionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SDEPopPinchInteractionController.swift; sourceTree = ""; }; 31 | FDB179B01BD00B610029A9AB /* UICollectionViewControllerExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = UICollectionViewControllerExtension.swift; path = Extension/UICollectionViewControllerExtension.swift; sourceTree = ""; }; 32 | FDBB4B4B1B5BDFBE00B6D674 /* SDECollectionViewAlbumTransition.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SDECollectionViewAlbumTransition.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33 | FDBB4B4E1B5BDFBE00B6D674 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 34 | FDBB4B581B5BDFBE00B6D674 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 35 | FDBB4B5A1B5BDFBE00B6D674 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 36 | FDBB4B601B5BE02400B6D674 /* SDEAlbumViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SDEAlbumViewController.swift; sourceTree = ""; }; 37 | FDBB4B611B5BE02400B6D674 /* SDEGalleriesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SDEGalleriesViewController.swift; sourceTree = ""; }; 38 | FDBB4B621B5BE02400B6D674 /* SDEPushAndPopAnimationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SDEPushAndPopAnimationController.swift; sourceTree = ""; }; 39 | FDBB4B641B5BE02400B6D674 /* SDEAdaptiveAssetCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SDEAdaptiveAssetCell.swift; sourceTree = ""; }; 40 | FDBB4B651B5BE02400B6D674 /* NSTimeIntervalOutputExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSTimeIntervalOutputExtension.swift; sourceTree = ""; }; 41 | FDBB4B661B5BE02400B6D674 /* video_call.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = video_call.png; sourceTree = ""; }; 42 | FDBB4B671B5BE02400B6D674 /* video_call@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "video_call@2x.png"; sourceTree = ""; }; 43 | FDBB4B701B5BE08800B6D674 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; 44 | FDBB4B721B5BE20800B6D674 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-568h@2x.png"; sourceTree = ""; }; 45 | FDE437501BCF86FC001A6BF8 /* SDENavigationControllerDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SDENavigationControllerDelegate.swift; sourceTree = ""; }; 46 | FDE437531BCF9703001A6BF8 /* PHFetchResultExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PHFetchResultExtension.swift; sourceTree = ""; }; 47 | /* End PBXFileReference section */ 48 | 49 | /* Begin PBXFrameworksBuildPhase section */ 50 | FDBB4B481B5BDFBE00B6D674 /* Frameworks */ = { 51 | isa = PBXFrameworksBuildPhase; 52 | buildActionMask = 2147483647; 53 | files = ( 54 | ); 55 | runOnlyForDeploymentPostprocessing = 0; 56 | }; 57 | /* End PBXFrameworksBuildPhase section */ 58 | 59 | /* Begin PBXGroup section */ 60 | FDBB4B421B5BDFBE00B6D674 = { 61 | isa = PBXGroup; 62 | children = ( 63 | FDE437521BCF8723001A6BF8 /* Classes */, 64 | FDBB4B4D1B5BDFBE00B6D674 /* Example */, 65 | FDBB4B4C1B5BDFBE00B6D674 /* Products */, 66 | ); 67 | sourceTree = ""; 68 | }; 69 | FDBB4B4C1B5BDFBE00B6D674 /* Products */ = { 70 | isa = PBXGroup; 71 | children = ( 72 | FDBB4B4B1B5BDFBE00B6D674 /* SDECollectionViewAlbumTransition.app */, 73 | ); 74 | name = Products; 75 | sourceTree = ""; 76 | }; 77 | FDBB4B4D1B5BDFBE00B6D674 /* Example */ = { 78 | isa = PBXGroup; 79 | children = ( 80 | FD62436A1C5578A5006FFBCF /* Base.lproj */, 81 | FDBB4B651B5BE02400B6D674 /* NSTimeIntervalOutputExtension.swift */, 82 | FDE437531BCF9703001A6BF8 /* PHFetchResultExtension.swift */, 83 | FDBB4B4E1B5BDFBE00B6D674 /* AppDelegate.swift */, 84 | FDBB4B611B5BE02400B6D674 /* SDEGalleriesViewController.swift */, 85 | FDBB4B601B5BE02400B6D674 /* SDEAlbumViewController.swift */, 86 | FDBB4B641B5BE02400B6D674 /* SDEAdaptiveAssetCell.swift */, 87 | FDBB4B661B5BE02400B6D674 /* video_call.png */, 88 | FDBB4B671B5BE02400B6D674 /* video_call@2x.png */, 89 | FDBB4B721B5BE20800B6D674 /* Default-568h@2x.png */, 90 | FDBB4B571B5BDFBE00B6D674 /* LaunchScreen.storyboard */, 91 | FDBB4B701B5BE08800B6D674 /* Main.storyboard */, 92 | FDBB4B5A1B5BDFBE00B6D674 /* Info.plist */, 93 | ); 94 | name = Example; 95 | path = SDECollectionViewAlbumTransition; 96 | sourceTree = ""; 97 | }; 98 | FDE437521BCF8723001A6BF8 /* Classes */ = { 99 | isa = PBXGroup; 100 | children = ( 101 | FDBB4B621B5BE02400B6D674 /* SDEPushAndPopAnimationController.swift */, 102 | FD83B6211BD529C100C4326E /* SDEPopPinchInteractionController.swift */, 103 | FDE437501BCF86FC001A6BF8 /* SDENavigationControllerDelegate.swift */, 104 | FDE437551BCFB09B001A6BF8 /* Extension */, 105 | ); 106 | name = Classes; 107 | path = ../Classes; 108 | sourceTree = ""; 109 | }; 110 | FDE437551BCFB09B001A6BF8 /* Extension */ = { 111 | isa = PBXGroup; 112 | children = ( 113 | FDB179B01BD00B610029A9AB /* UICollectionViewControllerExtension.swift */, 114 | ); 115 | name = Extension; 116 | sourceTree = ""; 117 | }; 118 | /* End PBXGroup section */ 119 | 120 | /* Begin PBXNativeTarget section */ 121 | FDBB4B4A1B5BDFBE00B6D674 /* SDECollectionViewAlbumTransition */ = { 122 | isa = PBXNativeTarget; 123 | buildConfigurationList = FDBB4B5D1B5BDFBE00B6D674 /* Build configuration list for PBXNativeTarget "SDECollectionViewAlbumTransition" */; 124 | buildPhases = ( 125 | FDBB4B471B5BDFBE00B6D674 /* Sources */, 126 | FDBB4B481B5BDFBE00B6D674 /* Frameworks */, 127 | FDBB4B491B5BDFBE00B6D674 /* Resources */, 128 | ); 129 | buildRules = ( 130 | ); 131 | dependencies = ( 132 | ); 133 | name = SDECollectionViewAlbumTransition; 134 | productName = ExploreAlbumOpenAnimation; 135 | productReference = FDBB4B4B1B5BDFBE00B6D674 /* SDECollectionViewAlbumTransition.app */; 136 | productType = "com.apple.product-type.application"; 137 | }; 138 | /* End PBXNativeTarget section */ 139 | 140 | /* Begin PBXProject section */ 141 | FDBB4B431B5BDFBE00B6D674 /* Project object */ = { 142 | isa = PBXProject; 143 | attributes = { 144 | LastUpgradeCheck = 0700; 145 | ORGANIZATIONNAME = seedante; 146 | TargetAttributes = { 147 | FDBB4B4A1B5BDFBE00B6D674 = { 148 | CreatedOnToolsVersion = 7.0; 149 | DevelopmentTeam = 6Z8XUV9H6J; 150 | }; 151 | }; 152 | }; 153 | buildConfigurationList = FDBB4B461B5BDFBE00B6D674 /* Build configuration list for PBXProject "SDECollectionViewAlbumTransition" */; 154 | compatibilityVersion = "Xcode 3.2"; 155 | developmentRegion = English; 156 | hasScannedForEncodings = 0; 157 | knownRegions = ( 158 | en, 159 | Base, 160 | ); 161 | mainGroup = FDBB4B421B5BDFBE00B6D674; 162 | productRefGroup = FDBB4B4C1B5BDFBE00B6D674 /* Products */; 163 | projectDirPath = ""; 164 | projectRoot = ""; 165 | targets = ( 166 | FDBB4B4A1B5BDFBE00B6D674 /* SDECollectionViewAlbumTransition */, 167 | ); 168 | }; 169 | /* End PBXProject section */ 170 | 171 | /* Begin PBXResourcesBuildPhase section */ 172 | FDBB4B491B5BDFBE00B6D674 /* Resources */ = { 173 | isa = PBXResourcesBuildPhase; 174 | buildActionMask = 2147483647; 175 | files = ( 176 | FDBB4B591B5BDFBE00B6D674 /* LaunchScreen.storyboard in Resources */, 177 | FDBB4B711B5BE08800B6D674 /* Main.storyboard in Resources */, 178 | FDBB4B6F1B5BE02400B6D674 /* video_call@2x.png in Resources */, 179 | FDBB4B6E1B5BE02400B6D674 /* video_call.png in Resources */, 180 | FD62436B1C5578A5006FFBCF /* Base.lproj in Resources */, 181 | FDBB4B731B5BE20800B6D674 /* Default-568h@2x.png in Resources */, 182 | ); 183 | runOnlyForDeploymentPostprocessing = 0; 184 | }; 185 | /* End PBXResourcesBuildPhase section */ 186 | 187 | /* Begin PBXSourcesBuildPhase section */ 188 | FDBB4B471B5BDFBE00B6D674 /* Sources */ = { 189 | isa = PBXSourcesBuildPhase; 190 | buildActionMask = 2147483647; 191 | files = ( 192 | FDBB4B691B5BE02400B6D674 /* SDEGalleriesViewController.swift in Sources */, 193 | FDBB4B6D1B5BE02400B6D674 /* NSTimeIntervalOutputExtension.swift in Sources */, 194 | FDBB4B6A1B5BE02400B6D674 /* SDEPushAndPopAnimationController.swift in Sources */, 195 | FDE437541BCF9703001A6BF8 /* PHFetchResultExtension.swift in Sources */, 196 | FDBB4B6C1B5BE02400B6D674 /* SDEAdaptiveAssetCell.swift in Sources */, 197 | FDE437511BCF86FC001A6BF8 /* SDENavigationControllerDelegate.swift in Sources */, 198 | FDBB4B4F1B5BDFBE00B6D674 /* AppDelegate.swift in Sources */, 199 | FD83B6221BD529C100C4326E /* SDEPopPinchInteractionController.swift in Sources */, 200 | FDBB4B681B5BE02400B6D674 /* SDEAlbumViewController.swift in Sources */, 201 | FDB179B41BD00B610029A9AB /* UICollectionViewControllerExtension.swift in Sources */, 202 | ); 203 | runOnlyForDeploymentPostprocessing = 0; 204 | }; 205 | /* End PBXSourcesBuildPhase section */ 206 | 207 | /* Begin PBXVariantGroup section */ 208 | FDBB4B571B5BDFBE00B6D674 /* LaunchScreen.storyboard */ = { 209 | isa = PBXVariantGroup; 210 | children = ( 211 | FDBB4B581B5BDFBE00B6D674 /* Base */, 212 | ); 213 | name = LaunchScreen.storyboard; 214 | sourceTree = ""; 215 | }; 216 | /* End PBXVariantGroup section */ 217 | 218 | /* Begin XCBuildConfiguration section */ 219 | FDBB4B5B1B5BDFBE00B6D674 /* Debug */ = { 220 | isa = XCBuildConfiguration; 221 | buildSettings = { 222 | ALWAYS_SEARCH_USER_PATHS = NO; 223 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 224 | CLANG_CXX_LIBRARY = "libc++"; 225 | CLANG_ENABLE_MODULES = YES; 226 | CLANG_ENABLE_OBJC_ARC = YES; 227 | CLANG_WARN_BOOL_CONVERSION = YES; 228 | CLANG_WARN_CONSTANT_CONVERSION = YES; 229 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 230 | CLANG_WARN_EMPTY_BODY = YES; 231 | CLANG_WARN_ENUM_CONVERSION = YES; 232 | CLANG_WARN_INT_CONVERSION = YES; 233 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 234 | CLANG_WARN_UNREACHABLE_CODE = YES; 235 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 236 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 237 | COPY_PHASE_STRIP = NO; 238 | DEBUG_INFORMATION_FORMAT = dwarf; 239 | ENABLE_STRICT_OBJC_MSGSEND = YES; 240 | ENABLE_TESTABILITY = YES; 241 | GCC_C_LANGUAGE_STANDARD = gnu99; 242 | GCC_DYNAMIC_NO_PIC = NO; 243 | GCC_NO_COMMON_BLOCKS = YES; 244 | GCC_OPTIMIZATION_LEVEL = 0; 245 | GCC_PREPROCESSOR_DEFINITIONS = ( 246 | "DEBUG=1", 247 | "$(inherited)", 248 | ); 249 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 250 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 251 | GCC_WARN_UNDECLARED_SELECTOR = YES; 252 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 253 | GCC_WARN_UNUSED_FUNCTION = YES; 254 | GCC_WARN_UNUSED_VARIABLE = YES; 255 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 256 | MTL_ENABLE_DEBUG_INFO = YES; 257 | ONLY_ACTIVE_ARCH = YES; 258 | SDKROOT = iphoneos; 259 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 260 | TARGETED_DEVICE_FAMILY = "1,2"; 261 | }; 262 | name = Debug; 263 | }; 264 | FDBB4B5C1B5BDFBE00B6D674 /* Release */ = { 265 | isa = XCBuildConfiguration; 266 | buildSettings = { 267 | ALWAYS_SEARCH_USER_PATHS = NO; 268 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 269 | CLANG_CXX_LIBRARY = "libc++"; 270 | CLANG_ENABLE_MODULES = YES; 271 | CLANG_ENABLE_OBJC_ARC = YES; 272 | CLANG_WARN_BOOL_CONVERSION = YES; 273 | CLANG_WARN_CONSTANT_CONVERSION = YES; 274 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 275 | CLANG_WARN_EMPTY_BODY = YES; 276 | CLANG_WARN_ENUM_CONVERSION = YES; 277 | CLANG_WARN_INT_CONVERSION = YES; 278 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 279 | CLANG_WARN_UNREACHABLE_CODE = YES; 280 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 281 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 282 | COPY_PHASE_STRIP = NO; 283 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 284 | ENABLE_NS_ASSERTIONS = NO; 285 | ENABLE_STRICT_OBJC_MSGSEND = YES; 286 | GCC_C_LANGUAGE_STANDARD = gnu99; 287 | GCC_NO_COMMON_BLOCKS = YES; 288 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 289 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 290 | GCC_WARN_UNDECLARED_SELECTOR = YES; 291 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 292 | GCC_WARN_UNUSED_FUNCTION = YES; 293 | GCC_WARN_UNUSED_VARIABLE = YES; 294 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 295 | MTL_ENABLE_DEBUG_INFO = NO; 296 | SDKROOT = iphoneos; 297 | TARGETED_DEVICE_FAMILY = "1,2"; 298 | VALIDATE_PRODUCT = YES; 299 | }; 300 | name = Release; 301 | }; 302 | FDBB4B5E1B5BDFBE00B6D674 /* Debug */ = { 303 | isa = XCBuildConfiguration; 304 | buildSettings = { 305 | CODE_SIGN_IDENTITY = "iPhone Developer"; 306 | INFOPLIST_FILE = "$(SRCROOT)/SDECollectionViewAlbumTransition/Info.plist"; 307 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 308 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 309 | PRODUCT_BUNDLE_IDENTIFIER = com.seedante.SDECollectionViewAlbumTransition; 310 | PRODUCT_NAME = SDECollectionViewAlbumTransition; 311 | TARGETED_DEVICE_FAMILY = "1,2"; 312 | }; 313 | name = Debug; 314 | }; 315 | FDBB4B5F1B5BDFBE00B6D674 /* Release */ = { 316 | isa = XCBuildConfiguration; 317 | buildSettings = { 318 | CODE_SIGN_IDENTITY = "iPhone Developer"; 319 | INFOPLIST_FILE = "$(SRCROOT)/SDECollectionViewAlbumTransition/Info.plist"; 320 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 321 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 322 | PRODUCT_BUNDLE_IDENTIFIER = com.seedante.SDECollectionViewAlbumTransition; 323 | PRODUCT_NAME = SDECollectionViewAlbumTransition; 324 | TARGETED_DEVICE_FAMILY = "1,2"; 325 | }; 326 | name = Release; 327 | }; 328 | /* End XCBuildConfiguration section */ 329 | 330 | /* Begin XCConfigurationList section */ 331 | FDBB4B461B5BDFBE00B6D674 /* Build configuration list for PBXProject "SDECollectionViewAlbumTransition" */ = { 332 | isa = XCConfigurationList; 333 | buildConfigurations = ( 334 | FDBB4B5B1B5BDFBE00B6D674 /* Debug */, 335 | FDBB4B5C1B5BDFBE00B6D674 /* Release */, 336 | ); 337 | defaultConfigurationIsVisible = 0; 338 | defaultConfigurationName = Release; 339 | }; 340 | FDBB4B5D1B5BDFBE00B6D674 /* Build configuration list for PBXNativeTarget "SDECollectionViewAlbumTransition" */ = { 341 | isa = XCConfigurationList; 342 | buildConfigurations = ( 343 | FDBB4B5E1B5BDFBE00B6D674 /* Debug */, 344 | FDBB4B5F1B5BDFBE00B6D674 /* Release */, 345 | ); 346 | defaultConfigurationIsVisible = 0; 347 | defaultConfigurationName = Release; 348 | }; 349 | /* End XCConfigurationList section */ 350 | }; 351 | rootObject = FDBB4B431B5BDFBE00B6D674 /* Project object */; 352 | } 353 | -------------------------------------------------------------------------------- /Demo/SDECollectionViewAlbumTransition.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/SDECollectionViewAlbumTransition/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // ExploreAlbumOpenAnimation 4 | // 5 | // Created by seedante on 15/7/19. 6 | // Copyright © 2015年 seedante. 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 | -------------------------------------------------------------------------------- /Demo/SDECollectionViewAlbumTransition/Base.lproj/LaunchScreen.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 | 27 | 28 | -------------------------------------------------------------------------------- /Demo/SDECollectionViewAlbumTransition/Default-568h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seedante/SDECollectionViewAlbumTransition/309a25c69c08b1df7485514ae45bf1bf883b7a90/Demo/SDECollectionViewAlbumTransition/Default-568h@2x.png -------------------------------------------------------------------------------- /Demo/SDECollectionViewAlbumTransition/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 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 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /Demo/SDECollectionViewAlbumTransition/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 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | -------------------------------------------------------------------------------- /Demo/SDECollectionViewAlbumTransition/NSTimeIntervalOutputExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimeIntervalOutputExtension.swift 3 | // Albums 4 | // 5 | // Created by seedante on 15/7/8. 6 | // Copyright © 2015年 seedante. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension NSTimeInterval{ 12 | public var humanOutput: String{ 13 | if self < 60{ 14 | if self < 1{ 15 | return "00:01" 16 | }else{ 17 | let int = Int(self) 18 | let outputString = int < 10 ? "00:0\(int)" : "00:\(int)" 19 | return outputString 20 | } 21 | }else if self < 3600{ 22 | let int = Int(self) 23 | let minute = int / 60 24 | let minuteString = minute < 10 ? "0\(minute):" : "\(minute):" 25 | let second = int % 60 26 | let secondString = second < 10 ? "0\(second)" : "\(second)" 27 | 28 | return minuteString + secondString 29 | }else if self < 360000{ 30 | let int = Int(self) 31 | let hour = int / 3600 32 | let hourString = hour < 10 ? "0\(hour):" : "\(hour):" 33 | let minute = (int % 3600) / 60 34 | let minuteString = minute < 10 ? "0\(minute):" : "\(minute):" 35 | let second = (int % 3600) % 60 36 | let secondString = second < 10 ? "0\(second)" : "\(second)" 37 | 38 | return hourString + minuteString + secondString 39 | }else{ 40 | return "> 100 Hours" 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /Demo/SDECollectionViewAlbumTransition/PHFetchResultExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PHFetchResultExtension.swift 3 | // SDEAlbumTransition 4 | // 5 | // Created by seedante on 15/10/15. 6 | // Copyright © 2015年 seedante. All rights reserved. 7 | // 8 | 9 | import Photos 10 | 11 | extension PHFetchResult{ 12 | class func fetchPosterImageForAssetCollection(assetCollection: PHAssetCollection, targetSize: CGSize) -> UIImage?{ 13 | 14 | let fetchOptions = PHFetchOptions() 15 | fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)] 16 | let fetchResult = PHAsset.fetchAssetsInAssetCollection(assetCollection, options: fetchOptions) 17 | 18 | var posterImage: UIImage? 19 | fetchResult.enumerateObjectsAtIndexes(NSIndexSet(index: 0), options: NSEnumerationOptions.Concurrent){ 20 | (assetItem: AnyObject!, index: Int, stop: UnsafeMutablePointer) -> Void in 21 | 22 | let requestOptions = PHImageRequestOptions() 23 | requestOptions.synchronous = true 24 | PHImageManager.defaultManager().requestImageForAsset(assetItem as! PHAsset, targetSize: targetSize, contentMode: .AspectFill, options: requestOptions, resultHandler:{ 25 | (image: UIImage?, info: [NSObject: AnyObject]?) -> Void in 26 | posterImage = image 27 | }) 28 | } 29 | 30 | return posterImage 31 | } 32 | 33 | class func fetchPosterImageForAssetCollection(assetCollection: PHAssetCollection, imageView: UIImageView, targetSize: CGSize){ 34 | let fetchOptions = PHFetchOptions() 35 | fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)] 36 | let fetchResult = PHAsset.fetchAssetsInAssetCollection(assetCollection, options: fetchOptions) 37 | 38 | fetchResult.enumerateObjectsAtIndexes(NSIndexSet(index: 0), options: NSEnumerationOptions.Concurrent){ 39 | (assetItem: AnyObject!, index: Int, stop: UnsafeMutablePointer) -> Void in 40 | 41 | PHImageManager.defaultManager().requestImageForAsset(assetItem as! PHAsset, targetSize: targetSize, contentMode: .AspectFill, options: nil, resultHandler:{ 42 | (image, info) -> Void in 43 | imageView.image = image 44 | }) 45 | } 46 | } 47 | 48 | func fetchImageAtIndex(index: Int, targetSize: CGSize) -> UIImage?{ 49 | if self.count > index{ 50 | var fetchedImage: UIImage? 51 | self.enumerateObjectsAtIndexes(NSIndexSet(index: index), options: NSEnumerationOptions.Concurrent){ 52 | (assetItem, index, stop) -> Void in 53 | 54 | if let asset = assetItem as? PHAsset{ 55 | let requestOptions = PHImageRequestOptions() 56 | requestOptions.synchronous = true 57 | 58 | PHImageManager.defaultManager().requestImageForAsset(asset, targetSize: targetSize, contentMode: .AspectFill, options: requestOptions, resultHandler: { 59 | (image, info) in 60 | fetchedImage = image 61 | }) 62 | } 63 | 64 | } 65 | return fetchedImage 66 | } 67 | 68 | return nil 69 | } 70 | 71 | func fetchImageAtIndex(index: Int, imageView:UIImageView, targetSize: CGSize){ 72 | if self.count > index{ 73 | self.enumerateObjectsAtIndexes(NSIndexSet(index: index), options: NSEnumerationOptions.Concurrent){ 74 | (assetItem, index, stop) -> Void in 75 | 76 | if let asset = assetItem as? PHAsset{ 77 | PHImageManager.defaultManager().requestImageForAsset(asset, targetSize: targetSize, contentMode: .AspectFill, options: nil, resultHandler: { 78 | (image, info) in 79 | imageView.image = image 80 | }) 81 | } 82 | } 83 | } 84 | } 85 | 86 | func getAssetAtIndex(index: Int) -> PHAsset?{ 87 | if self.count > index{ 88 | var asset: PHAsset? 89 | self.enumerateObjectsAtIndexes(NSIndexSet(index: index), options: NSEnumerationOptions.Concurrent, usingBlock: { 90 | (assetItem, _, _) in 91 | 92 | asset = assetItem as? PHAsset 93 | }) 94 | 95 | return asset 96 | } 97 | 98 | return nil 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Demo/SDECollectionViewAlbumTransition/SDEAdaptiveAssetCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SDEAdaptiveAssetCell.swift 3 | // Albums 4 | // 5 | // Created by seedante on 15/7/8. 6 | // Copyright © 2015年 seedante. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Photos 11 | 12 | class SDEAdaptiveAssetCell: UICollectionViewCell { 13 | 14 | @IBOutlet weak var imageView: UIImageView! 15 | @IBOutlet weak var videoLogo: UIImageView!//(20, 20) 16 | @IBOutlet weak var durationLabel: UILabel!//(50, 14) 17 | 18 | var asset: PHAsset?{ 19 | willSet{ 20 | durationLabel.text = newValue?.duration.humanOutput 21 | adjustLogoAndDurationLocation() 22 | } 23 | } 24 | 25 | func adjustLogoAndDurationLocation(){ 26 | dispatch_async(dispatch_get_main_queue()){ 27 | let cellWidth = self.frame.size.width 28 | let cellHeight = self.frame.size.height 29 | let imageWidth = self.imageView.image!.size.width 30 | let imageHeight = self.imageView.image!.size.height 31 | 32 | if self.imageView.image?.size.width > self.imageView.image?.size.height{ 33 | let resizedImageHeight = (cellWidth / imageWidth) * imageHeight 34 | let imageBottonHeight = cellHeight / 2.0 + resizedImageHeight / 2.0 35 | self.videoLogo.center = CGPointMake(15.0, imageBottonHeight - 15) 36 | self.durationLabel.center = CGPointMake(cellWidth - 30, imageBottonHeight - 15) 37 | }else{ 38 | let resizedImageWidth = (cellHeight / imageHeight) * imageWidth 39 | let imageLeftX = (cellWidth - resizedImageWidth) / 2.0 40 | let imageRightX = cellWidth - imageLeftX 41 | self.videoLogo.center = CGPointMake(imageLeftX + 15, cellHeight - 15) 42 | self.durationLabel.center = CGPointMake(imageRightX - 30, cellHeight - 15) 43 | } 44 | } 45 | 46 | 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Demo/SDECollectionViewAlbumTransition/SDEAlbumViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SDEAlbumViewController.swift 3 | // Albums 4 | // 5 | // Created by seedante on 15/7/8. 6 | // Copyright © 2015年 seedante. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Photos 11 | 12 | private let ImageIdentifier = "ImageCell" 13 | private let VideoIdentifier = "VideoCell" 14 | 15 | class SDEAlbumViewController: UICollectionViewController { 16 | 17 | var assetCollection:PHAssetCollection?{ 18 | willSet{ 19 | self.navigationItem.title = newValue!.localizedTitle 20 | let fetchOptions = PHFetchOptions() 21 | fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)] 22 | fetchResult = PHAsset.fetchAssetsInAssetCollection(newValue!, options: fetchOptions) 23 | } 24 | } 25 | var fetchResult:PHFetchResult?{ 26 | didSet{ 27 | self.collectionView?.reloadData() 28 | } 29 | } 30 | 31 | override func didReceiveMemoryWarning() { 32 | super.didReceiveMemoryWarning() 33 | } 34 | 35 | // MARK: UICollectionViewDataSource 36 | override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 37 | if fetchResult != nil{ 38 | return fetchResult!.count 39 | } 40 | return 0 41 | } 42 | 43 | override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { 44 | var identifier = ImageIdentifier 45 | let asset = fetchResult?.getAssetAtIndex(indexPath.row) 46 | switch asset!.mediaType{ 47 | case .Image: 48 | identifier = ImageIdentifier 49 | case .Video: 50 | identifier = VideoIdentifier 51 | default: 52 | identifier = ImageIdentifier 53 | } 54 | 55 | let cell = collectionView.dequeueReusableCellWithReuseIdentifier(identifier, forIndexPath: indexPath) 56 | 57 | if let imageView = cell.viewWithTag(-10) as? UIImageView{ 58 | fetchResult?.fetchImageAtIndex(indexPath.row, imageView: imageView, targetSize: CGSizeMake(150, 150)) 59 | } 60 | 61 | if let timeLabel = cell.viewWithTag(-30) as? UILabel{ 62 | timeLabel.text = asset!.duration.humanOutput 63 | } 64 | 65 | if identifier == VideoIdentifier{ 66 | let adaptiveCell = cell as! SDEAdaptiveAssetCell 67 | adaptiveCell.adjustLogoAndDurationLocation() 68 | } 69 | 70 | return cell 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Demo/SDECollectionViewAlbumTransition/SDEGalleriesViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SDEGalleriesViewController.swift 3 | // Albums 4 | // 5 | // Created by seedante on 15/7/6. 6 | // Copyright © 2015年 seedante. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Foundation 11 | import Photos 12 | 13 | private let reuseIdentifier = "Cell" 14 | private let headerReuseIdentifier = "Header" 15 | 16 | class SDEGalleriesViewController: UICollectionViewController, PHPhotoLibraryChangeObserver { 17 | 18 | var dataSource = [[PHAssetCollection]]() 19 | var headerDataSource = [String]() 20 | 21 | let fetchOptions = PHFetchOptions() 22 | 23 | var localAlbumsDataSource = [PHAssetCollection]() 24 | var filteredLocalAlbumsDataSource = [PHAssetCollection]() 25 | let localSubTypes = [PHAssetCollectionSubtype](arrayLiteral: 26 | .SmartAlbumUserLibrary, 27 | .SmartAlbumVideos, 28 | .SmartAlbumSlomoVideos, 29 | .SmartAlbumTimelapses, 30 | .SmartAlbumPanoramas, 31 | .SmartAlbumGeneric, 32 | .SmartAlbumBursts 33 | ) 34 | 35 | 36 | var specialAlbumsDataSource = [PHAssetCollection]() 37 | var filteredSpecialAlbumsDataSource = [PHAssetCollection]() 38 | let specialSubTypes = [PHAssetCollectionSubtype](arrayLiteral: 39 | .SmartAlbumFavorites, 40 | .SmartAlbumAllHidden 41 | ) 42 | 43 | var syncedAlbumsDataSource = [PHAssetCollection]() 44 | var filterdSyncedAlbumsDataSource = [PHAssetCollection]() 45 | let syncedSubTypes = [PHAssetCollectionSubtype](arrayLiteral: 46 | .AlbumSyncedEvent, 47 | .AlbumSyncedAlbum, 48 | .AlbumImported 49 | ) 50 | 51 | //MARK: View Life Circle 52 | override func awakeFromNib() { 53 | fetchOptions.predicate = NSPredicate(format: "estimatedAssetCount > 0", argumentArray: nil) 54 | 55 | for subType in localSubTypes{ 56 | let fetchResult = PHAssetCollection.fetchAssetCollectionsWithType(.SmartAlbum, subtype: subType, options: nil) 57 | fetchResult.enumerateObjectsUsingBlock({ 58 | (item, index, stop) in 59 | self.localAlbumsDataSource.append(item as! PHAssetCollection) 60 | }) 61 | } 62 | 63 | 64 | for subType in specialSubTypes{ 65 | let fetchResult = PHAssetCollection.fetchAssetCollectionsWithType(.SmartAlbum, subtype: subType, options: nil) 66 | fetchResult.enumerateObjectsUsingBlock({ 67 | (item, index, stop) in 68 | self.specialAlbumsDataSource.append(item as! PHAssetCollection) 69 | }) 70 | } 71 | 72 | for subType in syncedSubTypes{ 73 | let fetchResult = PHAssetCollection.fetchAssetCollectionsWithType(.Album, subtype: subType, options: nil) 74 | fetchResult.enumerateObjectsUsingBlock({ 75 | (item, index, stop) in 76 | self.syncedAlbumsDataSource.append(item as! PHAssetCollection) 77 | }) 78 | } 79 | 80 | fetchData() 81 | } 82 | 83 | override func viewDidLoad() { 84 | super.viewDidLoad() 85 | PHPhotoLibrary.sharedPhotoLibrary().registerChangeObserver(self) 86 | } 87 | 88 | override func viewWillAppear(animated: Bool) { 89 | super.viewWillAppear(animated) 90 | self.tabBarController?.navigationItem.title = "Galleries" 91 | } 92 | 93 | override func viewDidAppear(animated: Bool) { 94 | super.viewDidAppear(animated) 95 | 96 | } 97 | 98 | override func didReceiveMemoryWarning() { 99 | super.didReceiveMemoryWarning() 100 | // Dispose of any resources that can be recreated. 101 | } 102 | 103 | deinit{ 104 | PHPhotoLibrary.sharedPhotoLibrary().unregisterChangeObserver(self) 105 | } 106 | 107 | func fetchData(){ 108 | if headerDataSource.count > 0{ 109 | headerDataSource.removeAll() 110 | } 111 | 112 | if dataSource.count > 0{ 113 | dataSource.removeAll() 114 | } 115 | 116 | 117 | filteredLocalAlbumsDataSource = localAlbumsDataSource.filter({PHAsset.fetchAssetsInAssetCollection($0, options: nil).count > 0}) 118 | filteredSpecialAlbumsDataSource = specialAlbumsDataSource.filter({PHAsset.fetchAssetsInAssetCollection($0, options: nil).count > 0}) 119 | filterdSyncedAlbumsDataSource = syncedAlbumsDataSource.filter({$0.estimatedAssetCount > 0}) 120 | 121 | if filteredLocalAlbumsDataSource.count > 0{ 122 | headerDataSource.append("Albums") 123 | dataSource.append(filteredLocalAlbumsDataSource) 124 | } 125 | 126 | if filteredSpecialAlbumsDataSource.count > 0{ 127 | headerDataSource.append("SpecialAlbums") 128 | dataSource.append(filteredSpecialAlbumsDataSource) 129 | } 130 | 131 | if filterdSyncedAlbumsDataSource.count > 0{ 132 | headerDataSource.append("SyncedAlbums") 133 | dataSource.append(filterdSyncedAlbumsDataSource) 134 | } 135 | } 136 | 137 | // MARK: UICollectionViewDataSource 138 | override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int { 139 | return dataSource.count 140 | } 141 | 142 | 143 | override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 144 | let sectionInfo = dataSource[section] 145 | return sectionInfo.count 146 | } 147 | 148 | override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { 149 | let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) 150 | // Configure the cell 151 | if let imageView = cell.viewWithTag(-10) as? UIImageView{ 152 | imageView.layer.borderColor = UIColor.whiteColor().CGColor 153 | imageView.layer.borderWidth = 10.0 154 | 155 | let assetCollectionArray = dataSource[indexPath.section] 156 | let assetCollection = assetCollectionArray[indexPath.row] 157 | if let titleLabel = cell.viewWithTag(-20) as? UILabel{ 158 | let titleText = NSAttributedString(string: assetCollection.localizedTitle!) 159 | let count = PHAsset.fetchAssetsInAssetCollection(assetCollection, options: nil).count 160 | let countText = NSAttributedString(string: " \(count)", attributes: [NSForegroundColorAttributeName: UIColor.grayColor(), NSFontAttributeName: UIFont(name: "Helvetica Neue", size: 15.0)!]) 161 | let cellTitle = NSMutableAttributedString(attributedString: titleText) 162 | cellTitle.appendAttributedString(countText) 163 | titleLabel.attributedText = cellTitle 164 | } 165 | 166 | PHFetchResult.fetchPosterImageForAssetCollection(assetCollection, imageView: imageView, targetSize: CGSizeMake(170, 170)) 167 | } 168 | return cell 169 | } 170 | 171 | override func collectionView(collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionReusableView { 172 | let headerView = collectionView.dequeueReusableSupplementaryViewOfKind(UICollectionElementKindSectionHeader, withReuseIdentifier: headerReuseIdentifier, forIndexPath: indexPath) 173 | if let titleLabel = headerView.viewWithTag(-10) as? UILabel{ 174 | titleLabel.text = headerDataSource[indexPath.section] 175 | } 176 | return headerView 177 | } 178 | 179 | //MARK: PHPhotoLibraryChangeObserver 180 | func photoLibraryDidChange(changeInstance: PHChange) { 181 | let localAlbumsCopy = localAlbumsDataSource 182 | for assetCollection in localAlbumsCopy{ 183 | if let changeDetail = changeInstance.changeDetailsForObject(assetCollection){ 184 | let index = localAlbumsDataSource.indexOf(assetCollection) 185 | let newAssetCollection = changeDetail.objectAfterChanges as! PHAssetCollection 186 | localAlbumsDataSource[index!] = newAssetCollection 187 | } 188 | 189 | } 190 | 191 | let specialAlbumsCopy = specialAlbumsDataSource 192 | for assetCollection in specialAlbumsCopy{ 193 | if let changeDetail = changeInstance.changeDetailsForObject(assetCollection){ 194 | let index = specialAlbumsDataSource.indexOf(assetCollection) 195 | let newAssetCollection = changeDetail.objectAfterChanges as! PHAssetCollection 196 | specialAlbumsDataSource[index!] = newAssetCollection 197 | } 198 | } 199 | 200 | let syncedAlbumsCopy = syncedAlbumsDataSource 201 | for assetCollection in syncedAlbumsCopy{ 202 | if let changeDetail = changeInstance.changeDetailsForObject(assetCollection){ 203 | let index = syncedAlbumsDataSource.indexOf(assetCollection) 204 | let newAssetCollection = changeDetail.objectAfterChanges as! PHAssetCollection 205 | syncedAlbumsDataSource[index!] = newAssetCollection 206 | } 207 | } 208 | 209 | fetchData() 210 | } 211 | 212 | //MARK: UICollectionView Delegate 213 | override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) { 214 | self.selectedIndexPath = indexPath 215 | self.collectionView?.allowsSelection = false 216 | 217 | let layoutAttributes = self.collectionView!.layoutAttributesForItemAtIndexPath(indexPath) 218 | 219 | let circleView = UIView(frame: CGRectMake(0, 0, 30.0, 30.0)) 220 | circleView.layer.cornerRadius = 15.0 221 | circleView.backgroundColor = UIColor.blueColor() 222 | self.collectionView?.addSubview(circleView) 223 | circleView.center = layoutAttributes!.center 224 | 225 | UIView.animateKeyframesWithDuration(0.3, delay: 0, options: UIViewKeyframeAnimationOptions.AllowUserInteraction, animations: { 226 | circleView.transform = CGAffineTransformMakeScale(2, 2) 227 | circleView.alpha = 0 228 | }, completion: { 229 | finish in 230 | 231 | circleView.removeFromSuperview() 232 | if let albumVC = self.storyboard?.instantiateViewControllerWithIdentifier("AlbumVC") as? SDEAlbumViewController{ 233 | let assetCollection = self.dataSource[indexPath.section][indexPath.row] 234 | albumVC.assetCollection = assetCollection 235 | self.navigationController?.pushViewController(albumVC, animated: true) 236 | self.collectionView?.allowsSelection = true 237 | } 238 | }) 239 | } 240 | 241 | } 242 | -------------------------------------------------------------------------------- /Demo/SDECollectionViewAlbumTransition/video_call.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seedante/SDECollectionViewAlbumTransition/309a25c69c08b1df7485514ae45bf1bf883b7a90/Demo/SDECollectionViewAlbumTransition/video_call.png -------------------------------------------------------------------------------- /Demo/SDECollectionViewAlbumTransition/video_call@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seedante/SDECollectionViewAlbumTransition/309a25c69c08b1df7485514ae45bf1bf883b7a90/Demo/SDECollectionViewAlbumTransition/video_call@2x.png -------------------------------------------------------------------------------- /Figures/AlbumTransition.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seedante/SDECollectionViewAlbumTransition/309a25c69c08b1df7485514ae45bf1bf883b7a90/Figures/AlbumTransition.gif -------------------------------------------------------------------------------- /Figures/Config1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seedante/SDECollectionViewAlbumTransition/309a25c69c08b1df7485514ae45bf1bf883b7a90/Figures/Config1.png -------------------------------------------------------------------------------- /Figures/Config2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seedante/SDECollectionViewAlbumTransition/309a25c69c08b1df7485514ae45bf1bf883b7a90/Figures/Config2.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SDECollectionViewAlbumTransition 2 | UICollectionViewController Transition like open and close an album. Blog for this: [Part I](http://www.jianshu.com/p/7a35ee30e90c), [Part II](http://www.jianshu.com/p/2cdf0729934f) 3 | 4 | ![AlbumTransition](https://github.com/seedante/SDECollectionViewAlbumTransition/blob/PinchPopTransition/Figures/AlbumTransition.gif) 5 | 6 | 7 | ## Installtion 添加到你的工程 8 | 9 | Drag files in "Classes" folder into your project. 10 | 11 | 将 Classes 里的文件拖到你的工程中即可。 12 | 13 | ## Usage 使用方法 14 | 15 | - In your storyboard, drag a Object to your navigation controller, and set its custom class to "SDENavigationControllerDelegate" 16 | - 在你的 storyboard 里,拖一个 Object 到你的 navigation controller 上面,并将这个 Object 的类设置为「SDENavigationControllerDelegate」。 17 | 18 | ![drag an Object and set the custom class](https://github.com/seedante/SDECollectionViewAlbumTransition/blob/PinchPopTransition/Figures/Config1.png) 19 | 20 | - Set this object to be your navigation controller's delegate. 21 | - 将这个 Object 设置为 navigation controller 的 delegate。 22 | 23 | ![set the delegate](https://github.com/seedante/SDECollectionViewAlbumTransition/blob/PinchPopTransition/Figures/Config2.png) 24 | 25 | 26 | - At the last, add the follow codes in your UICollectionView's delegate: 27 | - 最后要做的一件事是,在下面的方法中添加几行行代码: 28 | 29 | override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) { 30 | self.selectedIndexPath = indexPath 31 | self.collectionView?.allowsSelection = false //disable interaction to prevent the issue of double tap on cell 32 | ... 33 | self.navigationController?.pushViewController(toVC, animated: true) 34 | self.collectionView?.allowsSelection = true //enable again after push. 35 | } 36 | 37 | **Now the collectionView supports to pinch to pop.** 38 | 39 | **现在当前的这个 collectionView 已经支持使用 pinch 手势来执行 pop 了。** 40 | 41 | 42 | ## Requirements 使用条件 43 | 44 | - iOS 8.0+ 45 | - Swift 2.0 46 | 47 | Thanks for [@CezaryKopacz](https://github.com/CezaryKopacz/CKWaveCollectionViewTransition), [@ColinEberhardt](https://github.com/ColinEberhardt/VCTransitionsLibrary). I learn a lot from their repo. 48 | And thanks for [ Vincent Ngo](http://www.raywenderlich.com/94565/how-to-create-an-ios-book-open-animation-part-1) very much, I find the key to resolve the problem of pinching to push. 49 | There is a little problem with time delta algorithm, I will update if I find the way to improvement. 50 | 51 | ## Pinch to push? 使用 Pinch 手势支持 push? 52 | 53 | Pinching to push is a little complex. ViewController to push must be created before push, and init a ViewController is complex than do something to a existed ViewController, so this is why most of libraries do not support pinch to push. 54 | 55 | 使用 pinch 手势来支持 push 操作有点麻烦。在 push 一个视图控制器前必然要生成一个实例,但这个类别是未知的,需要你来决定,这就是为什么很多库只支持 pinch push 的原因,pop 前视图控制器已经存在了,就不存在这个困扰。 56 | 57 | If you want to use pinch to support push and pop both, switch to the branch "Pinch-Push-Pop-Transition", drag files in "Classes" folder in this branch into your project, 58 | there is a little difference between "Pinch-Push-Pop-Transition" branch with other branches. 59 | 60 | 如果你想使用 pinch 手势同时支持 push 和 pop 操作,那么当前的分支的模式就不能用了,请切换到「Pinch-Push-Pop-Transition」分支。同样地,将 Classes 文件夹里的文件拖到你的工程里。 61 | 62 | Add the below properties to your UICollectionViewController child class: 63 | 64 | 在你的 UICollectionViewController 子类添加以下属性和方法: 65 | 66 | var transitionDelegate: SDENavigationControllerDelegate? 67 | var pinchGestureRecognizer: UIPinchGestureRecognizer?{ 68 | didSet(newValue){ 69 | collectionView?.addGestureRecognizer(pinchGestureRecognizer!) 70 | } 71 | } 72 | 73 | override viewDidLoad(){ 74 | ... 75 | pinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: "handlePinch:") 76 | } 77 | 78 | deinit{ 79 | if pinchGestureRecognizer != nil{ 80 | collectionView?.removeGestureRecognizer(pinchGestureRecognizer!) 81 | } 82 | } 83 | 84 | //MARK: Pinch Push and Pop 85 | func getIndexPathForGesture(gesture: UIPinchGestureRecognizer) -> NSIndexPath?{ 86 | let location0 = gesture.locationOfTouch(0, inView: gesture.view) 87 | let location1 = gesture.locationOfTouch(1, inView: gesture.view) 88 | let middleLocation = CGPointMake((location0.x + location1.x)/2, (location0.y + location1.y)/2) 89 | let indexPath = collectionView?.indexPathForItemAtPoint(middleLocation) 90 | return indexPath 91 | } 92 | 93 | func handlePinch(gesture: UIPinchGestureRecognizer){ 94 | switch gesture.state{ 95 | case .Began: 96 | if gesture.scale >= 1.0{ 97 | guard let indexPath = getIndexPathForGesture(gesture) else{ 98 | return 99 | } 100 | 101 | self.selectedIndexPath = indexPath 102 | 103 | if let toVC = ...{ 104 | transitionDelegate = navigationController?.delegate as? SDENavigationControllerDelegate 105 | transitionDelegate?.interactive = true 106 | navigationController?.pushViewController(toVC, animated: true) 107 | } 108 | 109 | }else{ 110 | //after view controller is poped, UIViewController.navigationController is nil. So you need to keep it somewhere before pop 111 | transitionDelegate = self.navigationController?.delegate as? SDENavigationControllerDelegate 112 | transitionDelegate?.interactive = true 113 | self.navigationController?.popViewControllerAnimated(true) 114 | } 115 | 116 | case .Changed: 117 | guard transitionDelegate != nil else{ 118 | return 119 | } 120 | guard let interactionController = transitionDelegate?.interactionController else{ 121 | return 122 | } 123 | 124 | var progress = gesture.scale 125 | if transitionDelegate!.isPush{ 126 | progress = gesture.scale - 1.0 >= 0.9 ? 0.9 : gesture.scale - 1.0 127 | }else{ 128 | progress = 1.0 - gesture.scale 129 | } 130 | 131 | interactionController.updateInteractiveTransition(progress) 132 | case .Ended, .Cancelled: 133 | guard transitionDelegate != nil else{ 134 | return 135 | } 136 | guard let interactionController = transitionDelegate?.interactionController else{ 137 | return 138 | } 139 | 140 | var progress = gesture.scale 141 | if transitionDelegate!.isPush{ 142 | progress = gesture.scale - 1.0 >= 0.9 ? 0.9 : gesture.scale - 1.0 143 | }else{ 144 | progress = 1.0 - gesture.scale 145 | } 146 | 147 | if progress >= 0.4{ 148 | interactionController.finishInteractiveTransition() 149 | }else{ 150 | interactionController.cancelInteractiveTransition() 151 | } 152 | transitionDelegate?.interactive = false 153 | default: 154 | guard transitionDelegate != nil else{ 155 | return 156 | } 157 | transitionDelegate?.interactive = false 158 | } 159 | } 160 | --------------------------------------------------------------------------------