Skip to content

Commit 6ed5102

Browse files
author
Kristian Angyal
committed
Fixes github issue #26 with swipe to dismiss to bottom not finishing.
1 parent 57a3c3d commit 6ed5102

File tree

1 file changed

+101
-91
lines changed

1 file changed

+101
-91
lines changed

ImageViewer/Source/ImageViewer/ImageViewer.swift

+101-91
Original file line numberDiff line numberDiff line change
@@ -10,34 +10,34 @@ import UIKit
1010
import AVFoundation
1111

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

4242
public final class ImageViewer: UIViewController, UIScrollViewDelegate, UIViewControllerTransitioningDelegate {
4343

@@ -70,8 +70,8 @@ public final class ImageViewer: UIViewController, UIScrollViewDelegate, UIViewCo
7070
private let showCloseButtonDuration = 0.2
7171
private let hideCloseButtonDuration = 0.05
7272
private let zoomDuration = 0.2
73-
private let thresholdVelocity: CGFloat = 1000 // It works as a threshold.
74-
73+
private let thresholdVelocity: CGFloat = 1000 // Based on UX experiments
74+
private let cutOffVelocity: CGFloat = 1000000 // we need some sufficiently large number, nobody can swipe faster then that
7575
/// TRANSITIONS
7676
private let presentTransition: ImageViewerPresentTransition
7777
private let dismissTransition: ImageViewerDismissTransition
@@ -117,7 +117,7 @@ public final class ImageViewer: UIViewController, UIScrollViewDelegate, UIViewCo
117117
self.swipeToDismissTransition = ImageViewerSwipeToDismissTransition()
118118

119119
super.init(nibName: nil, bundle: nil)
120-
120+
121121
transitioningDelegate = self
122122
modalPresentationStyle = .Custom
123123
extendedLayoutIncludesOpaqueBars = true
@@ -182,7 +182,7 @@ public final class ImageViewer: UIViewController, UIScrollViewDelegate, UIViewCo
182182

183183
let originX = -view.bounds.width
184184
let originY = -view.bounds.height
185-
185+
186186
let width = view.bounds.width * 4
187187
let height = view.bounds.height * 4
188188

@@ -202,10 +202,10 @@ public final class ImageViewer: UIViewController, UIScrollViewDelegate, UIViewCo
202202
scrollView = UIScrollView(frame: CGRectZero)
203203
overlayView = UIView(frame: CGRectZero)
204204
closeButton = UIButton(frame: CGRectZero)
205-
205+
206206
scrollView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
207207
overlayView.autoresizingMask = [.None]
208-
208+
209209
view.addSubview(overlayView)
210210
view.addSubview(scrollView)
211211
view.addSubview(closeButton)
@@ -216,12 +216,12 @@ public final class ImageViewer: UIViewController, UIScrollViewDelegate, UIViewCo
216216

217217
public override func viewDidLoad() {
218218
super.viewDidLoad()
219-
219+
220220
configureCloseButton()
221221
configureImageView()
222222
configureScrollView()
223223
}
224-
224+
225225
// MARK: - Transitioning Delegate
226226

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

260-
}) { (finished) -> Void in
261-
if (finished) {
262-
self.isAnimating = false
263-
self.scrollView.maximumZoomScale = maximumZoomScale(forBoundingSize: rotationAdjustedBounds().size, contentSize: self.imageView.bounds.size)
264-
UIView.animateWithDuration(self.showCloseButtonDuration, animations: { self.closeButton.alpha = 1.0 })
265-
}
260+
}) { (finished) -> Void in
261+
if (finished) {
262+
self.isAnimating = false
263+
self.scrollView.maximumZoomScale = maximumZoomScale(forBoundingSize: rotationAdjustedBounds().size, contentSize: self.imageView.bounds.size)
264+
UIView.animateWithDuration(self.showCloseButtonDuration, animations: { self.closeButton.alpha = 1.0 })
265+
}
266266
}
267267
}
268268

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

289-
}) { (finished) -> Void in
290-
completion?(finished)
289+
}) { (finished) -> Void in
290+
completion?(finished)
291+
292+
if finished {
293+
if isPortraitOnly() {
294+
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ImageViewer.rotate), name: UIDeviceOrientationDidChangeNotification, object: nil)
295+
}
296+
self.applicationWindow!.windowLevel = UIWindowLevelStatusBar + 1
291297

