Skip to content

Commit

Permalink
Fixes github issue #26 with swipe to dismiss to bottom not finishing.
Browse files Browse the repository at this point in the history
  • Loading branch information
Kristian Angyal committed Apr 13, 2016
1 parent 57a3c3d commit 6ed5102
Showing 1 changed file with 101 additions and 91 deletions.
192 changes: 101 additions & 91 deletions ImageViewer/Source/ImageViewer/ImageViewer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,34 +10,34 @@ import UIKit
import AVFoundation

/*

Features:

- double tap to toggle betweeen Aspect Fit & Aspect Fill zoom factor
- manual pinch to zoom up to approx. 4x the size of full-sized image
- rotation support
- swipe to dismiss
- initiation and completion blocks to support a case where the original image node should be hidden or unhidden alongside show and dismiss animations

Usage:

- Initialize ImageViewer, set optional initiation and completion blocks, and present by calling "presentImageViewer".

How it works:

- Gets presented modally via convenience UIViewControler extension, using custom modal presentation that is enforced internally.
- Displays itself in full screen (nothing is visible at that point, but it's there, trust me...)
- Makes a screenshot of the displaced view that can be any UIView (or subclass) really, but UIImageView is the most probable choice.
- Puts this screenshot into a new UIImageView and matches its position and size to the displaced view.
- Sets the target size and position for the UIImageView to aspectFit size and centered while kicking in the black overlay.
- Animates the image view into the scroll view (that serves as zooming canvas) and reaches final position and size.
- Immediately tries to get a full-sized version of the image from imageProvider.
- If successful, replaces the screenshot in the image view with probably a higher-res image.
- Gets dismissed either via Close button, or via "swipe up/down" gesture.
- While being "closed", image is animated back to it's "original" position which is a rect that matches to the displaced view's position
which overall gives us the illusion of the UI element returning to its original place.

*/
Features:
- double tap to toggle betweeen Aspect Fit & Aspect Fill zoom factor
- manual pinch to zoom up to approx. 4x the size of full-sized image
- rotation support
- swipe to dismiss
- initiation and completion blocks to support a case where the original image node should be hidden or unhidden alongside show and dismiss animations
Usage:
- Initialize ImageViewer, set optional initiation and completion blocks, and present by calling "presentImageViewer".
How it works:
- Gets presented modally via convenience UIViewControler extension, using custom modal presentation that is enforced internally.
- Displays itself in full screen (nothing is visible at that point, but it's there, trust me...)
- Makes a screenshot of the displaced view that can be any UIView (or subclass) really, but UIImageView is the most probable choice.
- Puts this screenshot into a new UIImageView and matches its position and size to the displaced view.
- Sets the target size and position for the UIImageView to aspectFit size and centered while kicking in the black overlay.
- Animates the image view into the scroll view (that serves as zooming canvas) and reaches final position and size.
- Immediately tries to get a full-sized version of the image from imageProvider.
- If successful, replaces the screenshot in the image view with probably a higher-res image.
- Gets dismissed either via Close button, or via "swipe up/down" gesture.
- While being "closed", image is animated back to it's "original" position which is a rect that matches to the displaced view's position
which overall gives us the illusion of the UI element returning to its original place.
*/

public final class ImageViewer: UIViewController, UIScrollViewDelegate, UIViewControllerTransitioningDelegate {

Expand Down Expand Up @@ -70,8 +70,8 @@ public final class ImageViewer: UIViewController, UIScrollViewDelegate, UIViewCo
private let showCloseButtonDuration = 0.2
private let hideCloseButtonDuration = 0.05
private let zoomDuration = 0.2
private let thresholdVelocity: CGFloat = 1000 // It works as a threshold.

private let thresholdVelocity: CGFloat = 1000 // Based on UX experiments
private let cutOffVelocity: CGFloat = 1000000 // we need some sufficiently large number, nobody can swipe faster then that
/// TRANSITIONS
private let presentTransition: ImageViewerPresentTransition
private let dismissTransition: ImageViewerDismissTransition
Expand Down Expand Up @@ -117,7 +117,7 @@ public final class ImageViewer: UIViewController, UIScrollViewDelegate, UIViewCo
self.swipeToDismissTransition = ImageViewerSwipeToDismissTransition()

super.init(nibName: nil, bundle: nil)

transitioningDelegate = self
modalPresentationStyle = .Custom
extendedLayoutIncludesOpaqueBars = true
Expand Down Expand Up @@ -182,7 +182,7 @@ public final class ImageViewer: UIViewController, UIScrollViewDelegate, UIViewCo

let originX = -view.bounds.width
let originY = -view.bounds.height

let width = view.bounds.width * 4
let height = view.bounds.height * 4

Expand All @@ -202,10 +202,10 @@ public final class ImageViewer: UIViewController, UIScrollViewDelegate, UIViewCo
scrollView = UIScrollView(frame: CGRectZero)
overlayView = UIView(frame: CGRectZero)
closeButton = UIButton(frame: CGRectZero)

scrollView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
overlayView.autoresizingMask = [.None]

view.addSubview(overlayView)
view.addSubview(scrollView)
view.addSubview(closeButton)
Expand All @@ -216,12 +216,12 @@ public final class ImageViewer: UIViewController, UIScrollViewDelegate, UIViewCo

public override func viewDidLoad() {
super.viewDidLoad()

configureCloseButton()
configureImageView()
configureScrollView()
}

// MARK: - Transitioning Delegate

public func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
Expand Down Expand Up @@ -257,12 +257,12 @@ public final class ImageViewer: UIViewController, UIScrollViewDelegate, UIViewCo
self.scrollView.contentSize = self.imageView.bounds.size
self.scrollView.setZoomScale(1.0, animated: false)

}) { (finished) -> Void in
if (finished) {
self.isAnimating = false
self.scrollView.maximumZoomScale = maximumZoomScale(forBoundingSize: rotationAdjustedBounds().size, contentSize: self.imageView.bounds.size)
UIView.animateWithDuration(self.showCloseButtonDuration, animations: { self.closeButton.alpha = 1.0 })
}
}) { (finished) -> Void in
if (finished) {
self.isAnimating = false
self.scrollView.maximumZoomScale = maximumZoomScale(forBoundingSize: rotationAdjustedBounds().size, contentSize: self.imageView.bounds.size)
UIView.animateWithDuration(self.showCloseButtonDuration, animations: { self.closeButton.alpha = 1.0 })
}
}
}

