ConfirmViewController.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500
  1. //
  2. // ALConfirmViewController.swift
  3. // ALCameraViewController
  4. //
  5. // Created by Alex Littlejohn on 2015/06/30.
  6. // Copyright (c) 2015 zero. All rights reserved.
  7. //
  8. //
  9. // Modified by Kevin Kieffer on 2019/08/06. Changes as follows: significantly updated the operation of this
  10. // class because as far as I could determine the subviews were not arranging themselves properly when the
  11. // device was rotated. Simplified this class by removing the centeringView and scrollView insets, and simply centering the
  12. // scrollView in the overall view, setting the scrollView content size = imageView frame size, and centering the cropOverlay
  13. // over the scrollView whenever the view finished laying out its subviews.
  14. //
  15. // Furthermore minimum scrollView zoom size was set based on the crop rectangle, but the initial view was set to fully
  16. // fit the image on the screen.
  17. //
  18. // A new aspectRatio constraint was created and all constraints were removed from the cropOverlay view in the .xib file.
  19. // The centeringView was also removed from the .xib file and the Confirm and Cancel buttons were moved closer to the edge.
  20. //
  21. // A center touch point is set on the CropOverlay if the CropParameters say its movable
  22. //
  23. // Lastly, the image cropping worked differently if the crop rectangle was out of the image bounds, depending on whether
  24. // the image came from a PHAsset or a UIImage. In the former, the crop maintained the aspect ratio of the crop rectangle
  25. // (possibly distorting the image), but in the latter, it truncated the crop rectangle to the bounds of the image, changing
  26. // the aspect ratio. Since maintaining the aspect ratio is preferred, a change to the UIImage extension was made to rescale the
  27. // cropped image back to the aspect ratio, which also possibly distorts the image but preserves the aspect ratio.
  28. import UIKit
  29. import Photos
  30. public class ConfirmViewController: UIViewController, UIScrollViewDelegate {
  31. var CROP_PADDING : CGFloat {
  32. switch UIDevice.current.userInterfaceIdiom {
  33. case .pad:
  34. return CGFloat(120)
  35. default:
  36. return CGFloat(30)
  37. }
  38. }
  39. let imageView = UIImageView()
  40. @IBOutlet weak var scrollView: UIScrollView!
  41. @IBOutlet weak var cropOverlay: CropOverlay!
  42. @IBOutlet weak var confirmButton: UIButton!
  43. @IBOutlet weak var cancelButton: UIButton!
  44. @IBOutlet weak var rotateButton: UIButton!
  45. var croppingParameters: CroppingParameters {
  46. didSet {
  47. cropOverlay.showsCenterPoint = croppingParameters.allowMoving
  48. cropOverlay.isResizable = croppingParameters.allowResizing
  49. cropOverlay.isMovable = croppingParameters.allowMoving
  50. cropOverlay.minimumSize = croppingParameters.minimumSize
  51. cropOverlay.showsButtons = croppingParameters.allowResizing
  52. }
  53. }
  54. public var onComplete: CameraViewCompletion?
  55. let asset: PHAsset?
  56. let image: UIImage?
  57. var didInitialAdjustCropOverlay = false
  58. var didInitialCenterCropOverlay = false
  59. public init(image: UIImage, croppingParameters: CroppingParameters) {
  60. self.croppingParameters = croppingParameters
  61. self.asset = nil
  62. self.image = image
  63. super.init(nibName: "ConfirmViewController", bundle: CameraGlobals.shared.bundle)
  64. }
  65. public init(asset: PHAsset, croppingParameters: CroppingParameters) {
  66. self.croppingParameters = croppingParameters
  67. self.asset = asset
  68. self.image = nil
  69. super.init(nibName: "ConfirmViewController", bundle: CameraGlobals.shared.bundle)
  70. }
  71. public required init?(coder aDecoder: NSCoder) {
  72. fatalError("init(coder:) has not been implemented")
  73. }
  74. deinit {
  75. NotificationCenter.default.removeObserver(self)
  76. }
  77. public override var prefersStatusBarHidden: Bool {
  78. return true
  79. }
  80. public override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
  81. return UIStatusBarAnimation.slide
  82. }
  83. public override func viewDidLoad() {
  84. super.viewDidLoad()
  85. view.backgroundColor = UIColor.black
  86. scrollView.addSubview(imageView)
  87. scrollView.delegate = self
  88. scrollView.maximumZoomScale = croppingParameters.maximumZoom
  89. cropOverlay.showsCenterPoint = croppingParameters.allowMoving
  90. cropOverlay.isHidden = true
  91. cropOverlay.isResizable = croppingParameters.allowResizing
  92. cropOverlay.isMovable = croppingParameters.allowMoving
  93. cropOverlay.minimumSize = croppingParameters.minimumSize
  94. cropOverlay.showsButtons = croppingParameters.allowResizing
  95. if !croppingParameters.allowRotate {
  96. rotateButton.isHidden = true
  97. }
  98. let spinner = showSpinner()
  99. disable()
  100. if let asset = asset { //load full resolution image size
  101. _ = SingleImageFetcher()
  102. .setAsset(asset)
  103. .onSuccess { [weak self] image in
  104. self?.configureWithImage(image)
  105. self?.hideSpinner(spinner)
  106. self?.enable()
  107. }
  108. .onFailure { [weak self] error in
  109. self?.hideSpinner(spinner)
  110. }
  111. .fetch()
  112. } else if let image = image {
  113. configureWithImage(image)
  114. hideSpinner(spinner)
  115. enable()
  116. }
  117. NotificationCenter.default.addObserver(self, selector: #selector(orientationChanged), name: Notification.Name("UIDeviceOrientationDidChangeNotification"), object: nil)
  118. }
  119. @objc func orientationChanged() {
  120. centerCropFrame()
  121. }
  122. public override func viewWillLayoutSubviews() {
  123. if !didInitialAdjustCropOverlay || !cropOverlay.isResizable {
  124. adjustCropOverlay() //keep it centered and constrainted on orientation changes
  125. didInitialAdjustCropOverlay = true
  126. }
  127. }
  128. public override func viewDidLayoutSubviews() {
  129. super.viewDidLayoutSubviews()
  130. let (minscale, initscale) = calculateMinimumAndInitialScale()
  131. scrollView.contentSize = imageView.frame.size
  132. scrollView.minimumZoomScale = minscale
  133. scrollView.zoomScale = initscale
  134. self.centerScrollViewContents()
  135. if !cropOverlay.isResizable || !didInitialCenterCropOverlay {
  136. self.centerCropFrame()
  137. didInitialCenterCropOverlay = true
  138. }
  139. }
  140. private func adjustCropOverlay() {
  141. switch UIDevice.current.orientation {
  142. case .landscapeLeft, .landscapeRight:
  143. cropOverlay.frame.size.height = view.frame.height - CROP_PADDING //height is constrained in landscale
  144. cropOverlay.frame.size.width = cropOverlay.frame.size.height / croppingParameters.aspectRatioHeightToWidth
  145. default:
  146. cropOverlay.frame.size.width = view.frame.width - CROP_PADDING //width is constrained in portrait
  147. cropOverlay.frame.size.height = cropOverlay.frame.size.width * croppingParameters.aspectRatioHeightToWidth
  148. }
  149. }
  150. private func configureWithImage(_ image: UIImage) {
  151. cropOverlay.isHidden = !croppingParameters.isEnabled
  152. buttonActions()
  153. imageView.image = image
  154. imageView.sizeToFit()
  155. view.setNeedsLayout()
  156. }
  157. //Returns a tuple containing the minimum scale and the desired initial scale of the scroll view
  158. private func calculateMinimumAndInitialScale() -> (CGFloat, CGFloat) {
  159. guard let image = imageView.image else {
  160. return (1,1)
  161. }
  162. //The initial scale will fit the entire image on the screen in either orientation
  163. let size = view.bounds
  164. let scaleWidth = size.width / image.size.width
  165. let scaleHeight = size.height / image.size.height
  166. let minSizeWithoutCrop = min(scaleWidth, scaleHeight)
  167. //If cropping enabled, the minimum scale fits the image into the crop rectangle, otherwise
  168. //its the same as in the initial scale
  169. if croppingParameters.isEnabled {
  170. let cropSize = cropOverlay.frame.size
  171. let cropScaleWidth = (cropSize.width - CROP_PADDING) / image.size.width
  172. let cropScaleHeight = (cropSize.height - CROP_PADDING) / image.size.height
  173. let minSizeWithCrop = min(cropScaleWidth, cropScaleHeight)
  174. return (minSizeWithCrop, minSizeWithoutCrop)
  175. }
  176. else {
  177. return (minSizeWithoutCrop, minSizeWithoutCrop)
  178. }
  179. }
  180. private func calculateScrollViewInsets(_ frame: CGRect) -> UIEdgeInsets {
  181. let bottom = view.frame.height - (frame.origin.y + frame.height)
  182. let right = view.frame.width - (frame.origin.x + frame.width)
  183. let insets = UIEdgeInsets(top: frame.origin.y, left: frame.origin.x, bottom: bottom, right: right)
  184. return insets
  185. }
  186. private func centerImageViewOnRotate() {
  187. if croppingParameters.isEnabled {
  188. let size = cropOverlay.frame.size
  189. let scrollInsets = scrollView.contentInset
  190. let imageSize = imageView.frame.size
  191. var contentOffset = CGPoint(x: -scrollInsets.left, y: -scrollInsets.top)
  192. contentOffset.x -= (size.width - imageSize.width) / 2
  193. contentOffset.y -= (size.height - imageSize.height) / 2
  194. scrollView.contentOffset = contentOffset
  195. }
  196. }
  197. private func centerCropFrame() {
  198. let size = scrollView.frame.size
  199. let cropSize = cropOverlay.frame.size
  200. var origin = CGPoint.zero
  201. if cropSize.width < size.width {
  202. origin.x = (size.width - cropSize.width) / 2
  203. }
  204. if cropSize.height < size.height {
  205. origin.y = (size.height - cropSize.height) / 2
  206. }
  207. cropOverlay.frame.origin = origin
  208. }
  209. private func centerScrollViewContents() {
  210. let size = scrollView.frame.size
  211. let imageSize = imageView.frame.size
  212. var imageOrigin = CGPoint.zero
  213. if imageSize.width < size.width {
  214. imageOrigin.x = (size.width - imageSize.width) / 2
  215. }
  216. if imageSize.height < size.height {
  217. imageOrigin.y = (size.height - imageSize.height) / 2
  218. }
  219. imageView.frame.origin = imageOrigin
  220. }
  221. private func buttonActions() {
  222. confirmButton.action = { [weak self] in self?.confirmPhoto() }
  223. cancelButton.action = { [weak self] in self?.cancel() }
  224. rotateButton.action = { [weak self] in self?.rotateRight() }
  225. }
  226. internal func rotateRight() {
  227. if let rotatedImage = imageView.image?.rotate() {
  228. configureWithImage(rotatedImage)
  229. centerScrollViewContents()
  230. }
  231. }
  232. internal func cancel() {
  233. cropOverlay.removeFromSuperview() //remove overlay while processing
  234. onComplete?(nil, nil)
  235. }
  236. internal func confirmPhoto() {
  237. guard let image = imageView.image else {
  238. return
  239. }
  240. disable()
  241. imageView.isHidden = true
  242. let spinner = showSpinner()
  243. if croppingParameters.isEnabled {
  244. let cropRect = makeProportionalCropRect()
  245. let resizedCropRect = CGRect(x: (image.size.width) * cropRect.origin.x,
  246. y: (image.size.height) * cropRect.origin.y,
  247. width: (image.size.width * cropRect.width),
  248. height: (image.size.height * cropRect.height))
  249. DispatchQueue.global(qos: .userInitiated).async {
  250. let croppedImage = image.crop(rect: resizedCropRect) //This can take long time and block UI
  251. DispatchQueue.main.async {
  252. self.onComplete?(croppedImage, self.asset)
  253. self.hideSpinner(spinner)
  254. }
  255. }
  256. }
  257. else {
  258. onComplete?(image, self.asset)
  259. hideSpinner(spinner)
  260. }
  261. cropOverlay.removeFromSuperview() //remove overlay while processing
  262. }
  263. public func viewForZooming(in scrollView: UIScrollView) -> UIView? {
  264. return imageView
  265. }
  266. public func scrollViewDidZoom(_ scrollView: UIScrollView) {
  267. centerScrollViewContents()
  268. }
  269. override public func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
  270. view.setNeedsLayout()
  271. }
  272. func showSpinner() -> UIActivityIndicatorView {
  273. let spinner = UIActivityIndicatorView()
  274. spinner.style = .white
  275. spinner.center = view.center
  276. spinner.startAnimating()
  277. view.addSubview(spinner)
  278. view.bringSubviewToFront(spinner)
  279. return spinner
  280. }
  281. func hideSpinner(_ spinner: UIActivityIndicatorView) {
  282. spinner.stopAnimating()
  283. spinner.removeFromSuperview()
  284. }
  285. func disable() {
  286. confirmButton.isEnabled = false
  287. cancelButton.isEnabled = false
  288. }
  289. func enable() {
  290. confirmButton.isEnabled = true
  291. cancelButton.isEnabled = true
  292. }
  293. func showNoImageScreen(_ error: NSError) {
  294. let permissionsView = PermissionsView(frame: view.bounds)
  295. let desc = localizedString("error.cant-fetch-photo.description")
  296. permissionsView.configureInView(view, title: error.localizedDescription, description: desc, completion: { [weak self] in self?.cancel() })
  297. }
  298. private func makeProportionalCropRect() -> CGRect {
  299. var cropRect = CGRect(x: cropOverlay.frame.origin.x + cropOverlay.outterGap,
  300. y: cropOverlay.frame.origin.y + cropOverlay.outterGap,
  301. width: cropOverlay.frame.size.width - 2 * cropOverlay.outterGap,
  302. height: cropOverlay.frame.size.height - 2 * cropOverlay.outterGap)
  303. cropRect.origin.x += scrollView.contentOffset.x - imageView.frame.origin.x
  304. cropRect.origin.y += scrollView.contentOffset.y - imageView.frame.origin.y
  305. let normalizedX = cropRect.origin.x / imageView.frame.width
  306. let normalizedY = cropRect.origin.y / imageView.frame.height
  307. let extraWidth = CGFloat(0) //fabs(cropRect.origin.x)
  308. let extraHeight = CGFloat(0) //fabs(cropRect.origin.y)
  309. let normalizedWidth = (cropRect.width + extraWidth) / imageView.frame.width
  310. let normalizedHeight = (cropRect.height + extraHeight) / imageView.frame.height
  311. return CGRect(x: normalizedX, y: normalizedY, width: normalizedWidth, height: normalizedHeight)
  312. }
  313. }
  314. extension UIImage {
  315. func crop(rect: CGRect) -> UIImage {
  316. var rectTransform: CGAffineTransform
  317. switch imageOrientation {
  318. case .left:
  319. rectTransform = CGAffineTransform(rotationAngle: radians(90)).translatedBy(x: 0, y: -size.height)
  320. case .right:
  321. rectTransform = CGAffineTransform(rotationAngle: radians(-90)).translatedBy(x: -size.width, y: 0)
  322. case .down:
  323. rectTransform = CGAffineTransform(rotationAngle: radians(-180)).translatedBy(x: -size.width, y: -size.height)
  324. default:
  325. rectTransform = CGAffineTransform.identity
  326. }
  327. rectTransform = rectTransform.scaledBy(x: scale, y: scale)
  328. let cropAspect = rect.height / rect.width
  329. if let cropped = cgImage?.cropping(to: rect.applying(rectTransform)) {
  330. let cropImage = UIImage(cgImage: cropped, scale: scale, orientation: imageOrientation).fixOrientation()
  331. //Rescale the cropped portion to maintain the crop aspect ratio
  332. let currentAspect = cropImage.size.height / cropImage.size.width
  333. return cropImage.scaledBy(size: CGSize(width: cropImage.size.width, height: cropImage.size.height * cropAspect / currentAspect)) ?? self
  334. }
  335. return self
  336. }
  337. func fixOrientation() -> UIImage {
  338. if imageOrientation == .up {
  339. return self
  340. }
  341. UIGraphicsBeginImageContextWithOptions(size, false, scale)
  342. draw(in: CGRect(origin: .zero, size: size))
  343. let normalizedImage: UIImage = UIGraphicsGetImageFromCurrentImageContext() ?? self
  344. UIGraphicsEndImageContext()
  345. return normalizedImage
  346. }
  347. func scaledBy(size: CGSize) -> UIImage? {
  348. let hasAlpha = false
  349. let scale: CGFloat = 0.0 // Automatically use scale factor of main screen
  350. UIGraphicsBeginImageContextWithOptions(size, !hasAlpha, scale)
  351. self.draw(in: CGRect(origin: CGPoint(x: 0, y: 0), size: size))
  352. let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
  353. UIGraphicsEndImageContext()
  354. return scaledImage
  355. }
  356. //Rotate by 90 degrees
  357. func rotate() -> UIImage? {
  358. let radians = Float.pi/2
  359. var newSize = CGRect(origin: CGPoint.zero, size: CGSize(width: self.size.width, height: self.size.height)).applying(CGAffineTransform(rotationAngle: CGFloat(radians))).size
  360. // Trim off the extremely small float value to prevent core graphics from rounding it up
  361. newSize.width = floor(newSize.width)
  362. newSize.height = floor(newSize.height)
  363. UIGraphicsBeginImageContextWithOptions(newSize, false, self.scale)
  364. let context = UIGraphicsGetCurrentContext()!
  365. // Move origin to middle
  366. context.translateBy(x: newSize.width/2, y: newSize.height/2)
  367. // Rotate around middle
  368. context.rotate(by: CGFloat(radians))
  369. // Draw the image at its center
  370. self.draw(in: CGRect(x: -self.size.width/2, y: -self.size.height/2, width: self.size.width, height: self.size.height))
  371. let newImage = UIGraphicsGetImageFromCurrentImageContext()
  372. UIGraphicsEndImageContext()
  373. return newImage
  374. }
  375. }