IOS: add imageview in a scrollview to have zoom
I want to set a UIImageView with a UIImage
and put this imageview inside a UIScrollView
to obtain a zoom of this image;
and I want this UIImageView
and UIScrollView
to fit in the rect at the center of the view...is it possible?
- Set your view controller up as a
<UIScrollViewDelegate>
- Draw your
UIScrollView
the size you want for the rectangle at the center of the view. Set the max zoom in the inspector to something bigger than 1. Like 4 or 10. - Right click on the scroll view and connect the delegate to your view controller.
- Draw your
UIImageView
in theUIScrollView
and set it up with whatever image you want. Make it the same size as theUIScrollView
. - Ctrl + drag form you
UIImageView
to the.h
of your View controller to create anIBOutlet
for theUIImageView
, call it something clever likeimageView
. -
Add this code:
-(UIView *) viewForZoomingInScrollView:(UIScrollView *)scrollView { return self.imageView; }
Run the app and pinch and pan til your heart's content.
Download this and this files. You'll need them to handle touches.
Add to your view the scrollView delegate <UIScrollViewDelegate>
and declare the outlets:
@property (nonatomic, retain) IBOutlet UIScrollView *imageScrollView;
@property (nonatomic, retain) UIImageView *imageView;
Import the downloaded file inside the screen and do:
#import "TapDetectingImageView.h"
#define ZOOM_STEP 2.0
@interface myView (UtilityMethods)
- (CGRect)zoomRectForScale:(float)scale withCenter:(CGPoint)center;
@end
@implementation myView
@synthesize imageScrollView, imageView;
- (void)viewDidLoad
{
[super viewDidLoad];
//Setting up the scrollView
imageScrollView.bouncesZoom = YES;
imageScrollView.delegate = self;
imageScrollView.clipsToBounds = YES;
//Setting up the imageView
imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"myImage.png"]];
imageView.userInteractionEnabled = YES;
imageView.autoresizingMask = ( UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin);
//Adding the imageView to the scrollView as subView
[imageScrollView addSubview:imageView];
imageScrollView.contentSize = CGSizeMake(imageView.bounds.size.width, imageView.bounds.size.height);
imageScrollView.decelerationRate = UIScrollViewDecelerationRateFast;
//UITapGestureRecognizer set up
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleSingleTap:)];
UITapGestureRecognizer *doubleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleDoubleTap:)];
UITapGestureRecognizer *twoFingerTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTwoFingerTap:)];
[doubleTap setNumberOfTapsRequired:2];
[twoFingerTap setNumberOfTouchesRequired:2];
//Adding gesture recognizer
[imageView addGestureRecognizer:doubleTap];
[imageView addGestureRecognizer:twoFingerTap];
[singleTap release];
[doubleTap release];
[twoFingerTap release];
// calculate minimum scale to perfectly fit image width, and begin at that scale
float minimumScale = 1.0;//This is the minimum scale, set it to whatever you want. 1.0 = default
imageScrollView.maximumZoomScale = 4.0;
imageScrollView.minimumZoomScale = minimumScale;
imageScrollView.zoomScale = minimumScale;
[imageScrollView setContentMode:UIViewContentModeScaleAspectFit];
[imageView sizeToFit];
[imageScrollView setContentSize:CGSizeMake(imageView.frame.size.width, imageView.frame.size.height)];
}
- (void)scrollViewDidZoom:(UIScrollView *)aScrollView {
CGFloat offsetX = (imageScrollView.bounds.size.width > imageScrollView.contentSize.width)?
(imageScrollView.bounds.size.width - imageScrollView.contentSize.width) * 0.5 : 0.0;
CGFloat offsetY = (imageScrollView.bounds.size.height > imageScrollView.contentSize.height)?
(imageScrollView.bounds.size.height - imageScrollView.contentSize.height) * 0.5 : 0.0;
imageView.center = CGPointMake(imageScrollView.contentSize.width * 0.5 + offsetX,
imageScrollView.contentSize.height * 0.5 + offsetY);
}
- (void)viewDidUnload {
self.imageScrollView = nil;
self.imageView = nil;
}
#pragma mark UIScrollViewDelegate methods
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
return imageView;
}
#pragma mark TapDetectingImageViewDelegate methods
- (void)handleDoubleTap:(UIGestureRecognizer *)gestureRecognizer {
// zoom in
float newScale = [imageScrollView zoomScale] * ZOOM_STEP;
if (newScale > self.imageScrollView.maximumZoomScale){
newScale = self.imageScrollView.minimumZoomScale;
CGRect zoomRect = [self zoomRectForScale:newScale withCenter:[gestureRecognizer locationInView:gestureRecognizer.view]];
[imageScrollView zoomToRect:zoomRect animated:YES];
}
else{
newScale = self.imageScrollView.maximumZoomScale;
CGRect zoomRect = [self zoomRectForScale:newScale withCenter:[gestureRecognizer locationInView:gestureRecognizer.view]];
[imageScrollView zoomToRect:zoomRect animated:YES];
}
}
- (void)handleTwoFingerTap:(UIGestureRecognizer *)gestureRecognizer {
// two-finger tap zooms out
float newScale = [imageScrollView zoomScale] / ZOOM_STEP;
CGRect zoomRect = [self zoomRectForScale:newScale withCenter:[gestureRecognizer locationInView:gestureRecognizer.view]];
[imageScrollView zoomToRect:zoomRect animated:YES];
}
#pragma mark Utility methods
- (CGRect)zoomRectForScale:(float)scale withCenter:(CGPoint)center {
CGRect zoomRect;
// the zoom rect is in the content view's coordinates.
// At a zoom scale of 1.0, it would be the size of the imageScrollView's bounds.
// As the zoom scale decreases, so more content is visible, the size of the rect grows.
zoomRect.size.height = [imageScrollView frame].size.height / scale;
zoomRect.size.width = [imageScrollView frame].size.width / scale;
// choose an origin so as to get the right center.
zoomRect.origin.x = center.x - (zoomRect.size.width / 2.0);
zoomRect.origin.y = center.y - (zoomRect.size.height / 2.0);
return zoomRect;
}
Done!
Basically what this code do is to add the imageView
as subview of the imageScrollView
.
Then, it adds the TapDetecting
class methods to the scrollView, in order to recognize the number of taps - the pinch the user do and add zoom functionalities.
You can set the minimumScale
of the image, if you leave 1.0
the image should be displayed as-it-is (if you set it a little bit lower it's being scaled), and the maximumZoomScale
, i suggest you to leave it to 4, it's fine!
Now, you can load images programmatically from there.
The last thing you have to do is to insert a UIScrollView
inside your xib file and link it to imageScrollView
. You'll have the image at the perfect center, you can double tap on it to zoom, pinch to zoom as you set up in code.
With Swift 4 and iOS 11, you can use one of the two following solutions in order to solve your problem.
#1. Using insets
ViewController.swift
import UIKit
final class ViewController: UIViewController {
private let scrollView = ImageScrollView(image: UIImage(named: "image")!)
override func viewDidLoad() {
view.backgroundColor = .black
view.addSubview(scrollView)
scrollView.frame = view.frame
scrollView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
}
}
ImageScrollView.swift
import UIKit
final class ImageScrollView: UIScrollView {
private let imageView = UIImageView()
override var frame: CGRect {
didSet {
if frame.size != oldValue.size { setZoomScale() }
}
}
required init(image: UIImage) {
super.init(frame: .zero)
imageView.image = image
imageView.sizeToFit()
addSubview(imageView)
contentSize = imageView.bounds.size
contentInsetAdjustmentBehavior = .never // Adjust content according to safe area if necessary
showsVerticalScrollIndicator = false
showsHorizontalScrollIndicator = false
alwaysBounceHorizontal = true
alwaysBounceVertical = true
delegate = self
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Helper methods
func setZoomScale() {
let widthScale = frame.size.width / imageView.bounds.width
let heightScale = frame.size.height / imageView.bounds.height
let minScale = min(widthScale, heightScale)
minimumZoomScale = minScale
zoomScale = minScale
}
}
extension ImageScrollView: UIScrollViewDelegate {
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return imageView
}
func scrollViewDidZoom(_ scrollView: UIScrollView) {
let imageViewSize = imageView.frame.size
let scrollViewSize = scrollView.bounds.size
let verticalInset = imageViewSize.height < scrollViewSize.height ? (scrollViewSize.height - imageViewSize.height) / 2 : 0
let horizontalInset = imageViewSize.width < scrollViewSize.width ? (scrollViewSize.width - imageViewSize.width) / 2 : 0
scrollView.contentInset = UIEdgeInsets(top: verticalInset, left: horizontalInset, bottom: verticalInset, right: horizontalInset)
}
}
#2. Using Auto Layout
ViewController.swift
import UIKit
final class ViewController: UIViewController {
private let scrollView = ImageScrollView(image: UIImage(named: "image")!)
override func viewDidLoad() {
view.backgroundColor = .black
view.addSubview(scrollView)
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
scrollView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
}
override func viewDidLayoutSubviews() {
scrollView.setZoomScale()
}
}
ImageScrollView.swift
import UIKit
final class ImageScrollView: UIScrollView {
private let imageView = UIImageView()
private var imageViewBottomConstraint = NSLayoutConstraint()
private var imageViewLeadingConstraint = NSLayoutConstraint()
private var imageViewTopConstraint = NSLayoutConstraint()
private var imageViewTrailingConstraint = NSLayoutConstraint()
required init(image: UIImage) {
super.init(frame: .zero)
imageView.image = image
imageView.sizeToFit()
addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
imageViewLeadingConstraint = imageView.leadingAnchor.constraint(equalTo: leadingAnchor)
imageViewTrailingConstraint = imageView.trailingAnchor.constraint(equalTo: trailingAnchor)
imageViewTopConstraint = imageView.topAnchor.constraint(equalTo: topAnchor)
imageViewBottomConstraint = imageView.bottomAnchor.constraint(equalTo: bottomAnchor)
NSLayoutConstraint.activate([imageViewLeadingConstraint, imageViewTrailingConstraint, imageViewTopConstraint, imageViewBottomConstraint])
contentInsetAdjustmentBehavior = .never // Adjust content according to safe area if necessary
showsVerticalScrollIndicator = false
showsHorizontalScrollIndicator = false
alwaysBounceHorizontal = true
alwaysBounceVertical = true
delegate = self
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Helper methods
func setZoomScale() {
let widthScale = frame.size.width / imageView.bounds.width
let heightScale = frame.size.height / imageView.bounds.height
let minScale = min(widthScale, heightScale)
minimumZoomScale = minScale
zoomScale = minScale
}
}
extension ImageScrollView: UIScrollViewDelegate {
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return imageView
}
func scrollViewDidZoom(_ scrollView: UIScrollView) {
let yOffset = max(0, (bounds.size.height - imageView.frame.height) / 2)
imageViewTopConstraint.constant = yOffset
imageViewBottomConstraint.constant = yOffset
let xOffset = max(0, (bounds.size.width - imageView.frame.width) / 2)
imageViewLeadingConstraint.constant = xOffset
imageViewTrailingConstraint.constant = xOffset
layoutIfNeeded()
}
}
Sources:
- Github / ImageScrollView
- raywenderlich.com / UIScrollView Tutorial: Getting Started
I have written an example application which also supports AutoLayout and Storyboards to demonstrate this behavior. I hope it saves everyone time trying to figure this out: http://rexstjohn.com/facebook-like-ios-photo-modal-gallery-swipe-gestures/.