Expand All @@ -286,27 +286,27 @@ public final class ImageViewer: UIViewController, UIScrollViewDelegate, UIViewCo
self.imageView.center = rotationAdjustedCenter(self.view)
self.scrollView.contentSize = self.imageView.bounds.size

}) { (finished) -> Void in
completion?(finished)
}) { (finished) -> Void in
completion?(finished)

if finished {
if isPortraitOnly() {
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ImageViewer.rotate), name: UIDeviceOrientationDidChangeNotification, object: nil)
}
self.applicationWindow!.windowLevel = UIWindowLevelStatusBar + 1

if finished {
if isPortraitOnly() {
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ImageViewer.rotate), name: UIDeviceOrientationDidChangeNotification, object: nil)
}
self.applicationWindow!.windowLevel = UIWindowLevelStatusBar + 1

self.scrollView.addSubview(self.imageView)
self.imageProvider.provideImage { [weak self] image in
self?.imageView.image = image
}

self.isAnimating = false
self.scrollView.maximumZoomScale = maximumZoomScale(forBoundingSize: rotationAdjustedBounds().size, contentSize: self.imageView.bounds.size)
UIView.animateWithDuration(self.showCloseButtonDuration, animations: { self.closeButton.alpha = 1.0 })
self.configureGestureRecognizers()
self.showCompletionBlock?()
self.displacedView.hidden = false
self.scrollView.addSubview(self.imageView)
self.imageProvider.provideImage { [weak self] image in
self?.imageView.image = image
}

