Skip to content

Instantly share code, notes, and snippets.

@leilee
Last active December 28, 2022 08:12
Show Gist options
  • Save leilee/8edd3bbeec43e8b294fdd6072f55960b to your computer and use it in GitHub Desktop.
Save leilee/8edd3bbeec43e8b294fdd6072f55960b to your computer and use it in GitHub Desktop.
View to expand & zoom the photo. Inspired by https://github.com/muukii/ZoomImageView
#import <UIKit/UIKit.h>
@interface OUPZoomImageView : UIScrollView
@property (nonatomic) UIImage* image;
@end
#import "OUPZoomImageView.h"
@interface OUPZoomImageView ()<UIScrollViewDelegate>
@property (nonatomic) UIImageView* imageView;
@property (nonatomic) CGSize oldSize;
@end
@implementation OUPZoomImageView
#pragma mark - init
- (instancetype)initWithCoder:(NSCoder*)coder
{
self = [super initWithCoder:coder];
if (self)
{
[self commonInit];
}
return self;
}
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
[self commonInit];
}
return self;
}
- (void)commonInit
{
self.backgroundColor = UIColor.clearColor;
self.delegate = self;
self.showsVerticalScrollIndicator = NO;
self.showsHorizontalScrollIndicator = NO;
self.decelerationRate = UIScrollViewDecelerationRateFast;
_imageView = [[UIImageView alloc] init];
_imageView.contentMode = UIViewContentModeScaleAspectFill;
[self addSubview:_imageView];
auto doubleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleDoubleTap:)];
doubleTap.numberOfTapsRequired = 2;
[self addGestureRecognizer:doubleTap];
}
- (void)layoutSubviews
{
[super layoutSubviews];
auto bounds = self.bounds;
if (self.image != nil && _oldSize != bounds.size)
{
[self updateImageView];
_oldSize = bounds.size;
}
if (CGRectGetWidth(_imageView.frame) <= CGRectGetWidth(bounds))
{
auto center = _imageView.center;
center.x = CGRectGetWidth(bounds) * 0.5;
_imageView.center = center;
}
if (CGRectGetHeight(_imageView.frame) <= CGRectGetHeight(bounds))
{
auto center = _imageView.center;
center.y = CGRectGetHeight(bounds) * 0.5;
_imageView.center = center;
}
}
#pragma mark - action
- (void)handleDoubleTap:(UITapGestureRecognizer*)gr
{
if (self.minimumZoomScale >= self.maximumZoomScale)
{
return;
}
if (self.zoomScale < self.maximumZoomScale)
{
[self zoomToMaxScaleWithGestureRecognizer:gr];
}
else
{
[self zoomToMinScale];
}
}
#pragma mark - zoom
- (void)zoomToMaxScaleWithGestureRecognizer:(UITapGestureRecognizer*)gr
{
auto zoomRect = [self zoomRectWithScale:self.maximumZoomScale
withCenter:[gr locationInView:gr.view]];
[self zoomToRect:zoomRect animated:YES];
}
- (void)zoomToMinScale
{
[self setZoomScale:self.minimumZoomScale animated:YES];
}
- (CGRect)zoomRectWithScale:(CGFloat)scale withCenter:(CGPoint)center
{
center = [_imageView convertPoint:center fromView:self];
auto imageSize = _imageView.bounds.size;
auto zoomSize = imageSize / scale;
auto zoomX = center.x - (zoomSize.width / 2.f);
auto zoomY = center.y - (zoomSize.height / 2.f);
return CGRect{.origin = {zoomX, zoomY}, .size = zoomSize};
}
#pragma mark - update
- (void)updateImageView
{
if (!self.image)
{
return;
}
auto boundingSize = self.bounds.size;
auto imageSize = self.image.size;
auto widthRatio = boundingSize.width / imageSize.width;
auto heightRatio = boundingSize.height / imageSize.height;
if (widthRatio < heightRatio)
{
boundingSize.height = boundingSize.width / imageSize.width * imageSize.height;
}
else if (heightRatio < widthRatio)
{
boundingSize.width = boundingSize.height / imageSize.height * imageSize.width;
}
auto size = CGSizeMake(ceil(boundingSize.width), ceil(boundingSize.height));
_imageView.bounds = {.origin = CGPointZero, .size = size};
self.contentSize = size;
_imageView.center = [self contentCenterForBoundingSize:self.bounds.size contentSize:self.contentSize];
self.zoomScale = 1;
self.minimumZoomScale = 1;
self.maximumZoomScale = 3;
}
#pragma mark - accessor
- (UIImage*)image
{
return _imageView.image;
}
- (void)setImage:(UIImage*)image
{
auto oldImage = _imageView.image;
_imageView.image = image;
if (oldImage.size != image.size)
{
_oldSize = CGSizeZero;
[self setNeedsLayout];
[self updateImageView];
}
}
#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidZoom:(UIScrollView*)scrollView
{
_imageView.center = [self contentCenterForBoundingSize:self.bounds.size
contentSize:self.contentSize];
}
- (UIView*)viewForZoomingInScrollView:(UIScrollView*)scrollView
{
return _imageView;
}
#pragma mark - internal
- (CGPoint)contentCenterForBoundingSize:(CGSize)boundingSize contentSize:(CGSize)contentSize
{
/// When the zoom scale changes i.e. the image is zoomed in or out, the hypothetical center
/// of content view changes too. But the default Apple implementation is keeping the last center
/// value which doesn't make much sense. If the image ratio is not matching the screen
/// ratio, there will be some empty space horizontaly or verticaly. This needs to be calculated
/// so that we can get the correct new center value. When these are added, edges of contentView
/// are aligned in realtime and always aligned with corners of scrollview.
auto horizontalOffest = (boundingSize.width > contentSize.width) ? ((boundingSize.width - contentSize.width) * 0.5) : 0.0;
auto verticalOffset = (boundingSize.height > contentSize.height) ? ((boundingSize.height - contentSize.height) * 0.5) : 0.0;
return CGPointMake(contentSize.width * 0.5 + horizontalOffest, contentSize.height * 0.5 + verticalOffset);
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment