-
Notifications
You must be signed in to change notification settings - Fork 54
/
Copy pathBFRImageContainerViewController.m
312 lines (256 loc) · 13.1 KB
/
BFRImageContainerViewController.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
//
// BFRImageContainerViewController.m
// Buffer
//
// Created by Jordan Morgan on 11/10/15.
//
//
#import "BFRImageContainerViewController.h"
#import <DACircularProgress/DACircularProgressView.h>
#import "AFNetworking.h"
@interface BFRImageContainerViewController () <UIScrollViewDelegate, UIGestureRecognizerDelegate>
@property (strong, nonatomic) UIScrollView *scrollView;
@property (strong, nonatomic) UIImageView *imgView;
@property (strong, nonatomic) UIImage *imgLoaded;
@property (strong, nonatomic) DACircularProgressView *progressView;
@property (strong, nonatomic) UIDynamicAnimator *animator;
@property (strong, nonatomic) UIAttachmentBehavior *imgAttatchment;
@end
@implementation BFRImageContainerViewController
#pragma mark - Lifecycle
//With peeking and popping, setting up your subviews in loadView will throw an exception
- (void)viewDidLoad {
[super viewDidLoad];
//View setup
self.automaticallyAdjustsScrollViewInsets = NO;
self.view.backgroundColor = [UIColor clearColor];
//Scrollview (for pinching in and out of image)
self.scrollView = [self createScrollView];
[self.view addSubview:self.scrollView];
//Fetch image - or just display it
if ([self.imgSrc isKindOfClass:[NSURL class]]) {
//Loading view
self.progressView = [self createProgressView];
[self.view addSubview:self.progressView];
[self retrieveImageFromURL];
} else {
self.imgLoaded = (UIImage *)self.imgSrc;
[self addImageToScrollView];
}
//Animator - used to snap the image back to the center when done dragging
self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.scrollView];
}
- (void)viewWillLayoutSubviews {
//Scrollview
[self.scrollView setFrame:self.view.bounds];
//Set the aspect ration of the image
float hfactor = self.imgLoaded.size.width / self.view.bounds.size.width;
float vfactor = self.imgLoaded.size.height / self.view.bounds.size.height;
float factor = fmax(hfactor, vfactor);
//Divide the size by the greater of the vertical or horizontal shrinkage factor
float newWidth = self.imgLoaded.size.width / factor;
float newHeight = self.imgLoaded.size.height / factor;
//Then figure out offset to center vertically or horizontally
float leftOffset = (self.view.bounds.size.width - newWidth) / 2;
float topOffset = ( self.view.bounds.size.height - newHeight) / 2;
//Reposition image view
CGRect newRect = CGRectMake(leftOffset, topOffset, newWidth, newHeight);
self.imgView.frame = newRect;
}
#pragma mark - UI Methods
- (UIScrollView *)createScrollView {
UIScrollView *sv = [[UIScrollView alloc] initWithFrame:self.view.bounds];
sv.delegate = self;
sv.showsHorizontalScrollIndicator = NO;
sv.showsVerticalScrollIndicator = NO;
sv.decelerationRate = UIScrollViewDecelerationRateFast;
sv.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
//For UI Toggling
UITapGestureRecognizer *singleSVTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(dismissUI)];
singleSVTap.numberOfTapsRequired = 1;
singleSVTap.cancelsTouchesInView = NO;
[sv addGestureRecognizer:singleSVTap];
return sv;
}
- (DACircularProgressView *)createProgressView {
CGFloat screenWidth = self.view.bounds.size.width;
CGFloat screenHeight = self.view.bounds.size.height;
DACircularProgressView *progressView = [[DACircularProgressView alloc] initWithFrame:CGRectMake((screenWidth-35.)/2., (screenHeight-35.)/2, 35.0f, 35.0f)];
[progressView setProgress:0.0f];
progressView.thicknessRatio = 0.1;
progressView.roundedCorners = NO;
progressView.trackTintColor = [UIColor colorWithWhite:0.2 alpha:1];
progressView.progressTintColor = [UIColor colorWithWhite:1.0 alpha:1];
return progressView;
}
- (UIImageView *)createImageView {
UIImageView *resizableImageView = [[UIImageView alloc] initWithImage:self.imgLoaded];
resizableImageView.frame = self.view.bounds;
resizableImageView.clipsToBounds = YES;
resizableImageView.contentMode = UIViewContentModeScaleAspectFill;
resizableImageView.backgroundColor = [UIColor colorWithWhite:0 alpha:1];
//Scale to keep its aspect ration
CGRect screenBound = [[UIScreen mainScreen] bounds];
CGFloat screenWidth = screenBound.size.width;
CGFloat screenHeight = screenBound.size.height;
float scaleFactor = (self.imgLoaded ? self.imgLoaded.size.width : screenWidth) / screenWidth;
CGRect finalImageViewFrame = CGRectMake(0, (screenHeight/2)-((self.imgLoaded.size.height / scaleFactor)/2), screenWidth, self.imgLoaded.size.height / scaleFactor);
resizableImageView.layer.frame = finalImageViewFrame;
//Toggle UI controls
UITapGestureRecognizer *singleImgTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(dismissUI)];
singleImgTap.numberOfTapsRequired = 1;
[resizableImageView setUserInteractionEnabled:YES];
[resizableImageView addGestureRecognizer:singleImgTap];
//Recent the image on double tap
UITapGestureRecognizer *doubleImgTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(recenterImageOriginOrZoomToPoint:)];
doubleImgTap.numberOfTapsRequired = 2;
[resizableImageView addGestureRecognizer:doubleImgTap];
//Ensure the single tap doesn't fire when a user attempts to double tap
[singleImgTap requireGestureRecognizerToFail:doubleImgTap];
//Dragging to dismiss
UIPanGestureRecognizer *panImg = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleDrag:)];
if (self.shouldDisableHorizontalDrag) {
panImg.delegate = self;
}
[resizableImageView addGestureRecognizer:panImg];
return resizableImageView;
}
- (void)addImageToScrollView {
if (!self.imgView) {
self.imgView = [self createImageView];
[self.scrollView addSubview:self.imgView];
[self setMaxMinZoomScalesForCurrentBounds];
}
}
#pragma mark - Gesture Recognizer Delegate
//If we have more than one image, this will cancel out dragging horizontally to make it easy to navigate between images
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
CGPoint velocity = [(UIPanGestureRecognizer *)gestureRecognizer velocityInView:self.scrollView];
return fabs(velocity.y) > fabs(velocity.x);
}
#pragma mark - Scrollview Delegate
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
return self.scrollView.subviews.firstObject;
}
- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
[self.animator removeAllBehaviors];
[self centerScrollViewContents];
}
#pragma mark - Scrollview Util Methods
/*! This calculates the correct zoom scale for the scrollview once we have the image's size */
- (void)setMaxMinZoomScalesForCurrentBounds {
// Sizes
CGSize boundsSize = self.scrollView.bounds.size;
CGSize imageSize = self.imgView.frame.size;
// Calculate Min
CGFloat xScale = boundsSize.width / imageSize.width;
CGFloat yScale = boundsSize.height / imageSize.height;
CGFloat minScale = MIN(xScale, yScale);
// Calculate Max
CGFloat maxScale = 4.0;
if ([UIScreen instancesRespondToSelector:@selector(scale)]) {
maxScale = maxScale / [[UIScreen mainScreen] scale];
if (maxScale < minScale) {
maxScale = minScale * 2;
}
}
//Apply zoom
self.scrollView.maximumZoomScale = maxScale;
self.scrollView.minimumZoomScale = minScale;
self.scrollView.zoomScale = minScale;
}
/*! Called during zooming of the image to ensure it stays centered */
- (void)centerScrollViewContents {
CGSize boundsSize = self.scrollView.bounds.size;
CGRect contentsFrame = self.imgView.frame;
if (contentsFrame.size.width < boundsSize.width) {
contentsFrame.origin.x = (boundsSize.width - contentsFrame.size.width) / 2.0f;
} else {
contentsFrame.origin.x = 0.0f;
}
if (contentsFrame.size.height < boundsSize.height) {
contentsFrame.origin.y = (boundsSize.height - contentsFrame.size.height) / 2.0f;
} else {
contentsFrame.origin.y = 0.0f;
}
self.imgView.frame = contentsFrame;
}
/*! Called when an image is double tapped. Either zooms out or to specific point */
- (void)recenterImageOriginOrZoomToPoint:(UITapGestureRecognizer *)tap {
if (self.scrollView.zoomScale == self.scrollView.maximumZoomScale) {
//Zoom out since we zoomed in here
[self.scrollView setZoomScale:self.scrollView.minimumZoomScale animated:YES];
} else {
//Zoom to a point
CGPoint touchPoint = [tap locationInView:self.scrollView];
[self.scrollView zoomToRect:CGRectMake(touchPoint.x, touchPoint.y, 1, 1) animated:YES];
}
}
#pragma mark - Dragging Methods
- (void)handleDrag:(UIPanGestureRecognizer *)recognizer {
if (recognizer.state == UIGestureRecognizerStateBegan) {
[self.animator removeAllBehaviors];
CGPoint location = [recognizer locationInView:self.scrollView];
CGPoint imgLocation = [recognizer locationInView:self.imgView];
UIOffset centerOffset = UIOffsetMake(imgLocation.x - CGRectGetMidX(self.imgView.bounds),
imgLocation.y - CGRectGetMidY(self.imgView.bounds));
self.imgAttatchment = [[UIAttachmentBehavior alloc] initWithItem:self.imgView offsetFromCenter:centerOffset attachedToAnchor:location];
[self.animator addBehavior:self.imgAttatchment];
} else if (recognizer.state == UIGestureRecognizerStateChanged) {
[self.imgAttatchment setAnchorPoint:[recognizer locationInView:self.scrollView]];
} else if (recognizer.state == UIGestureRecognizerStateEnded) {
CGPoint location = [recognizer locationInView:self.scrollView];
CGRect closeTopThreshhold = CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height * .15);
CGRect closeBottomThreshhold = CGRectMake(0, self.view.bounds.size.height - closeTopThreshhold.size.height, self.view.bounds.size.width, self.view.bounds.size.height * .15);
//Check if we should close - or just snap back to the center
if (CGRectContainsPoint(closeTopThreshhold, location) || CGRectContainsPoint(closeBottomThreshhold, location)) {
[self.animator removeAllBehaviors];
self.imgView.userInteractionEnabled = NO;
self.scrollView.userInteractionEnabled = NO;
UIGravityBehavior *exitGravity = [[UIGravityBehavior alloc] initWithItems:@[self.imgView]];
if (CGRectContainsPoint(closeTopThreshhold, location)) {
exitGravity.gravityDirection = CGVectorMake(0.0, -1.0);
}
exitGravity.magnitude = 15.0f;
[self.animator addBehavior:exitGravity];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.35f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self dismissUI];
});
} else {
[self.scrollView setZoomScale:self.scrollView.minimumZoomScale animated:YES];
UISnapBehavior *snapBack = [[UISnapBehavior alloc] initWithItem:self.imgView snapToPoint:self.scrollView.center];
[self.animator addBehavior:snapBack];
}
}
}
#pragma mark - Misc. Methods
- (void)dismissUI {
[[NSNotificationCenter defaultCenter] postNotificationName:@"DismissUI" object:nil];
}
- (void)retrieveImageFromURL {
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:(NSURL *)self.imgSrc cachePolicy:NSURLRequestReturnCacheDataElseLoad timeoutInterval:0];
AFHTTPRequestOperation *imgOperation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
imgOperation.responseSerializer = [AFImageResponseSerializer serializer];
[imgOperation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
self.imgLoaded = responseObject;
[self addImageToScrollView];
[self.progressView removeFromSuperview];
} failure:^(AFHTTPRequestOperation *operation, NSError *error){
[self.progressView removeFromSuperview];
[self showError];
}];
[imgOperation setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) {
CGFloat progress = ((CGFloat)totalBytesRead)/((CGFloat)totalBytesExpectedToRead);
[self.progressView setProgress:progress];
}];
[[NSOperationQueue mainQueue] addOperation:imgOperation];
}
- (void)showError {
UIAlertController *controller = [UIAlertController alertControllerWithTitle:@"Whoops" message:@"Looks like we ran into an issue loading the image, sorry about that!" preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *closeAction = [UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action){
[[NSNotificationCenter defaultCenter] postNotificationName:@"ImageLoadingError" object:nil];
}];
[controller addAction:closeAction];
[self presentViewController:controller animated:YES completion:nil];
}
@end