123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500 |
- //
- // ALConfirmViewController.swift
- // ALCameraViewController
- //
- // Created by Alex Littlejohn on 2015/06/30.
- // Copyright (c) 2015 zero. All rights reserved.
- //
- //
- // Modified by Kevin Kieffer on 2019/08/06. Changes as follows: significantly updated the operation of this
- // class because as far as I could determine the subviews were not arranging themselves properly when the
- // device was rotated. Simplified this class by removing the centeringView and scrollView insets, and simply centering the
- // scrollView in the overall view, setting the scrollView content size = imageView frame size, and centering the cropOverlay
- // over the scrollView whenever the view finished laying out its subviews.
- //
- // Furthermore minimum scrollView zoom size was set based on the crop rectangle, but the initial view was set to fully
- // fit the image on the screen.
- //
- // A new aspectRatio constraint was created and all constraints were removed from the cropOverlay view in the .xib file.
- // The centeringView was also removed from the .xib file and the Confirm and Cancel buttons were moved closer to the edge.
- //
- // A center touch point is set on the CropOverlay if the CropParameters say its movable
- //
- // Lastly, the image cropping worked differently if the crop rectangle was out of the image bounds, depending on whether
- // the image came from a PHAsset or a UIImage. In the former, the crop maintained the aspect ratio of the crop rectangle
- // (possibly distorting the image), but in the latter, it truncated the crop rectangle to the bounds of the image, changing
- // the aspect ratio. Since maintaining the aspect ratio is preferred, a change to the UIImage extension was made to rescale the
- // cropped image back to the aspect ratio, which also possibly distorts the image but preserves the aspect ratio.
- import UIKit
- import Photos
- public class ConfirmViewController: UIViewController, UIScrollViewDelegate {
-
- var CROP_PADDING : CGFloat {
- switch UIDevice.current.userInterfaceIdiom {
- case .pad:
- return CGFloat(120)
- default:
- return CGFloat(30)
- }
- }
-
- let imageView = UIImageView()
-
- @IBOutlet weak var scrollView: UIScrollView!
- @IBOutlet weak var cropOverlay: CropOverlay!
- @IBOutlet weak var confirmButton: UIButton!
- @IBOutlet weak var cancelButton: UIButton!
- @IBOutlet weak var rotateButton: UIButton!
-
-
-
- var croppingParameters: CroppingParameters {
- didSet {
- cropOverlay.showsCenterPoint = croppingParameters.allowMoving
- cropOverlay.isResizable = croppingParameters.allowResizing
- cropOverlay.isMovable = croppingParameters.allowMoving
- cropOverlay.minimumSize = croppingParameters.minimumSize
- cropOverlay.showsButtons = croppingParameters.allowResizing
- }
- }
- public var onComplete: CameraViewCompletion?
- let asset: PHAsset?
- let image: UIImage?
-
- var didInitialAdjustCropOverlay = false
- var didInitialCenterCropOverlay = false
- public init(image: UIImage, croppingParameters: CroppingParameters) {
- self.croppingParameters = croppingParameters
- self.asset = nil
- self.image = image
- super.init(nibName: "ConfirmViewController", bundle: CameraGlobals.shared.bundle)
- }
-
- public init(asset: PHAsset, croppingParameters: CroppingParameters) {
- self.croppingParameters = croppingParameters
- self.asset = asset
- self.image = nil
- super.init(nibName: "ConfirmViewController", bundle: CameraGlobals.shared.bundle)
- }
-
- public required init?(coder aDecoder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-
- deinit {
- NotificationCenter.default.removeObserver(self)
- }
-
- public override var prefersStatusBarHidden: Bool {
- return true
- }
-
- public override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
- return UIStatusBarAnimation.slide
- }
-
- public override func viewDidLoad() {
- super.viewDidLoad()
- view.backgroundColor = UIColor.black
-
- scrollView.addSubview(imageView)
- scrollView.delegate = self
- scrollView.maximumZoomScale = croppingParameters.maximumZoom
-
- cropOverlay.showsCenterPoint = croppingParameters.allowMoving
- cropOverlay.isHidden = true
- cropOverlay.isResizable = croppingParameters.allowResizing
- cropOverlay.isMovable = croppingParameters.allowMoving
- cropOverlay.minimumSize = croppingParameters.minimumSize
- cropOverlay.showsButtons = croppingParameters.allowResizing
- if !croppingParameters.allowRotate {
- rotateButton.isHidden = true
- }
-
- let spinner = showSpinner()
-
- disable()
-
- if let asset = asset { //load full resolution image size
- _ = SingleImageFetcher()
- .setAsset(asset)
- .onSuccess { [weak self] image in
- self?.configureWithImage(image)
- self?.hideSpinner(spinner)
- self?.enable()
- }
- .onFailure { [weak self] error in
- self?.hideSpinner(spinner)
- }
- .fetch()
- } else if let image = image {
- configureWithImage(image)
- hideSpinner(spinner)
- enable()
- }
-
- NotificationCenter.default.addObserver(self, selector: #selector(orientationChanged), name: Notification.Name("UIDeviceOrientationDidChangeNotification"), object: nil)
- }
-
- @objc func orientationChanged() {
- centerCropFrame()
- }
-
- public override func viewWillLayoutSubviews() {
-
- if !didInitialAdjustCropOverlay || !cropOverlay.isResizable {
- adjustCropOverlay() //keep it centered and constrainted on orientation changes
- didInitialAdjustCropOverlay = true
- }
-
- }
-
- public override func viewDidLayoutSubviews() {
- super.viewDidLayoutSubviews()
-
-
- let (minscale, initscale) = calculateMinimumAndInitialScale()
-
- scrollView.contentSize = imageView.frame.size
- scrollView.minimumZoomScale = minscale
- scrollView.zoomScale = initscale
- self.centerScrollViewContents()
-
- if !cropOverlay.isResizable || !didInitialCenterCropOverlay {
- self.centerCropFrame()
- didInitialCenterCropOverlay = true
- }
-
- }
-
- private func adjustCropOverlay() {
- switch UIDevice.current.orientation {
- case .landscapeLeft, .landscapeRight:
- cropOverlay.frame.size.height = view.frame.height - CROP_PADDING //height is constrained in landscale
- cropOverlay.frame.size.width = cropOverlay.frame.size.height / croppingParameters.aspectRatioHeightToWidth
- default:
- cropOverlay.frame.size.width = view.frame.width - CROP_PADDING //width is constrained in portrait
- cropOverlay.frame.size.height = cropOverlay.frame.size.width * croppingParameters.aspectRatioHeightToWidth
- }
- }
-
- private func configureWithImage(_ image: UIImage) {
- cropOverlay.isHidden = !croppingParameters.isEnabled
-
- buttonActions()
-
- imageView.image = image
- imageView.sizeToFit()
- view.setNeedsLayout()
- }
-
-
- //Returns a tuple containing the minimum scale and the desired initial scale of the scroll view
- private func calculateMinimumAndInitialScale() -> (CGFloat, CGFloat) {
- guard let image = imageView.image else {
- return (1,1)
- }
-
- //The initial scale will fit the entire image on the screen in either orientation
- let size = view.bounds
- let scaleWidth = size.width / image.size.width
- let scaleHeight = size.height / image.size.height
-
- let minSizeWithoutCrop = min(scaleWidth, scaleHeight)
-
-
- //If cropping enabled, the minimum scale fits the image into the crop rectangle, otherwise
- //its the same as in the initial scale
- if croppingParameters.isEnabled {
-
- let cropSize = cropOverlay.frame.size
- let cropScaleWidth = (cropSize.width - CROP_PADDING) / image.size.width
- let cropScaleHeight = (cropSize.height - CROP_PADDING) / image.size.height
-
- let minSizeWithCrop = min(cropScaleWidth, cropScaleHeight)
-
- return (minSizeWithCrop, minSizeWithoutCrop)
- }
- else {
- return (minSizeWithoutCrop, minSizeWithoutCrop)
- }
-
- }
-
- private func calculateScrollViewInsets(_ frame: CGRect) -> UIEdgeInsets {
- let bottom = view.frame.height - (frame.origin.y + frame.height)
- let right = view.frame.width - (frame.origin.x + frame.width)
- let insets = UIEdgeInsets(top: frame.origin.y, left: frame.origin.x, bottom: bottom, right: right)
- return insets
- }
-
- private func centerImageViewOnRotate() {
- if croppingParameters.isEnabled {
- let size = cropOverlay.frame.size
- let scrollInsets = scrollView.contentInset
- let imageSize = imageView.frame.size
- var contentOffset = CGPoint(x: -scrollInsets.left, y: -scrollInsets.top)
- contentOffset.x -= (size.width - imageSize.width) / 2
- contentOffset.y -= (size.height - imageSize.height) / 2
- scrollView.contentOffset = contentOffset
- }
- }
-
- private func centerCropFrame() {
- let size = scrollView.frame.size
- let cropSize = cropOverlay.frame.size
- var origin = CGPoint.zero
-
- if cropSize.width < size.width {
- origin.x = (size.width - cropSize.width) / 2
- }
-
- if cropSize.height < size.height {
- origin.y = (size.height - cropSize.height) / 2
- }
-
- cropOverlay.frame.origin = origin
- }
-
- private func centerScrollViewContents() {
- let size = scrollView.frame.size
- let imageSize = imageView.frame.size
- var imageOrigin = CGPoint.zero
-
- if imageSize.width < size.width {
- imageOrigin.x = (size.width - imageSize.width) / 2
- }
-
- if imageSize.height < size.height {
- imageOrigin.y = (size.height - imageSize.height) / 2
- }
-
- imageView.frame.origin = imageOrigin
- }
-
- private func buttonActions() {
- confirmButton.action = { [weak self] in self?.confirmPhoto() }
- cancelButton.action = { [weak self] in self?.cancel() }
- rotateButton.action = { [weak self] in self?.rotateRight() }
- }
-
- internal func rotateRight() {
- if let rotatedImage = imageView.image?.rotate() {
- configureWithImage(rotatedImage)
- centerScrollViewContents()
- }
- }
-
- internal func cancel() {
- cropOverlay.removeFromSuperview() //remove overlay while processing
- onComplete?(nil, nil)
- }
-
- internal func confirmPhoto() {
-
- guard let image = imageView.image else {
- return
- }
-
- disable()
-
- imageView.isHidden = true
-
- let spinner = showSpinner()
-
- if croppingParameters.isEnabled {
- let cropRect = makeProportionalCropRect()
- let resizedCropRect = CGRect(x: (image.size.width) * cropRect.origin.x,
- y: (image.size.height) * cropRect.origin.y,
- width: (image.size.width * cropRect.width),
- height: (image.size.height * cropRect.height))
-
- DispatchQueue.global(qos: .userInitiated).async {
- let croppedImage = image.crop(rect: resizedCropRect) //This can take long time and block UI
- DispatchQueue.main.async {
- self.onComplete?(croppedImage, self.asset)
- self.hideSpinner(spinner)
- }
- }
-
- }
- else {
- onComplete?(image, self.asset)
- hideSpinner(spinner)
- }
-
- cropOverlay.removeFromSuperview() //remove overlay while processing
- }
-
- public func viewForZooming(in scrollView: UIScrollView) -> UIView? {
- return imageView
- }
-
- public func scrollViewDidZoom(_ scrollView: UIScrollView) {
- centerScrollViewContents()
- }
-
-
- override public func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
- view.setNeedsLayout()
- }
-
- func showSpinner() -> UIActivityIndicatorView {
- let spinner = UIActivityIndicatorView()
- spinner.style = .white
- spinner.center = view.center
- spinner.startAnimating()
-
- view.addSubview(spinner)
- view.bringSubviewToFront(spinner)
-
- return spinner
- }
-
- func hideSpinner(_ spinner: UIActivityIndicatorView) {
- spinner.stopAnimating()
- spinner.removeFromSuperview()
- }
-
- func disable() {
- confirmButton.isEnabled = false
- cancelButton.isEnabled = false
- }
-
- func enable() {
- confirmButton.isEnabled = true
- cancelButton.isEnabled = true
- }
-
- func showNoImageScreen(_ error: NSError) {
- let permissionsView = PermissionsView(frame: view.bounds)
-
- let desc = localizedString("error.cant-fetch-photo.description")
-
- permissionsView.configureInView(view, title: error.localizedDescription, description: desc, completion: { [weak self] in self?.cancel() })
- }
-
- private func makeProportionalCropRect() -> CGRect {
- var cropRect = CGRect(x: cropOverlay.frame.origin.x + cropOverlay.outterGap,
- y: cropOverlay.frame.origin.y + cropOverlay.outterGap,
- width: cropOverlay.frame.size.width - 2 * cropOverlay.outterGap,
- height: cropOverlay.frame.size.height - 2 * cropOverlay.outterGap)
-
- cropRect.origin.x += scrollView.contentOffset.x - imageView.frame.origin.x
- cropRect.origin.y += scrollView.contentOffset.y - imageView.frame.origin.y
- let normalizedX = cropRect.origin.x / imageView.frame.width
- let normalizedY = cropRect.origin.y / imageView.frame.height
- let extraWidth = CGFloat(0) //fabs(cropRect.origin.x)
- let extraHeight = CGFloat(0) //fabs(cropRect.origin.y)
- let normalizedWidth = (cropRect.width + extraWidth) / imageView.frame.width
- let normalizedHeight = (cropRect.height + extraHeight) / imageView.frame.height
-
- return CGRect(x: normalizedX, y: normalizedY, width: normalizedWidth, height: normalizedHeight)
- }
-
- }
- extension UIImage {
-
- func crop(rect: CGRect) -> UIImage {
- var rectTransform: CGAffineTransform
- switch imageOrientation {
- case .left:
- rectTransform = CGAffineTransform(rotationAngle: radians(90)).translatedBy(x: 0, y: -size.height)
- case .right:
- rectTransform = CGAffineTransform(rotationAngle: radians(-90)).translatedBy(x: -size.width, y: 0)
- case .down:
- rectTransform = CGAffineTransform(rotationAngle: radians(-180)).translatedBy(x: -size.width, y: -size.height)
- default:
- rectTransform = CGAffineTransform.identity
- }
-
- rectTransform = rectTransform.scaledBy(x: scale, y: scale)
-
- let cropAspect = rect.height / rect.width
-
- if let cropped = cgImage?.cropping(to: rect.applying(rectTransform)) {
-
- let cropImage = UIImage(cgImage: cropped, scale: scale, orientation: imageOrientation).fixOrientation()
-
-
- //Rescale the cropped portion to maintain the crop aspect ratio
- let currentAspect = cropImage.size.height / cropImage.size.width
-
- return cropImage.scaledBy(size: CGSize(width: cropImage.size.width, height: cropImage.size.height * cropAspect / currentAspect)) ?? self
-
-
- }
-
- return self
- }
-
- func fixOrientation() -> UIImage {
- if imageOrientation == .up {
- return self
- }
-
- UIGraphicsBeginImageContextWithOptions(size, false, scale)
- draw(in: CGRect(origin: .zero, size: size))
- let normalizedImage: UIImage = UIGraphicsGetImageFromCurrentImageContext() ?? self
- UIGraphicsEndImageContext()
-
- return normalizedImage
- }
-
- func scaledBy(size: CGSize) -> UIImage? {
- let hasAlpha = false
- let scale: CGFloat = 0.0 // Automatically use scale factor of main screen
-
- UIGraphicsBeginImageContextWithOptions(size, !hasAlpha, scale)
- self.draw(in: CGRect(origin: CGPoint(x: 0, y: 0), size: size))
-
- let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
- UIGraphicsEndImageContext()
-
- return scaledImage
- }
-
- //Rotate by 90 degrees
- func rotate() -> UIImage? {
-
- let radians = Float.pi/2
-
- var newSize = CGRect(origin: CGPoint.zero, size: CGSize(width: self.size.width, height: self.size.height)).applying(CGAffineTransform(rotationAngle: CGFloat(radians))).size
-
- // Trim off the extremely small float value to prevent core graphics from rounding it up
- newSize.width = floor(newSize.width)
- newSize.height = floor(newSize.height)
- UIGraphicsBeginImageContextWithOptions(newSize, false, self.scale)
- let context = UIGraphicsGetCurrentContext()!
-
- // Move origin to middle
- context.translateBy(x: newSize.width/2, y: newSize.height/2)
- // Rotate around middle
- context.rotate(by: CGFloat(radians))
- // Draw the image at its center
- self.draw(in: CGRect(x: -self.size.width/2, y: -self.size.height/2, width: self.size.width, height: self.size.height))
- let newImage = UIGraphicsGetImageFromCurrentImageContext()
- UIGraphicsEndImageContext()
- return newImage
- }
- }
|