292-
if finished {
293-
if isPortraitOnly() {
294-
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ImageViewer.rotate), name: UIDeviceOrientationDidChangeNotification, object: nil)
295-
}
296-
self.applicationWindow!.windowLevel = UIWindowLevelStatusBar + 1
297-
298-
self.scrollView.addSubview(self.imageView)
299-
self.imageProvider.provideImage { [weak self] image in
300-
self?.imageView.image = image
301-
}
302-
303-
self.isAnimating = false
304-
self.scrollView.maximumZoomScale = maximumZoomScale(forBoundingSize: rotationAdjustedBounds().size, contentSize: self.imageView.bounds.size)
305-
UIView.animateWithDuration(self.showCloseButtonDuration, animations: { self.closeButton.alpha = 1.0 })
306-
self.configureGestureRecognizers()
307-
self.showCompletionBlock?()
308-
self.displacedView.hidden = false
298+
self.scrollView.addSubview(self.imageView)
299+
self.imageProvider.provideImage { [weak self] image in
300+
self?.imageView.image = image
309301
}
302+
303+
self.isAnimating = false
304+
self.scrollView.maximumZoomScale = maximumZoomScale(forBoundingSize: rotationAdjustedBounds().size, contentSize: self.imageView.bounds.size)
305+
UIView.animateWithDuration(self.showCloseButtonDuration, animations: { self.closeButton.alpha = 1.0 })
306+
self.configureGestureRecognizers()
307+
self.showCompletionBlock?()
308+
self.displacedView.hidden = false
309+
}
310310
}
311311
}
312312

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

330-
}) { (finished) -> Void in
331-
completion?(finished)
332-
if finished {
333-
NSNotificationCenter.defaultCenter().removeObserver(self)
334-
self.applicationWindow!.windowLevel = UIWindowLevelNormal
335-
336-
self.displacedView.hidden = false
337-
self.isAnimating = false
338-
339-
self.closeButtonActionCompletionBlock?()
340-
self.dismissCompletionBlock?()
341-
}
330+
}) { (finished) -> Void in
331+
completion?(finished)
332+
if finished {
333+
NSNotificationCenter.defaultCenter().removeObserver(self)
334+
self.applicationWindow!.windowLevel = UIWindowLevelNormal
335+
336+
self.displacedView.hidden = false
337+
self.isAnimating = false
338+
339+
self.closeButtonActionCompletionBlock?()
340+
self.dismissCompletionBlock?()
341+
}
342342
}
343343
}
344344

@@ -428,39 +428,49 @@ public final class ImageViewer: UIViewController, UIScrollViewDelegate, UIViewCo
428428
isSwipingToDismiss = true
429429
dynamicTransparencyActive = true
430430

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

435435
switch recognizer.state {
436436

437437
case .Began:
438438
applicationWindow!.windowLevel = UIWindowLevelNormal
439439
fallthrough
440+
440441
case .Changed:
441-
scrollView.setContentOffset(CGPoint(x: 0, y: -latestTouchPoint.y), animated: false)
442+
scrollView.setContentOffset(CGPoint(x: 0, y: -lastTouchPoint.y), animated: false)
442443

443444
case .Ended:
444-
445-
/// In points per second
446-
let verticalVelocity = recognizer.velocityInView(view).y
447-
448-
if verticalVelocity < -thresholdVelocity {
449-
swipeToDismissTransition.setParameters(latestTouchPoint.y, targetOffset: targetOffsetToReachTop, verticalVelocity: verticalVelocity)
450-
presentingViewController?.dismissViewControllerAnimated(true, completion: nil)
451-
}
452-
else if verticalVelocity >= -thresholdVelocity && verticalVelocity <= thresholdVelocity {
453-
swipeToDismissCanceledAnimation()
454-
}
455-
else {
456-
swipeToDismissTransition.setParameters(latestTouchPoint.y, targetOffset: targetOffsetToReachBottom, verticalVelocity: verticalVelocity)
457-
}
445+
handleSwipeToDismissEnded(verticalVelocity, lastTouchPoint: lastTouchPoint, targetOffset: targetOffsetToReachEdge)
458446

459447
default:
460448
break
461449
}
462450
}
463451

452+
func handleSwipeToDismissEnded(verticalVelocity: CGFloat, lastTouchPoint: CGPoint, targetOffset: CGFloat) {
453+
454+
let velocity = abs(verticalVelocity)
455+
456+
switch velocity {
457+
458+
case 0 ..< thresholdVelocity:
459+
460+
swipeToDismissCanceledAnimation()
461+
462+
case thresholdVelocity ... cutOffVelocity:
463+
464+
let offset = (verticalVelocity > 0) ? -targetOffset : targetOffset
465+
let touchPoint = (verticalVelocity > 0) ? -lastTouchPoint.y : lastTouchPoint.y
466+
467+
swipeToDismissTransition.setParameters(touchPoint, targetOffset: offset, verticalVelocity: verticalVelocity)
468+
presentingViewController?.dismissViewControllerAnimated(true, completion: nil)
469+
470+
default: break
471+
}
472+
}
473+
464474
public func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {
465475
return imageView
466476
}

0 commit comments

Comments
 (0)