// // CropView.swift // CropViewController // // Created by Guilherme Moura on 2/25/16. // Copyright © 2016 Reefactor, Inc. All rights reserved. // Credit https://github.com/sprint84/PhotoCropEditor import UIKit import AVFoundation open class CropView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelegate, CropRectViewDelegate { open var image: UIImage? { didSet { if image != nil { imageSize = image!.size } imageView?.removeFromSuperview() imageView = nil zoomingView?.removeFromSuperview() zoomingView = nil setNeedsLayout() } } open var imageView: UIView? { didSet { if let view = imageView , image == nil { imageSize = view.frame.size } usingCustomImageView = true setNeedsLayout() } } open var croppedImage: UIImage? { return image?.rotatedImageWithTransform(rotation, croppedToRect: zoomedCropRect()) } open var keepAspectRatio = false { didSet { cropRectView.keepAspectRatio = keepAspectRatio } } open var cropAspectRatio: CGFloat { set { setCropAspectRatio(newValue, shouldCenter: true) } get { let rect = scrollView.frame let width = rect.width let height = rect.height return width / height } } open var rotation: CGAffineTransform { guard let imgView = imageView else { return CGAffineTransform.identity } return imgView.transform } open var rotationAngle: CGFloat { set { imageView?.transform = CGAffineTransform(rotationAngle: newValue) } get { return atan2(rotation.b, rotation.a) } } open var cropRect: CGRect { set { zoomToCropRect(newValue) } get { return scrollView.frame } } open var imageCropRect = CGRect.zero { didSet { resetCropRect() let scale = min(scrollView.frame.width / imageSize.width, scrollView.frame.height / imageSize.height) let x = imageCropRect.minX * scale + scrollView.frame.minX let y = imageCropRect.minY * scale + scrollView.frame.minY let width = imageCropRect.width * scale let height = imageCropRect.height * scale let rect = CGRect(x: x, y: y, width: width, height: height) let intersection = rect.intersection(scrollView.frame) if !intersection.isNull { cropRect = intersection } } } open var resizeEnabled = true { didSet { cropRectView.enableResizing(resizeEnabled) } } open var showCroppedArea = true { didSet { layoutIfNeeded() scrollView.clipsToBounds = !showCroppedArea showOverlayView(showCroppedArea) } } open var rotationGestureRecognizer: UIRotationGestureRecognizer! fileprivate var imageSize = CGSize(width: 1.0, height: 1.0) fileprivate var scrollView: UIScrollView! fileprivate var zoomingView: UIView? fileprivate let cropRectView = CropRectView() fileprivate let topOverlayView = UIView() fileprivate let leftOverlayView = UIView() fileprivate let rightOverlayView = UIView() fileprivate let bottomOverlayView = UIView() fileprivate var insetRect = CGRect.zero fileprivate var editingRect = CGRect.zero fileprivate var interfaceOrientation = UIApplication.shared.statusBarOrientation fileprivate var resizing = false fileprivate var usingCustomImageView = false fileprivate let MarginTop: CGFloat = 37.0 fileprivate let MarginLeft: CGFloat = 20.0 public override init(frame: CGRect) { super.init(frame: frame) initialize() } public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) initialize() } fileprivate func initialize() { autoresizingMask = [.flexibleWidth, .flexibleHeight] backgroundColor = UIColor.clear scrollView = UIScrollView(frame: bounds) scrollView.delegate = self scrollView.autoresizingMask = [.flexibleTopMargin, .flexibleLeftMargin, .flexibleBottomMargin, .flexibleRightMargin] scrollView.backgroundColor = UIColor.clear scrollView.maximumZoomScale = 20.0 scrollView.minimumZoomScale = 1.0 scrollView.showsHorizontalScrollIndicator = false scrollView.showsVerticalScrollIndicator = false scrollView.bounces = false scrollView.bouncesZoom = false scrollView.clipsToBounds = false addSubview(scrollView) rotationGestureRecognizer = UIRotationGestureRecognizer(target: self, action: #selector(CropView.handleRotation(_:))) rotationGestureRecognizer?.delegate = self scrollView.addGestureRecognizer(rotationGestureRecognizer) cropRectView.delegate = self addSubview(cropRectView) showOverlayView(showCroppedArea) addSubview(topOverlayView) addSubview(leftOverlayView) addSubview(rightOverlayView) addSubview(bottomOverlayView) } open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if !isUserInteractionEnabled { return nil } if let hitView = cropRectView.hitTest(convert(point, to: cropRectView), with: event) { return hitView } let locationInImageView = convert(point, to: zoomingView) let zoomedPoint = CGPoint(x: locationInImageView.x * scrollView.zoomScale, y: locationInImageView.y * scrollView.zoomScale) if zoomingView!.frame.contains(zoomedPoint) { return scrollView } return super.hitTest(point, with: event) } open override func layoutSubviews() { super.layoutSubviews() let interfaceOrientation = UIApplication.shared.statusBarOrientation if image == nil && imageView == nil { return } setupEditingRect() if imageView == nil { if interfaceOrientation.isPortrait { insetRect = bounds.insetBy(dx: MarginLeft, dy: MarginTop) } else { insetRect = bounds.insetBy(dx: MarginLeft, dy: MarginLeft) } if !showCroppedArea { insetRect = editingRect } setupZoomingView() setupImageView() } else if usingCustomImageView { if interfaceOrientation.isPortrait { insetRect = bounds.insetBy(dx: MarginLeft, dy: MarginTop) } else { insetRect = bounds.insetBy(dx: MarginLeft, dy: MarginLeft) } if !showCroppedArea { insetRect = editingRect } setupZoomingView() imageView?.frame = zoomingView!.bounds zoomingView?.addSubview(imageView!) usingCustomImageView = false } if !resizing { layoutCropRectViewWithCropRect(scrollView.frame) if self.interfaceOrientation != interfaceOrientation { zoomToCropRect(scrollView.frame) } } self.interfaceOrientation = interfaceOrientation } open func setRotationAngle(_ rotationAngle: CGFloat, snap: Bool) { var rotation = rotationAngle if snap { rotation = nearbyint(rotationAngle / CGFloat(Double.pi/2)) * CGFloat(Double.pi/2) } self.rotationAngle = rotation } open func resetCropRect() { resetCropRectAnimated(false) } open func resetCropRectAnimated(_ animated: Bool) { if animated { UIView.beginAnimations(nil, context: nil) UIView.setAnimationDuration(0.25) UIView.setAnimationBeginsFromCurrentState(true) } imageView?.transform = CGAffineTransform.identity let contentSize = scrollView.contentSize let initialRect = CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height) scrollView.zoom(to: initialRect, animated: false) layoutCropRectViewWithCropRect(scrollView.bounds) if animated { UIView.commitAnimations() } } open func zoomedCropRect() -> CGRect { let cropRect = convert(scrollView.frame, to: zoomingView) var ratio: CGFloat = 1.0 let orientation = UIApplication.shared.statusBarOrientation if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiom.pad || orientation.isPortrait) { ratio = AVMakeRect(aspectRatio: imageSize, insideRect: insetRect).width / imageSize.width } else { ratio = AVMakeRect(aspectRatio: imageSize, insideRect: insetRect).height / imageSize.height } let zoomedCropRect = CGRect(x: cropRect.origin.x / ratio, y: cropRect.origin.y / ratio, width: cropRect.size.width / ratio, height: cropRect.size.height / ratio) return zoomedCropRect } open func croppedImage(_ image: UIImage) -> UIImage { imageSize = image.size return image.rotatedImageWithTransform(rotation, croppedToRect: zoomedCropRect()) } @objc func handleRotation(_ gestureRecognizer: UIRotationGestureRecognizer) { if let imageView = imageView { let rotation = gestureRecognizer.rotation let transform = imageView.transform.rotated(by: rotation) imageView.transform = transform gestureRecognizer.rotation = 0.0 } switch gestureRecognizer.state { case .began, .changed: cropRectView.showsGridMinor = true default: cropRectView.showsGridMinor = false } } // MARK: - Private methods fileprivate func showOverlayView(_ show: Bool) { let color = show ? UIColor(white: 0.0, alpha: 0.4) : UIColor.clear topOverlayView.backgroundColor = color leftOverlayView.backgroundColor = color rightOverlayView.backgroundColor = color bottomOverlayView.backgroundColor = color } fileprivate func setupEditingRect() { let interfaceOrientation = UIApplication.shared.statusBarOrientation if interfaceOrientation.isPortrait { editingRect = bounds.insetBy(dx: MarginLeft, dy: MarginTop) } else { editingRect = bounds.insetBy(dx: MarginLeft, dy: MarginLeft) } if !showCroppedArea { editingRect = CGRect(x: 0, y: 0, width: bounds.width, height: bounds.height) } } fileprivate func setupZoomingView() { let cropRect = AVMakeRect(aspectRatio: imageSize, insideRect: insetRect) scrollView.frame = cropRect scrollView.contentSize = cropRect.size zoomingView = UIView(frame: scrollView.bounds) zoomingView?.backgroundColor = .clear scrollView.addSubview(zoomingView!) } fileprivate func setupImageView() { let imageView = UIImageView(frame: zoomingView!.bounds) imageView.backgroundColor = .clear imageView.contentMode = .scaleAspectFit imageView.image = image zoomingView?.addSubview(imageView) self.imageView = imageView usingCustomImageView = false } fileprivate func layoutCropRectViewWithCropRect(_ cropRect: CGRect) { cropRectView.frame = cropRect layoutOverlayViewsWithCropRect(cropRect) } fileprivate func layoutOverlayViewsWithCropRect(_ cropRect: CGRect) { topOverlayView.frame = CGRect(x: 0, y: 0, width: bounds.width, height: cropRect.minY) leftOverlayView.frame = CGRect(x: 0, y: cropRect.minY, width: cropRect.minX, height: cropRect.height) rightOverlayView.frame = CGRect(x: cropRect.maxX, y: cropRect.minY, width: bounds.width - cropRect.maxX, height: cropRect.height) bottomOverlayView.frame = CGRect(x: 0, y: cropRect.maxY, width: bounds.width, height: bounds.height - cropRect.maxY) } fileprivate func zoomToCropRect(_ toRect: CGRect) { zoomToCropRect(toRect, shouldCenter: false, animated: true) } fileprivate func zoomToCropRect(_ toRect: CGRect, shouldCenter: Bool, animated: Bool, completion: (() -> Void)? = nil) { if scrollView.frame.equalTo(toRect) { return } let width = toRect.width let height = toRect.height let scale = min(editingRect.width / width, editingRect.height / height) let scaledWidth = width * scale let scaledHeight = height * scale let cropRect = CGRect(x: (bounds.width - scaledWidth) / 2.0, y: (bounds.height - scaledHeight) / 2.0, width: scaledWidth, height: scaledHeight) var zoomRect = convert(toRect, to: zoomingView) zoomRect.size.width = cropRect.width / (scrollView.zoomScale * scale) zoomRect.size.height = cropRect.height / (scrollView.zoomScale * scale) if let imgView = imageView , shouldCenter { let imageViewBounds = imgView.bounds zoomRect.origin.x = (imageViewBounds.width / 2.0) - (zoomRect.width / 2.0) zoomRect.origin.y = (imageViewBounds.height / 2.0) - (zoomRect.height / 2.0) } var duration = 0.0 if animated { duration = 0.25 } UIView.animate(withDuration: duration, delay: 0.0, options: .beginFromCurrentState, animations: { [unowned self] in self.scrollView.bounds = cropRect self.scrollView.zoom(to: zoomRect, animated: false) self.layoutCropRectViewWithCropRect(cropRect) }) { finished in completion?() } } fileprivate func cappedCropRectInImageRectWithCropRectView(_ cropRectView: CropRectView) -> CGRect { var cropRect = cropRectView.frame let rect = convert(cropRect, to: scrollView) if rect.minX < zoomingView!.frame.minX { cropRect.origin.x = scrollView.convert(zoomingView!.frame, to: self).minX let cappedWidth = rect.maxX let height = !keepAspectRatio ? cropRect.size.height : cropRect.size.height * (cappedWidth / cropRect.size.width) cropRect.size = CGSize(width: cappedWidth, height: height) } if rect.minY < zoomingView!.frame.minY { cropRect.origin.y = scrollView.convert(zoomingView!.frame, to: self).minY let cappedHeight = rect.maxY let width = !keepAspectRatio ? cropRect.size.width : cropRect.size.width * (cappedHeight / cropRect.size.height) cropRect.size = CGSize(width: width, height: cappedHeight) } if rect.maxX > zoomingView!.frame.maxX { let cappedWidth = scrollView.convert(zoomingView!.frame, to: self).maxX - cropRect.minX let height = !keepAspectRatio ? cropRect.size.height : cropRect.size.height * (cappedWidth / cropRect.size.width) cropRect.size = CGSize(width: cappedWidth, height: height) } if rect.maxY > zoomingView!.frame.maxY { let cappedHeight = scrollView.convert(zoomingView!.frame, to: self).maxY - cropRect.minY let width = !keepAspectRatio ? cropRect.size.width : cropRect.size.width * (cappedHeight / cropRect.size.height) cropRect.size = CGSize(width: width, height: cappedHeight) } return cropRect } fileprivate func automaticZoomIfEdgeTouched(_ cropRect: CGRect) { if cropRect.minX < editingRect.minX - 5.0 || cropRect.maxX > editingRect.maxX + 5.0 || cropRect.minY < editingRect.minY - 5.0 || cropRect.maxY > editingRect.maxY + 5.0 { UIView.animate(withDuration: 1.0, delay: 0.0, options: .beginFromCurrentState, animations: { [unowned self] in self.zoomToCropRect(self.cropRectView.frame) }, completion: nil) } } fileprivate func setCropAspectRatio(_ ratio: CGFloat, shouldCenter: Bool) { var cropRect = scrollView.frame var width = cropRect.width var height = cropRect.height if ratio <= 1.0 { width = height * ratio if width > imageView!.bounds.width { width = cropRect.width height = width / ratio } } else { height = width / ratio if height > imageView!.bounds.height { height = cropRect.height width = height * ratio } } cropRect.size = CGSize(width: width, height: height) zoomToCropRect(cropRect, shouldCenter: shouldCenter, animated: false) { let scale = self.scrollView.zoomScale self.scrollView.minimumZoomScale = scale } } // MARK: - CropView delegate methods func cropRectViewDidBeginEditing(_ view: CropRectView) { resizing = true } func cropRectViewDidChange(_ view: CropRectView) { let cropRect = cappedCropRectInImageRectWithCropRectView(view) layoutCropRectViewWithCropRect(cropRect) automaticZoomIfEdgeTouched(cropRect) } func cropRectViewDidEndEditing(_ view: CropRectView) { resizing = false zoomToCropRect(cropRectView.frame) } // MARK: - ScrollView delegate methods open func viewForZooming(in scrollView: UIScrollView) -> UIView? { return zoomingView } open func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { let contentOffset = scrollView.contentOffset targetContentOffset.pointee = contentOffset } // MARK: - Gesture Recognizer delegate methods open func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return true } }