-
Notifications
You must be signed in to change notification settings - Fork 80
/
URBMediaFocusViewController.m
1395 lines (1175 loc) · 56.9 KB
/
URBMediaFocusViewController.m
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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
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
160
161
162
163
164
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
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
//
// URBMediaFocusViewController.m
// URBMediaFocusViewControllerDemo
//
// Created by Nicholas Shipes on 11/3/13.
// Copyright (c) 2013 Urban10 Interactive. All rights reserved.
//
#import <Accelerate/Accelerate.h>
#import <QuartzCore/QuartzCore.h>
#import <AssetsLibrary/AssetsLibrary.h>
#import "URBMediaFocusViewController.h"
static const CGFloat __overlayAlpha = 0.6f; // opacity of the black overlay displayed below the focused image
static const CGFloat __animationDuration = 0.18f; // the base duration for present/dismiss animations (except physics-related ones)
static const CGFloat __maximumDismissDelay = 0.5f; // maximum time of delay (in seconds) between when image view is push out and dismissal animations begin
static const CGFloat __resistance = 0.0f; // linear resistance applied to the image’s dynamic item behavior
static const CGFloat __density = 1.0f; // relative mass density applied to the image's dynamic item behavior
static const CGFloat __velocityFactor = 1.0f; // affects how quickly the view is pushed out of the view
static const CGFloat __angularVelocityFactor = 1.0f; // adjusts the amount of spin applied to the view during a push force, increases towards the view bounds
static const CGFloat __minimumVelocityRequiredForPush = 50.0f; // defines how much velocity is required for the push behavior to be applied
/* parallax options */
static const CGFloat __backgroundScale = 0.9f; // defines how much the background view should be scaled
static const CGFloat __blurRadius = 2.0f; // defines how much the background view is blurred
static const CGFloat __blurSaturationDeltaMask = 0.8f;
static const CGFloat __blurTintColorAlpha = 0.2f; // defines how much to tint the background view
@interface UIView (URBMediaFocusViewController)
- (UIImage *)urb_snapshotImageWithScale:(CGFloat)scale;
- (void)urb_snapshowImageWithScale:(CGFloat)scale completion:(void (^)(UIImage *snapshotImage))completionBlock;
@end
/**
Pulled from Apple's UIImage+ImageEffects category, but renamed to avoid potential selector name conflicts.
*/
@interface UIImage (URBImageEffects)
- (UIImage *)urb_applyBlurWithRadius:(CGFloat)blurRadius tintColor:(UIColor *)tintColor saturationDeltaFactor:(CGFloat)saturationDeltaFactor maskImage:(UIImage *)maskImage;
@end
@interface URBMediaFocusViewController () <UIScrollViewDelegate, UIActionSheetDelegate>
@property (nonatomic, strong) UIView *fromView;
@property (nonatomic, assign) CGRect fromRect;
@property (nonatomic, weak) UIViewController *targetViewController;
@property (nonatomic, strong) UIImageView *imageView;
@property (nonatomic, strong) UIScrollView *scrollView;
@property (nonatomic, strong) UIView *containerView;
@property (nonatomic, strong) UIView *backgroundView;
@property (nonatomic, strong) UIDynamicAnimator *animator;
@property (nonatomic, strong) UISnapBehavior *snapBehavior;
@property (nonatomic, strong) UIPushBehavior *pushBehavior;
@property (nonatomic, strong) UIAttachmentBehavior *panAttachmentBehavior;
@property (nonatomic, strong) UIDynamicItemBehavior *itemBehavior;
@property (nonatomic, readonly) UIWindow *keyWindow;
@property (nonatomic, strong) UIPanGestureRecognizer *panRecognizer;
@property (nonatomic, strong) UITapGestureRecognizer *doubleTapRecognizer;
@property (nonatomic, strong) UITapGestureRecognizer *tapRecognizer;
@property (nonatomic, strong) UITapGestureRecognizer *photoTapRecognizer;
@property (nonatomic, strong) UILongPressGestureRecognizer *photoLongPressRecognizer;
@property (nonatomic, strong) UIActivityIndicatorView *loadingView;
@property (nonatomic, strong) NSURLConnection *urlConnection;
@property (nonatomic, strong) NSMutableData *urlData;
@property (nonatomic, strong) UIImageView *blurredSnapshotView;
@property (nonatomic, strong) UIView *snapshotView;
@end
@implementation URBMediaFocusViewController {
CGRect _originalFrame;
CGFloat _minScale;
CGFloat _maxScale;
CGFloat _lastPinchScale;
CGFloat _lastZoomScale;
UIInterfaceOrientation _currentOrientation;
BOOL _hasLaidOut;
BOOL _unhideStatusBarOnDismiss;
BOOL _hasGeneratedBlurBackground;
}
- (void)dealloc {
self.delegate = nil;
[self.urlConnection cancel];
}
- (id)init {
self = [super init];
if (self) {
_hasLaidOut = NO;
_unhideStatusBarOnDismiss = YES;
_hasGeneratedBlurBackground = NO;
self.shouldBlurBackground = YES;
self.parallaxEnabled = YES;
self.shouldDismissOnTap = YES;
self.shouldDismissOnImageTap = NO;
self.shouldShowPhotoActions = NO;
self.shouldRotateToDeviceOrientation = YES;
self.allowSwipeOnBackgroundView = YES;
}
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
[self setup];
}
- (void)setup {
self.view.frame = self.keyWindow.bounds;
self.backgroundView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(self.keyWindow.frame), CGRectGetHeight(self.keyWindow.frame))];
self.backgroundView.backgroundColor = [UIColor colorWithWhite:0.0f alpha:__overlayAlpha];
self.backgroundView.alpha = 0.0f;
self.backgroundView.autoresizingMask = UIViewAutoresizingFlexibleHeight|UIViewAutoresizingFlexibleWidth;
[self.view addSubview:self.backgroundView];
self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
self.scrollView.backgroundColor = [UIColor clearColor];
self.scrollView.delegate = self;
self.scrollView.showsHorizontalScrollIndicator = NO;
self.scrollView.showsVerticalScrollIndicator = NO;
self.scrollView.scrollEnabled = NO;
self.scrollView.canCancelContentTouches = NO;
[self.view addSubview:self.scrollView];
self.containerView = [[UIView alloc] initWithFrame:self.view.bounds];
self.containerView.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;
[self.scrollView addSubview:self.containerView];
self.imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 50.0, 50.0)];
self.imageView.contentMode = UIViewContentModeScaleAspectFit;
self.imageView.alpha = 0.0f;
self.imageView.userInteractionEnabled = YES;
// Enable edge antialiasing on 7.0 or later.
// This symbol appears pre-7.0 but is not considered public API until 7.0
if (([[[UIDevice currentDevice] systemVersion] compare:@"7.0" options:NSNumericSearch] != NSOrderedAscending)) {
self.imageView.layer.allowsEdgeAntialiasing = YES;
}
[self.containerView addSubview:self.imageView];
/* setup gesture recognizers */
// double tap gesture to return scaled image back to center for easier dismissal
self.doubleTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleDoubleTapGesture:)];
self.doubleTapRecognizer.delegate = self;
self.doubleTapRecognizer.numberOfTapsRequired = 2;
self.doubleTapRecognizer.numberOfTouchesRequired = 1;
[self.imageView addGestureRecognizer:self.doubleTapRecognizer];
// tap recognizer on area outside image view for dismissing
self.tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleDismissFromTap:)];
self.tapRecognizer.delegate = self;
self.tapRecognizer.numberOfTapsRequired = 1;
self.tapRecognizer.numberOfTouchesRequired = 1;
[self.tapRecognizer requireGestureRecognizerToFail:self.doubleTapRecognizer];
[self.view addGestureRecognizer:self.tapRecognizer];
// long press gesture recognizer to bring up photo actions (when `shouldShowPhotoActions` is set to YES)
if (self.shouldShowPhotoActions) {
self.photoLongPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPressGesture:)];
self.photoLongPressRecognizer.delegate = self;
[self.imageView addGestureRecognizer:self.photoLongPressRecognizer];
}
// only add pan gesture and physics stuff if we can (e.g., iOS 7+)
if (NSClassFromString(@"UIDynamicAnimator")) {
// pan gesture to handle the physics
self.panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanGesture:)];
self.panRecognizer.delegate = self;
if (self.allowSwipeOnBackgroundView) {
[self.containerView addGestureRecognizer:self.panRecognizer];
}
else {
[self.imageView addGestureRecognizer:self.panRecognizer];
}
/* UIDynamics stuff */
self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
self.animator.delegate = self;
// snap behavior to keep image view in the center as needed
self.snapBehavior = [[UISnapBehavior alloc] initWithItem:self.imageView snapToPoint:self.view.center];
self.snapBehavior.damping = 1.0f;
self.pushBehavior = [[UIPushBehavior alloc] initWithItems:@[self.imageView] mode:UIPushBehaviorModeInstantaneous];
self.pushBehavior.angle = 0.0f;
self.pushBehavior.magnitude = 0.0f;
self.itemBehavior = [[UIDynamicItemBehavior alloc] initWithItems:@[self.imageView]];
self.itemBehavior.elasticity = 0.0f;
self.itemBehavior.friction = 0.2f;
self.itemBehavior.allowsRotation = YES;
self.itemBehavior.density = __density;
self.itemBehavior.resistance = __resistance;
}
else {
// add tap gesture to image to also dismiss since we don't have UIDynamics to flick out of view
self.photoTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleDismissFromTap:)];
self.photoTapRecognizer.delegate = self;
self.photoTapRecognizer.numberOfTapsRequired = 1;
self.photoTapRecognizer.numberOfTouchesRequired = 1;
[self.photoTapRecognizer requireGestureRecognizerToFail:self.doubleTapRecognizer];
[self.imageView addGestureRecognizer:self.photoTapRecognizer];
}
}
- (void)cancelURLConnectionIfAny {
if (self.loadingView) {
[self.loadingView stopAnimating];
if (self.loadingView.superview) [self.loadingView removeFromSuperview];
}
if (self.urlConnection) [self.urlConnection cancel];
};
#pragma mark - Presenting and Dismissing
- (void)showImage:(UIImage *)image fromView:(UIView *)fromView {
[self showImage:image fromView:fromView inViewController:nil];
}
- (void)showImage:(UIImage *)image fromView:(UIView *)fromView inViewController:(UIViewController *)parentViewController {
self.fromView = fromView;
//self.targetViewController = parentViewController;
UIView *superview = (parentViewController) ? parentViewController.view : fromView.superview;
CGRect fromRect = [superview convertRect:fromView.frame toView:nil];
[self showImage:image fromRect:fromRect];
}
- (void)showImage:(UIImage *)image fromRect:(CGRect)fromRect {
[self view]; // make sure view has loaded first
_currentOrientation = [UIApplication sharedApplication].statusBarOrientation;
// since UIWindow always use portrait orientation in convertRect:inView:, we need to convert the source view's frame to
// this controller's view based on the current interface orientation
self.fromRect = [self convertRect:fromRect forOrientation:_currentOrientation];
self.imageView.transform = CGAffineTransformIdentity;
self.imageView.image = image;
self.imageView.alpha = 0.2;
// create snapshot of background if parallax is enabled
if (self.parallaxEnabled || self.shouldBlurBackground) {
[self createViewsForBackground:NULL];
// hide status bar, but store whether or not we need to unhide it later when dismissing this view
// NOTE: in iOS 7+, this only works if you set `UIViewControllerBasedStatusBarAppearance` to NO in your Info.plist
_unhideStatusBarOnDismiss = ![UIApplication sharedApplication].statusBarHidden;
[[UIApplication sharedApplication] setStatusBarHidden:YES withAnimation:UIStatusBarAnimationNone];
if ([self respondsToSelector:@selector(setNeedsStatusBarAppearanceUpdate)]) {
[self setNeedsStatusBarAppearanceUpdate];
}
}
// update scrollView.contentSize to the size of the image
self.scrollView.contentSize = image.size;
CGFloat scaleWidth = CGRectGetWidth(self.scrollView.frame) / self.scrollView.contentSize.width;
CGFloat scaleHeight = CGRectGetHeight(self.scrollView.frame) / self.scrollView.contentSize.height;
CGFloat scale = MIN(scaleWidth, scaleHeight);
// image view's destination frame is the size of the image capped to the width/height of the target view
CGPoint midpoint = CGPointMake(CGRectGetMidX(self.view.bounds), CGRectGetMidY(self.view.bounds));
CGSize scaledImageSize = CGSizeMake(image.size.width * scale, image.size.height * scale);
CGRect targetRect = CGRectMake(midpoint.x - scaledImageSize.width / 2.0, midpoint.y - scaledImageSize.height / 2.0, scaledImageSize.width, scaledImageSize.height);
// set initial frame of image view to match that of the presenting image
self.imageView.frame = self.fromRect;
_originalFrame = targetRect;
// rotate imageView based on current device orientation
[self reposition];
if (scale < 1.0f) {
self.scrollView.minimumZoomScale = 1.0f;
self.scrollView.maximumZoomScale = 1.0f / scale;
}
else {
self.scrollView.minimumZoomScale = 1.0f / scale;
self.scrollView.maximumZoomScale = 1.0f;
}
_minScale = self.scrollView.minimumZoomScale;
_maxScale = self.scrollView.maximumZoomScale;
_lastPinchScale = 1.0f;
_hasLaidOut = YES;
// register for device orientation changes
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(deviceOrientationChanged:) name:UIDeviceOrientationDidChangeNotification object:nil];
// register with the device that we want to know when the device orientation changes
[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
if (self.targetViewController) {
[self willMoveToParentViewController:self.targetViewController];
if ([UIView instancesRespondToSelector:@selector(setTintAdjustmentMode:)]) {
self.targetViewController.view.tintAdjustmentMode = UIViewTintAdjustmentModeDimmed;
[self.targetViewController.view tintColorDidChange];
}
[self.targetViewController addChildViewController:self];
[self.targetViewController.view addSubview:self.view];
if (self.snapshotView) {
[self.targetViewController.view insertSubview:self.snapshotView belowSubview:self.view];
[self.targetViewController.view insertSubview:self.blurredSnapshotView aboveSubview:self.snapshotView];
}
}
else {
// add this view to the main window if no targetViewController was set
if ([UIView instancesRespondToSelector:@selector(setTintAdjustmentMode:)]) {
self.keyWindow.tintAdjustmentMode = UIViewTintAdjustmentModeDimmed;
[self.keyWindow tintColorDidChange];
}
[self.keyWindow addSubview:self.view];
if (self.snapshotView) {
[self.keyWindow insertSubview:self.snapshotView belowSubview:self.view];
[self.keyWindow insertSubview:self.blurredSnapshotView aboveSubview:self.snapshotView];
}
}
[UIView animateWithDuration:__animationDuration delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
self.backgroundView.alpha = 1.0f;
self.imageView.alpha = 1.0f;
self.imageView.frame = targetRect;
if (self.snapshotView) {
self.blurredSnapshotView.alpha = 1.0f;
if (self.parallaxEnabled) {
self.blurredSnapshotView.transform = CGAffineTransformScale(CGAffineTransformIdentity, __backgroundScale, __backgroundScale);
self.snapshotView.transform = CGAffineTransformScale(CGAffineTransformIdentity, __backgroundScale, __backgroundScale);
}
}
} completion:^(BOOL finished) {
//[self.imageView addGestureRecognizer:self.pinchRecognizer];
if (self.targetViewController) {
[self didMoveToParentViewController:self.targetViewController];
}
if ([self.delegate respondsToSelector:@selector(mediaFocusViewControllerDidAppear:)]) {
[self.delegate mediaFocusViewControllerDidAppear:self];
}
}];
}
- (void)showImageFromURL:(NSURL *)url fromView:(UIView *)fromView {
[self showImageFromURL:url fromView:fromView inViewController:nil];
}
- (void)showImageFromURL:(NSURL *)url fromView:(UIView *)fromView inViewController:(UIViewController *)parentViewController {
self.fromView = fromView;
//self.targetViewController = parentViewController;
UIView *superview = (parentViewController) ? parentViewController.view : fromView.superview;
CGRect fromRect = [superview convertRect:fromView.frame toView:nil];
[self showImageFromURL:url fromRect:fromRect];
}
- (void)showImageFromURL:(NSURL *)url fromRect:(CGRect)fromRect {
self.fromRect = fromRect;
// cancel any outstanding requests if we have one
[self cancelURLConnectionIfAny];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
if (self.requestHTTPHeaders.count > 0) {
for (NSString *key in self.requestHTTPHeaders) {
NSString *value = [self.requestHTTPHeaders valueForKey:key];
[request setValue:value forHTTPHeaderField:key];
}
}
// stores data as it's loaded from the request
self.urlData = [[NSMutableData alloc] init];
// show loading indicator on fromView
if (!self.loadingView) {
self.loadingView = [[UIActivityIndicatorView alloc] initWithFrame:CGRectMake(0, 0, 30.0, 30.0)];
}
if (self.fromView) {
[self.fromView addSubview:self.loadingView];
self.loadingView.center = CGPointMake(CGRectGetWidth(self.fromView.frame) / 2.0, CGRectGetHeight(self.fromView.frame) / 2.0);
}
[self.loadingView startAnimating];
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
self.urlConnection = connection;
}
- (void)dismiss:(BOOL)animated {
if (animated) {
[self dismissToTargetView];
}
else {
self.backgroundView.alpha = 0.0f;
self.imageView.alpha = 0.0f;
[self cleanup];
}
}
- (void)dismissAfterPush {
[self hideSnapshotView];
[UIView animateWithDuration:__animationDuration delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
self.backgroundView.alpha = 0.0f;
} completion:^(BOOL finished) {
[self cleanup];
}];
}
- (void)dismissToTargetView {
[self hideSnapshotView];
if (self.scrollView.zoomScale != 1.0f) {
[self.scrollView setZoomScale:1.0f animated:NO];
}
CGRect targetFrame = [self.view convertRect:self.fromView.frame fromView:nil];
if (!CGRectIsEmpty(self.fromRect)) {
targetFrame = self.fromRect;
}
[UIView animateWithDuration:__animationDuration delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
self.imageView.frame = targetFrame;
if (!CGRectIsEmpty(self.fromRect)) {
self.imageView.frame = self.fromRect;
}
else {
self.imageView.frame = [self.view convertRect:self.fromView.frame fromView:nil];
}
//self.imageView.alpha = 0.0f;
self.backgroundView.alpha = 0.0f;
} completion:^(BOOL finished) {
[self cleanup];
}];
// offset image fade out slightly than background/frame animation
[UIView animateWithDuration:__animationDuration - 0.1 delay:0.05 options:UIViewAnimationOptionCurveEaseOut animations:^{
self.imageView.alpha = 0.0f;
} completion:nil];
}
#pragma mark - Private Methods
- (UIWindow *)keyWindow {
return [UIApplication sharedApplication].keyWindow;
}
- (CGRect)windowBounds {
CGRect windowBounds = [UIScreen mainScreen].bounds;
if (UIInterfaceOrientationIsLandscape(_currentOrientation)) {
windowBounds.size.width = windowBounds.size.height;
windowBounds.size.height = CGRectGetWidth([UIScreen mainScreen].bounds);
}
return windowBounds;
}
- (CGRect)convertRect:(CGRect)rect forOrientation:(UIInterfaceOrientation)orientation {
if (orientation == UIInterfaceOrientationPortraitUpsideDown) {
rect.origin.x = CGRectGetWidth(self.view.frame) - CGRectGetWidth(rect) - CGRectGetMinX(rect);
rect.origin.y = CGRectGetHeight(self.view.frame) - CGRectGetHeight(rect) - CGRectGetMinY(rect);
}
else if (orientation == UIInterfaceOrientationLandscapeLeft) {
rect.origin = CGPointMake(CGRectGetHeight(self.view.frame) - CGRectGetHeight(rect) - CGRectGetMinY(rect), CGRectGetMinX(rect));
}
else if (orientation == UIInterfaceOrientationLandscapeRight) {
rect.origin = CGPointMake(CGRectGetMinY(rect), CGRectGetWidth(self.view.frame) - CGRectGetWidth(rect) - CGRectGetMinX(rect));
}
return rect;
}
- (void)createViewsForBackground:(void (^)())completionBlock {
// container view for window
CGRect containerFrame = CGRectMake(0, 0, CGRectGetWidth(self.keyWindow.frame), CGRectGetHeight(self.keyWindow.frame));
// inset container view so we can blur the edges, but we also need to scale up so when __backgroundScale is applied, everything lines up
// only perform inset if `parallaxEnabled` is YES
if (self.parallaxEnabled) {
containerFrame.size.width *= 1.0f / __backgroundScale;
containerFrame.size.height *= 1.0f / __backgroundScale;
}
UIView *containerView = [[UIView alloc] initWithFrame:CGRectIntegral(containerFrame)];
containerView.backgroundColor = [UIColor blackColor];
// add snapshot of window to the container
UIImage *windowSnapshot = [self.keyWindow urb_snapshotImageWithScale:[UIScreen mainScreen].scale];
UIImageView *windowSnapshotView = [[UIImageView alloc] initWithImage:windowSnapshot];
windowSnapshotView.center = containerView.center;
[containerView addSubview:windowSnapshotView];
containerView.center = self.keyWindow.center;
UIImageView *snapshotView;
// only add blurred view if radius is above 0
if (self.shouldBlurBackground && __blurRadius) {
UIImage *snapshot = [containerView urb_snapshotImageWithScale:[UIScreen mainScreen].scale];
snapshot = [snapshot urb_applyBlurWithRadius:__blurRadius
tintColor:[UIColor colorWithWhite:0.0f alpha:__blurTintColorAlpha]
saturationDeltaFactor:__blurSaturationDeltaMask
maskImage:nil];
snapshotView = [[UIImageView alloc] initWithImage:snapshot];
snapshotView.center = containerView.center;
snapshotView.alpha = 0.0f;
snapshotView.userInteractionEnabled = NO;
}
self.snapshotView = containerView;
self.blurredSnapshotView = snapshotView;
}
- (void)adjustFrame {
CGRect imageFrame = self.imageView.frame;
// snap x sides
if (CGRectGetWidth(imageFrame) > CGRectGetWidth(self.view.frame)) {
if (CGRectGetMinX(imageFrame) > 0) {
imageFrame.origin.x = 0;
}
else if (CGRectGetMaxX(imageFrame) < CGRectGetWidth(self.view.frame)) {
imageFrame.origin.x = CGRectGetWidth(self.view.frame) - CGRectGetWidth(imageFrame);
}
}
else if (self.imageView.center.x != CGRectGetMidX(self.view.frame)) {
imageFrame.origin.x = CGRectGetMidX(self.view.frame) - CGRectGetWidth(imageFrame) / 2.0f;
}
// snap y sides
if (CGRectGetHeight(imageFrame) > CGRectGetHeight(self.view.frame)) {
if (CGRectGetMinY(imageFrame) > 0) {
imageFrame.origin.y = 0;
}
else if (CGRectGetMaxY(imageFrame) < CGRectGetHeight(self.view.frame)) {
imageFrame.origin.y = CGRectGetHeight(self.view.frame) - CGRectGetHeight(imageFrame);
}
}
else if (self.imageView.center.y != CGRectGetMidY(self.view.frame)) {
imageFrame.origin.y = CGRectGetMidY(self.view.frame) - CGRectGetHeight(imageFrame) / 2.0f;
}
[UIView animateWithDuration:0.3f delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
self.imageView.frame = imageFrame;
} completion:^(BOOL finished) {
}];
}
/**
* When adding UIDynamics to a view, it resets `zoomScale` on UIScrollView back to 1.0, which is an issue when applying dynamics
* to the imageView when scaled down. So we just scale the imageView.frame while dynamics are applied.
*/
- (void)scaleImageForDynamics {
_lastZoomScale = self.scrollView.zoomScale;
CGRect imageFrame = self.imageView.frame;
imageFrame.size.width *= _lastZoomScale;
imageFrame.size.height *= _lastZoomScale;
self.imageView.frame = imageFrame;
}
- (void)centerScrollViewContents {
CGSize contentSize = self.scrollView.contentSize;
CGFloat offsetX = (CGRectGetWidth(self.scrollView.frame) > contentSize.width) ? (CGRectGetWidth(self.scrollView.frame) - contentSize.width) / 2.0f : 0.0f;
CGFloat offsetY = (CGRectGetHeight(self.scrollView.frame) > contentSize.height) ? (CGRectGetHeight(self.scrollView.frame) - contentSize.height) / 2.0f : 0.0f;
self.imageView.center = CGPointMake(self.scrollView.contentSize.width / 2.0f + offsetX, self.scrollView.contentSize.height / 2.0f + offsetY);
}
- (void)returnToCenter {
if (self.animator) {
[self.animator removeAllBehaviors];
}
[UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
self.imageView.transform = CGAffineTransformIdentity;
self.imageView.frame = _originalFrame;
} completion:nil];
}
- (void)hideSnapshotView {
// only unhide status bar if it wasn't hidden before this view appeared
if (_unhideStatusBarOnDismiss) {
[[UIApplication sharedApplication] setStatusBarHidden:NO withAnimation:UIStatusBarAnimationNone];
}
[UIView animateWithDuration:__animationDuration delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
self.blurredSnapshotView.alpha = 0.0f;
self.blurredSnapshotView.transform = CGAffineTransformIdentity;
self.snapshotView.transform = CGAffineTransformIdentity;
} completion:^(BOOL finished) {
[self.snapshotView removeFromSuperview];
[self.blurredSnapshotView removeFromSuperview];
self.snapshotView = nil;
self.blurredSnapshotView = nil;
}];
}
- (void)cleanup {
_hasLaidOut = NO;
_hasGeneratedBlurBackground = NO;
[self.view removeFromSuperview];
if (self.targetViewController) {
if ([UIView instancesRespondToSelector:@selector(setTintAdjustmentMode:)]) {
self.targetViewController.view.tintAdjustmentMode = UIViewTintAdjustmentModeAutomatic;
[self.targetViewController.view tintColorDidChange];
}
[self willMoveToParentViewController:nil];
[self removeFromParentViewController];
}
else {
if ([UIWindow instancesRespondToSelector:@selector(setTintAdjustmentMode:)]) {
self.keyWindow.tintAdjustmentMode = UIViewTintAdjustmentModeAutomatic;
[self.keyWindow tintColorDidChange];
}
}
if (self.animator) {
[self.animator removeAllBehaviors];
}
[[UIDevice currentDevice] endGeneratingDeviceOrientationNotifications];
[[NSNotificationCenter defaultCenter] removeObserver:self];
if ([self.delegate respondsToSelector:@selector(mediaFocusViewControllerDidDisappear:)]) {
[self.delegate mediaFocusViewControllerDidDisappear:self];
}
if ([self respondsToSelector:@selector(setNeedsStatusBarAppearanceUpdate)]) {
[self setNeedsStatusBarAppearanceUpdate];
}
}
- (void)saveImageToLibrary:(UIImage *)image {
ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
[library writeImageToSavedPhotosAlbum:image.CGImage orientation:(ALAssetOrientation)image.imageOrientation completionBlock:^(NSURL *assetURL, NSError *error) {
if (error) {
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:error.localizedDescription
message:error.localizedRecoverySuggestion
delegate:nil
cancelButtonTitle:NSLocalizedString(@"OK", nil)
otherButtonTitles:nil];
[alertView show];
}
}];
}
- (void)copyImage:(UIImage *)image {
[UIPasteboard generalPasteboard].image = image;
}
#pragma mark - Gesture Methods
- (void)handlePanGesture:(UIPanGestureRecognizer *)gestureRecognizer {
UIView *view = gestureRecognizer.view;
CGPoint location = [gestureRecognizer locationInView:self.view];
CGPoint boxLocation = [gestureRecognizer locationInView:self.imageView];
if (gestureRecognizer.state == UIGestureRecognizerStateBegan) {
[self.animator removeBehavior:self.snapBehavior];
[self.animator removeBehavior:self.pushBehavior];
UIOffset centerOffset = UIOffsetMake(boxLocation.x - CGRectGetMidX(self.imageView.bounds), boxLocation.y - CGRectGetMidY(self.imageView.bounds));
self.panAttachmentBehavior = [[UIAttachmentBehavior alloc] initWithItem:self.imageView offsetFromCenter:centerOffset attachedToAnchor:location];
//self.panAttachmentBehavior.frequency = 0.0f;
[self.animator addBehavior:self.panAttachmentBehavior];
[self.animator addBehavior:self.itemBehavior];
[self scaleImageForDynamics];
}
else if (gestureRecognizer.state == UIGestureRecognizerStateChanged) {
self.panAttachmentBehavior.anchorPoint = location;
}
else if (gestureRecognizer.state == UIGestureRecognizerStateEnded) {
[self.animator removeBehavior:self.panAttachmentBehavior];
// need to scale velocity values to tame down physics on the iPad
CGFloat deviceVelocityScale = (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) ? 0.2f : 1.0f;
CGFloat deviceAngularScale = (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) ? 0.7f : 1.0f;
// factor to increase delay before `dismissAfterPush` is called on iPad to account for more area to cover to disappear
CGFloat deviceDismissDelay = (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) ? 1.8f : 1.0f;
CGPoint velocity = [gestureRecognizer velocityInView:self.view];
CGFloat velocityAdjust = 10.0f * deviceVelocityScale;
if (fabs(velocity.x / velocityAdjust) > __minimumVelocityRequiredForPush || fabs(velocity.y / velocityAdjust) > __minimumVelocityRequiredForPush) {
UIOffset offsetFromCenter = UIOffsetMake(boxLocation.x - CGRectGetMidX(self.imageView.bounds), boxLocation.y - CGRectGetMidY(self.imageView.bounds));
CGFloat radius = sqrtf(powf(offsetFromCenter.horizontal, 2.0f) + powf(offsetFromCenter.vertical, 2.0f));
CGFloat pushVelocity = sqrtf(powf(velocity.x, 2.0f) + powf(velocity.y, 2.0f));
// calculate angles needed for angular velocity formula
CGFloat velocityAngle = atan2f(velocity.y, velocity.x);
CGFloat locationAngle = atan2f(offsetFromCenter.vertical, offsetFromCenter.horizontal);
if (locationAngle > 0) {
locationAngle -= M_PI * 2;
}
// angle (θ) is the angle between the push vector (V) and vector component parallel to radius, so it should always be positive
CGFloat angle = fabs(fabs(velocityAngle) - fabs(locationAngle));
// angular velocity formula: w = (abs(V) * sin(θ)) / abs(r)
CGFloat angularVelocity = fabs((fabs(pushVelocity) * sinf(angle)) / fabs(radius));
// rotation direction is dependent upon which corner was pushed relative to the center of the view
// when velocity.y is positive, pushes to the right of center rotate clockwise, left is counterclockwise
CGFloat direction = (location.x < view.center.x) ? -1.0f : 1.0f;
// when y component of velocity is negative, reverse direction
if (velocity.y < 0) { direction *= -1; }
// amount of angular velocity should be relative to how close to the edge of the view the force originated
// angular velocity is reduced the closer to the center the force is applied
// for angular velocity: positive = clockwise, negative = counterclockwise
CGFloat xRatioFromCenter = fabs(offsetFromCenter.horizontal) / (CGRectGetWidth(self.imageView.frame) / 2.0f);
CGFloat yRatioFromCetner = fabs(offsetFromCenter.vertical) / (CGRectGetHeight(self.imageView.frame) / 2.0f);
// apply device scale to angular velocity
angularVelocity *= deviceAngularScale;
// adjust angular velocity based on distance from center, force applied farther towards the edges gets more spin
angularVelocity *= ((xRatioFromCenter + yRatioFromCetner) / 2.0f);
[self.itemBehavior addAngularVelocity:angularVelocity * __angularVelocityFactor * direction forItem:self.imageView];
[self.animator addBehavior:self.pushBehavior];
self.pushBehavior.pushDirection = CGVectorMake((velocity.x / velocityAdjust) * __velocityFactor, (velocity.y / velocityAdjust) * __velocityFactor);
self.pushBehavior.active = YES;
// delay for dismissing is based on push velocity also
CGFloat delay = __maximumDismissDelay - (pushVelocity / 10000.0f);
[self performSelector:@selector(dismissAfterPush) withObject:nil afterDelay:(delay * deviceDismissDelay) * __velocityFactor];
}
else {
[self returnToCenter];
}
}
}
- (void)handleDoubleTapGesture:(UITapGestureRecognizer *)gestureRecognizer {
if (self.scrollView.zoomScale != self.scrollView.minimumZoomScale) {
[self.scrollView setZoomScale:self.scrollView.minimumZoomScale animated:YES];
}
else {
CGPoint tapPoint = [self.imageView convertPoint:[gestureRecognizer locationInView:gestureRecognizer.view] fromView:self.scrollView];
CGFloat newZoomScale = self.scrollView.maximumZoomScale;
CGFloat w = CGRectGetWidth(self.imageView.frame) / newZoomScale;
CGFloat h = CGRectGetHeight(self.imageView.frame) / newZoomScale;
if (w != CGRectGetWidth(self.imageView.frame)) {
CGRect zoomRect = CGRectMake(tapPoint.x - (w / 2.0f), tapPoint.y - (h / 2.0f), w, h);
[self enablePanGesture:NO]; // disable pan gesture before we start zooming to prevent some weirdness
[self.scrollView zoomToRect:zoomRect animated:YES];
}
}
}
- (void)handleDismissFromTap:(UITapGestureRecognizer *)gestureRecognizer {
CGPoint location = [gestureRecognizer locationInView:self.view];
// if we are allowing a tap anywhere to dismiss, check if we allow taps within image bounds to dismiss also
// otherwise a tap outside image bounds will only be able to dismiss
if (self.shouldDismissOnTap) {
if (self.shouldDismissOnImageTap || !CGRectContainsPoint(self.imageView.frame, location)) {
[self dismissToTargetView];
return;
}
}
if (self.shouldDismissOnImageTap && CGRectContainsPoint(self.imageView.frame, location)) {
// we aren't allowing taps outside of image bounds to dismiss, but tap was detected on image view, we can dismiss
[self dismissToTargetView];
return;
}
}
- (void)handleLongPressGesture:(UILongPressGestureRecognizer *)gestureRecognizer {
if (gestureRecognizer.state == UIGestureRecognizerStateBegan) {
UIActionSheet *actionSheet = [[UIActionSheet alloc] initWithTitle:nil
delegate:self
cancelButtonTitle:NSLocalizedString(@"Cancel", nil)
destructiveButtonTitle:nil
otherButtonTitles:NSLocalizedString(@"Save Photo", nil), NSLocalizedString(@"Copy Photo", nil), nil];
[actionSheet showInView:self.view];
}
}
- (void)enablePanGesture:(BOOL)enabled {
if (!self.panRecognizer) {
return;
}
if (!enabled) {
if (self.allowSwipeOnBackgroundView) {
[self.containerView removeGestureRecognizer:self.panRecognizer];
}
else {
[self.imageView removeGestureRecognizer:self.panRecognizer];
}
}
else {
if (self.allowSwipeOnBackgroundView) {
[self.containerView addGestureRecognizer:self.panRecognizer];
}
else {
[self.imageView addGestureRecognizer:self.panRecognizer];
}
}
}
#pragma mark - UIScrollViewDelegate Methods
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
return self.imageView;
}
- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
// zoomScale of 1.0 is always our starting point, so anything other than that we disable the pan gesture recognizer
if (!scrollView.zooming) {
if (scrollView.zoomScale <= 1.0f) {
[self enablePanGesture:YES];
scrollView.scrollEnabled = NO;
}
else {
[self enablePanGesture:NO];
scrollView.scrollEnabled = YES;
}
}
[self centerScrollViewContents];
if (scrollView.zoomScale > 1.0) {
self.containerView.frame = CGRectMake(0, 0, scrollView.contentSize.width, scrollView.contentSize.height);
}
else {
self.containerView.frame = self.scrollView.bounds;
}
}
#pragma mark - UIActionSheetDelegate
- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex {
if (buttonIndex == 0) {
[self saveImageToLibrary:self.imageView.image];
}
else if (buttonIndex == 1) {
[self copyImage:self.imageView.image];
}
}
#pragma mark - UIGestureRecognizerDelegate Methods
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
CGFloat transformScale = self.imageView.transform.a;
BOOL shouldRecognize = transformScale > _minScale;
// make sure tap and double tap gestures aren't recognized simultaneously
shouldRecognize = shouldRecognize && !([gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]] && [otherGestureRecognizer isKindOfClass:[UITapGestureRecognizer class]]);
return shouldRecognize;
}
#pragma mark - NSURLConnectionDelegate
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[self.urlData appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
[self.loadingView stopAnimating];
[self.loadingView removeFromSuperview];
if (self.urlData) {
NSString *urlPath = connection.currentRequest.URL.absoluteString;
__block UIImage *image;
__block UIImage *staticImageForGif;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// determine if the loaded url is an animated GIF, and setup accordingly if so
if ([[urlPath substringFromIndex:[urlPath length] - 3] isEqualToString:@"gif"]) {
staticImageForGif = [UIImage imageWithData:self.urlData];
image = [UIImage urb_animatedImageWithAnimatedGIFData:self.urlData];
}
else {
image = [UIImage imageWithData:self.urlData];
}
dispatch_async(dispatch_get_main_queue(), ^{
// sometimes the server can return bad or corrupt image data which will result in a crash if we don't throw an error here
if (!image) {
NSString *errorDescription = [NSString stringWithFormat:@"Bad or corrupt image data for %@", urlPath];
NSError *error = [NSError errorWithDomain:@"com.urban10.URBMediaFocusViewController" code:100 userInfo:@{NSLocalizedDescriptionKey: errorDescription}];
if ([self.delegate respondsToSelector:@selector(mediaFocusViewController:didFailLoadingImageWithError:)]) {
[self.delegate mediaFocusViewController:self didFailLoadingImageWithError:error];
}
return;
}
// set the initial image to the static version of the GIF for the present animation
if (staticImageForGif) {
self.imageView.image = staticImageForGif;
}
[self showImage:image fromRect:self.fromRect];
if ([self.delegate respondsToSelector:@selector(mediaFocusViewController:didFinishLoadingImage:)]) {
[self.delegate mediaFocusViewController:self didFinishLoadingImage:image];
}
});
});
}
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
if ([self.delegate respondsToSelector:@selector(mediaFocusViewController:didFailLoadingImageWithError:)]) {
[self.delegate mediaFocusViewController:self didFailLoadingImageWithError:error];
}
}
#pragma mark - Orientation Helpers
- (void)deviceOrientationChanged:(NSNotification *)notification {
UIInterfaceOrientation deviceOrientation = (UIInterfaceOrientation)[UIDevice currentDevice].orientation;
if (_currentOrientation != deviceOrientation) {
_currentOrientation = deviceOrientation;
if (self.shouldRotateToDeviceOrientation) {
[self reposition];
}
}
}
- (CGAffineTransform)transformForOrientation:(UIInterfaceOrientation)orientation {
CGAffineTransform transform = CGAffineTransformIdentity;
// calculate a rotation transform that matches the required orientation
if (orientation == UIInterfaceOrientationPortraitUpsideDown) {
transform = CGAffineTransformMakeRotation(M_PI);
}
else if (orientation == UIInterfaceOrientationLandscapeLeft) {
transform = CGAffineTransformMakeRotation(-M_PI_2);
}
else if (orientation == UIInterfaceOrientationLandscapeRight) {
transform = CGAffineTransformMakeRotation(M_PI_2);
}
return transform;
}
- (void)reposition {
CGAffineTransform baseTransform = [self transformForOrientation:_currentOrientation];
// determine if the rotation we're about to undergo is 90 or 180 degrees
CGAffineTransform t1 = self.imageView.transform;
CGAffineTransform t2 = baseTransform;
CGFloat dot = t1.a * t2.a + t1.c * t2.c;
CGFloat n1 = sqrtf(t1.a * t1.a + t1.c * t1.c);
CGFloat n2 = sqrtf(t2.a * t2.a + t2.c * t2.c);
CGFloat rotationDelta = acosf(dot / (n1 * n2));
BOOL isDoubleRotation = (rotationDelta > 1.581);
// use the system rotation duration
CGFloat duration = [UIApplication sharedApplication].statusBarOrientationAnimationDuration;
// iPad lies about its rotation duration
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { duration = 0.4; }
// double the animation duration if we're rotation 180 degrees
if (isDoubleRotation) { duration *= 2; }
// if we haven't laid out the subviews yet, we don't want to animate rotation and position transforms
if (_hasLaidOut) {
[UIView animateWithDuration:duration animations:^{
self.containerView.transform = CGAffineTransformConcat(CGAffineTransformIdentity, baseTransform);
}];
}
else {
self.containerView.transform = CGAffineTransformConcat(CGAffineTransformIdentity, baseTransform);
}
}
@end
@implementation UIView (URBMediaFocusViewController)
- (UIImage *)urb_snapshotImageWithScale:(CGFloat)scale {
__strong CALayer *underlyingLayer = self.layer;
CGRect bounds = self.bounds;
CGSize size = bounds.size;
if (self.contentMode == UIViewContentModeScaleToFill ||
self.contentMode == UIViewContentModeScaleAspectFill ||
self.contentMode == UIViewContentModeScaleAspectFit ||
self.contentMode == UIViewContentModeRedraw)
{
// prevents edge artefacts
size.width = floorf(size.width * scale) / scale;
size.height = floorf(size.height * scale) / scale;
}
else if ([[UIDevice currentDevice].systemVersion floatValue] < 7.0f && [UIScreen mainScreen].scale == 1.0f) {
// prevents pixelation on old devices