@@ -10,34 +10,34 @@ import UIKit
10
10
import AVFoundation
11
11
12
12
/*
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
+ */
41
41
42
42
public final class ImageViewer : UIViewController , UIScrollViewDelegate , UIViewControllerTransitioningDelegate {
43
43
@@ -70,8 +70,8 @@ public final class ImageViewer: UIViewController, UIScrollViewDelegate, UIViewCo
70
70
private let showCloseButtonDuration = 0.2
71
71
private let hideCloseButtonDuration = 0.05
72
72
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
75
75
/// TRANSITIONS
76
76
private let presentTransition : ImageViewerPresentTransition
77
77
private let dismissTransition : ImageViewerDismissTransition
@@ -117,7 +117,7 @@ public final class ImageViewer: UIViewController, UIScrollViewDelegate, UIViewCo
117
117
self . swipeToDismissTransition = ImageViewerSwipeToDismissTransition ( )
118
118
119
119
super. init ( nibName: nil , bundle: nil )
120
-
120
+
121
121
transitioningDelegate = self
122
122
modalPresentationStyle = . Custom
123
123
extendedLayoutIncludesOpaqueBars = true
@@ -182,7 +182,7 @@ public final class ImageViewer: UIViewController, UIScrollViewDelegate, UIViewCo
182
182
183
183
let originX = - view. bounds. width
184
184
let originY = - view. bounds. height
185
-
185
+
186
186
let width = view. bounds. width * 4
187
187
let height = view. bounds. height * 4
188
188
@@ -202,10 +202,10 @@ public final class ImageViewer: UIViewController, UIScrollViewDelegate, UIViewCo
202
202
scrollView = UIScrollView ( frame: CGRectZero)
203
203
overlayView = UIView ( frame: CGRectZero)
204
204
closeButton = UIButton ( frame: CGRectZero)
205
-
205
+
206
206
scrollView. autoresizingMask = [ . FlexibleWidth, . FlexibleHeight]
207
207
overlayView. autoresizingMask = [ . None]
208
-
208
+
209
209
view. addSubview ( overlayView)
210
210
view. addSubview ( scrollView)
211
211
view. addSubview ( closeButton)
@@ -216,12 +216,12 @@ public final class ImageViewer: UIViewController, UIScrollViewDelegate, UIViewCo
216
216
217
217
public override func viewDidLoad( ) {
218
218
super. viewDidLoad ( )
219
-
219
+
220
220
configureCloseButton ( )
221
221
configureImageView ( )
222
222
configureScrollView ( )
223
223
}
224
-
224
+
225
225
// MARK: - Transitioning Delegate
226
226
227
227
public func animationControllerForPresentedController( presented: UIViewController , presentingController presenting: UIViewController , sourceController source: UIViewController ) -> UIViewControllerAnimatedTransitioning ? {
@@ -257,12 +257,12 @@ public final class ImageViewer: UIViewController, UIScrollViewDelegate, UIViewCo
257
257
self . scrollView. contentSize = self . imageView. bounds. size
258
258
self . scrollView. setZoomScale ( 1.0 , animated: false )
259
259
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
+ }
266
266
}
267
267
}
268
268
@@ -286,27 +286,27 @@ public final class ImageViewer: UIViewController, UIScrollViewDelegate, UIViewCo
286
286
self . imageView. center = rotationAdjustedCenter ( self . view)
287
287
self . scrollView. contentSize = self . imageView. bounds. size
288
288
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
291
297
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
309
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
309
+ }
310
310
}
311
311
}
312
312
@@ -327,18 +327,18 @@ public final class ImageViewer: UIViewController, UIScrollViewDelegate, UIViewCo
327
327
self . view. bounds = ( self . applicationWindow? . bounds) !
328
328
self . imageView. frame = CGRectIntegral ( self . applicationWindow!. convertRect ( self . displacedView. bounds, fromView: self . displacedView) )
329
329
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
+ }
342
342
}
343
343
}
344
344
@@ -428,39 +428,49 @@ public final class ImageViewer: UIViewController, UIScrollViewDelegate, UIViewCo
428
428
isSwipingToDismiss = true
429
429
dynamicTransparencyActive = true
430
430
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
434
434
435
435
switch recognizer. state {
436
436
437
437
case . Began:
438
438
applicationWindow!. windowLevel = UIWindowLevelNormal
439
439
fallthrough
440
+
440
441
case . Changed:
441
- scrollView. setContentOffset ( CGPoint ( x: 0 , y: - latestTouchPoint . y) , animated: false )
442
+ scrollView. setContentOffset ( CGPoint ( x: 0 , y: - lastTouchPoint . y) , animated: false )
442
443
443
444
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)
458
446
459
447
default :
460
448
break
461
449
}
462
450
}
463
451
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
+
464
474
public func viewForZoomingInScrollView( scrollView: UIScrollView ) -> UIView ? {
465
475
return imageView
466
476
}
0 commit comments