self.isAnimating = false
self.scrollView.maximumZoomScale = maximumZoomScale(forBoundingSize: rotationAdjustedBounds().size, contentSize: self.imageView.bounds.size)
UIView.animateWithDuration(self.showCloseButtonDuration, animations: { self.closeButton.alpha = 1.0 })
self.configureGestureRecognizers()
self.showCompletionBlock?()
self.displacedView.hidden = false
}
}
}

Expand All @@ -327,18 +327,18 @@ public final class ImageViewer: UIViewController, UIScrollViewDelegate, UIViewCo
self.view.bounds = (self.applicationWindow?.bounds)!
self.imageView.frame = CGRectIntegral(self.applicationWindow!.convertRect(self.displacedView.bounds, fromView: self.displacedView))

}) { (finished) -> Void in
completion?(finished)
if finished {
NSNotificationCenter.defaultCenter().removeObserver(self)
self.applicationWindow!.windowLevel = UIWindowLevelNormal

self.displacedView.hidden = false
self.isAnimating = false
self.closeButtonActionCompletionBlock?()
self.dismissCompletionBlock?()
}
}) { (finished) -> Void in
completion?(finished)
if finished {
NSNotificationCenter.defaultCenter().removeObserver(self)
self.applicationWindow!.windowLevel = UIWindowLevelNormal
self.displacedView.hidden = false
self.isAnimating = false

self.closeButtonActionCompletionBlock?()
self.dismissCompletionBlock?()
}
}
}

Expand Down Expand Up @@ -428,39 +428,49 @@ public final class ImageViewer: UIViewController, UIScrollViewDelegate, UIViewCo
isSwipingToDismiss = true
dynamicTransparencyActive = true

let targetOffsetToReachTop = (view.bounds.height / 2) + (imageView.bounds.height / 2)
let targetOffsetToReachBottom = -targetOffsetToReachTop
let latestTouchPoint = recognizer.translationInView(view)
let targetOffsetToReachEdge = (view.bounds.height / 2) + (imageView.bounds.height / 2)
let lastTouchPoint = recognizer.translationInView(view)
let verticalVelocity = recognizer.velocityInView(view).y

switch recognizer.state {

case .Began:
applicationWindow!.windowLevel = UIWindowLevelNormal
fallthrough

case .Changed:
scrollView.setContentOffset(CGPoint(x: 0, y: -latestTouchPoint.y), animated: false)
scrollView.setContentOffset(CGPoint(x: 0, y: -lastTouchPoint.y), animated: false)

case .Ended:

/// In points per second
let verticalVelocity = recognizer.velocityInView(view).y

if verticalVelocity < -thresholdVelocity {
swipeToDismissTransition.setParameters(latestTouchPoint.y, targetOffset: targetOffsetToReachTop, verticalVelocity: verticalVelocity)
presentingViewController?.dismissViewControllerAnimated(true, completion: nil)
}
else if verticalVelocity >= -thresholdVelocity && verticalVelocity <= thresholdVelocity {
swipeToDismissCanceledAnimation()
}
else {
swipeToDismissTransition.setParameters(latestTouchPoint.y, targetOffset: targetOffsetToReachBottom, verticalVelocity: verticalVelocity)
}
handleSwipeToDismissEnded(verticalVelocity, lastTouchPoint: lastTouchPoint, targetOffset: targetOffsetToReachEdge)

default:
break
}
}

func handleSwipeToDismissEnded(verticalVelocity: CGFloat, lastTouchPoint: CGPoint, targetOffset: CGFloat) {

let velocity = abs(verticalVelocity)

switch velocity {

case 0 ..< thresholdVelocity:

swipeToDismissCanceledAnimation()

case thresholdVelocity ... cutOffVelocity:

let offset = (verticalVelocity > 0) ? -targetOffset : targetOffset
let touchPoint = (verticalVelocity > 0) ? -lastTouchPoint.y : lastTouchPoint.y

swipeToDismissTransition.setParameters(touchPoint, targetOffset: offset, verticalVelocity: verticalVelocity)
presentingViewController?.dismissViewControllerAnimated(true, completion: nil)

default: break
}
}

public func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {
return imageView
}
Expand Down

0 comments on commit 6ed5102

Please sign in to comment.