Created
August 28, 2018 07:17
-
-
Save AmirDaliri/2050e136856777a0dbd757c89098733b to your computer and use it in GitHub Desktop.
ZoomImageView
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// ZoomImageView.swift | |
// | |
// | |
// Created by Amir Daliri on 6/5/1397 AP. | |
// Copyright © 1397 Adl. All rights reserved. | |
// | |
import UIKit | |
open class ZoomImageView : UIScrollView, UIScrollViewDelegate { | |
public enum ZoomMode { | |
case fit | |
case fill | |
} | |
// MARK: - Properties | |
public let imageView = UIImageView() | |
public var zoomMode: ZoomMode = .fit { | |
didSet { | |
updateImageView() | |
} | |
} | |
open var image: UIImage? { | |
get { | |
return imageView.image | |
} | |
set { | |
let oldImage = imageView.image | |
imageView.image = newValue | |
if oldImage?.size != newValue?.size { | |
oldSize = nil | |
updateImageView() | |
} | |
} | |
} | |
open override var intrinsicContentSize: CGSize { | |
return imageView.intrinsicContentSize | |
} | |
private var oldSize: CGSize? | |
// MARK: - Initializers | |
public override init(frame: CGRect) { | |
super.init(frame: frame) | |
setup() | |
} | |
public init(image: UIImage) { | |
super.init(frame: CGRect.zero) | |
self.image = image | |
setup() | |
} | |
public required init?(coder aDecoder: NSCoder) { | |
super.init(coder: aDecoder) | |
setup() | |
} | |
// MARK: - Functions | |
open func scrollToCenter() { | |
let centerOffset = CGPoint( | |
x: (contentSize.width / 2) - (bounds.width / 2), | |
y: (contentSize.height / 2) - (bounds.height / 2) | |
) | |
contentOffset = centerOffset | |
} | |
open func setup() { | |
#if swift(>=3.2) | |
if #available(iOS 11, *) { | |
contentInsetAdjustmentBehavior = .never | |
} | |
#endif | |
backgroundColor = UIColor.clear | |
delegate = self | |
imageView.contentMode = .scaleAspectFill | |
showsVerticalScrollIndicator = false | |
showsHorizontalScrollIndicator = false | |
decelerationRate = UIScrollView.DecelerationRate.fast | |
addSubview(imageView) | |
let doubleTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap)) | |
doubleTapGesture.numberOfTapsRequired = 2 | |
addGestureRecognizer(doubleTapGesture) | |
} | |
open override func didMoveToSuperview() { | |
super.didMoveToSuperview() | |
} | |
open override func layoutSubviews() { | |
super.layoutSubviews() | |
if imageView.image != nil && oldSize != bounds.size { | |
updateImageView() | |
oldSize = bounds.size | |
} | |
if imageView.frame.width <= bounds.width { | |
imageView.center.x = bounds.width * 0.5 | |
} | |
if imageView.frame.height <= bounds.height { | |
imageView.center.y = bounds.height * 0.5 | |
} | |
} | |
open override func updateConstraints() { | |
super.updateConstraints() | |
updateImageView() | |
} | |
private func updateImageView() { | |
func fitSize(aspectRatio: CGSize, boundingSize: CGSize) -> CGSize { | |
let widthRatio = (boundingSize.width / aspectRatio.width) | |
let heightRatio = (boundingSize.height / aspectRatio.height) | |
var boundingSize = boundingSize | |
if widthRatio < heightRatio { | |
boundingSize.height = boundingSize.width / aspectRatio.width * aspectRatio.height | |
} | |
else if (heightRatio < widthRatio) { | |
boundingSize.width = boundingSize.height / aspectRatio.height * aspectRatio.width | |
} | |
return CGSize(width: ceil(boundingSize.width), height: ceil(boundingSize.height)) | |
} | |
func fillSize(aspectRatio: CGSize, minimumSize: CGSize) -> CGSize { | |
let widthRatio = (minimumSize.width / aspectRatio.width) | |
let heightRatio = (minimumSize.height / aspectRatio.height) | |
var minimumSize = minimumSize | |
if widthRatio > heightRatio { | |
minimumSize.height = minimumSize.width / aspectRatio.width * aspectRatio.height | |
} | |
else if (heightRatio > widthRatio) { | |
minimumSize.width = minimumSize.height / aspectRatio.height * aspectRatio.width | |
} | |
return CGSize(width: ceil(minimumSize.width), height: ceil(minimumSize.height)) | |
} | |
guard let image = imageView.image else { return } | |
var size: CGSize | |
switch zoomMode { | |
case .fit: | |
size = fitSize(aspectRatio: image.size, boundingSize: bounds.size) | |
case .fill: | |
size = fillSize(aspectRatio: image.size, minimumSize: bounds.size) | |
} | |
size.height = round(size.height) | |
size.width = round(size.width) | |
zoomScale = 1 | |
maximumZoomScale = image.size.width / size.width | |
imageView.bounds.size = size | |
contentSize = size | |
imageView.center = contentCenter(forBoundingSize: bounds.size, contentSize: contentSize) | |
} | |
@objc private func handleDoubleTap() { | |
if self.zoomScale == 1 { | |
setZoomScale(max(1, maximumZoomScale / 3), animated: true) | |
} else { | |
setZoomScale(1, animated: true) | |
} | |
} | |
// MARK: - UIScrollViewDelegate | |
@objc dynamic public func scrollViewDidZoom(_ scrollView: UIScrollView) { | |
imageView.center = contentCenter(forBoundingSize: bounds.size, contentSize: contentSize) | |
} | |
@objc dynamic public func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) { | |
} | |
@objc dynamic public func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) { | |
} | |
@objc dynamic public func viewForZooming(in scrollView: UIScrollView) -> UIView? { | |
return imageView | |
} | |
@inline(__always) | |
private func contentCenter(forBoundingSize boundingSize: CGSize, contentSize: CGSize) -> CGPoint { | |
/// 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. | |
let horizontalOffest = (boundingSize.width > contentSize.width) ? ((boundingSize.width - contentSize.width) * 0.5): 0.0 | |
let verticalOffset = (boundingSize.height > contentSize.height) ? ((boundingSize.height - contentSize.height) * 0.5): 0.0 | |
return CGPoint(x: contentSize.width * 0.5 + horizontalOffest, y: contentSize.height * 0.5 + verticalOffset) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment