123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630 |
- //
- // CameraViewController.swift
- // CameraViewController
- //
- // Created by Alex Littlejohn.
- // Copyright (c) 2016 zero. All rights reserved.
- //
- // Modified by Kevin Kieffer on 2019/08/06. Changes as follows:
- // Update the overlay constraints when rotating, so the overlay is properly positioned and sized
- //
- // Updated the button constraint calls and removed the rotate animation method which was not working
- import UIKit
- import AVFoundation
- import Photos
- public typealias CameraViewCompletion = (UIImage?, PHAsset?) -> Void
- public extension CameraViewController {
- /// Provides an image picker wrapped inside a UINavigationController instance
- class func imagePickerViewController(croppingParameters: CroppingParameters, completion: @escaping CameraViewCompletion) -> UINavigationController {
- let imagePicker = PhotoLibraryViewController()
- let navigationController = UINavigationController(rootViewController: imagePicker)
-
- navigationController.navigationBar.barTintColor = UIColor.black
- navigationController.navigationBar.barStyle = UIBarStyle.black
- navigationController.modalTransitionStyle = UIModalTransitionStyle.crossDissolve
- imagePicker.onSelectionComplete = { [weak imagePicker] asset in
- if let asset = asset {
- let confirmController = ConfirmViewController(asset: asset, croppingParameters: croppingParameters)
- confirmController.onComplete = { [weak imagePicker] image, asset in
- if let image = image, let asset = asset {
- completion(image, asset)
- } else {
- imagePicker?.dismiss(animated: true, completion: nil)
- }
- }
- confirmController.modalTransitionStyle = UIModalTransitionStyle.crossDissolve
- confirmController.modalPresentationStyle = .fullScreen
- imagePicker?.present(confirmController, animated: true, completion: nil)
- } else {
- completion(nil, nil)
- }
- }
-
- return navigationController
- }
- }
- open class CameraViewController: UIViewController {
-
- var didUpdateViews = false
- var croppingParameters: CroppingParameters
- var animationRunning = false
- let allowVolumeButtonCapture: Bool
-
- var lastInterfaceOrientation : UIInterfaceOrientation?
- open var onCompletion: CameraViewCompletion?
- var volumeControl: VolumeControl?
-
- var animationDuration: TimeInterval = 0.5
- var animationSpring: CGFloat = 0.5
- var rotateAnimation: UIView.AnimationOptions = .curveLinear
-
- var cameraButtonEdgeConstraint: NSLayoutConstraint?
- var cameraButtonGravityConstraint: NSLayoutConstraint?
-
- var closeButtonEdgeConstraint: NSLayoutConstraint?
- var closeButtonGravityConstraint: NSLayoutConstraint?
-
- var containerButtonsEdgeOneConstraint: NSLayoutConstraint?
- var containerButtonsEdgeTwoConstraint: NSLayoutConstraint?
- var containerButtonsGravityConstraint: NSLayoutConstraint?
-
- var swapButtonEdgeOneConstraint: NSLayoutConstraint?
- var swapButtonEdgeTwoConstraint: NSLayoutConstraint?
- var swapButtonGravityConstraint: NSLayoutConstraint?
-
- var libraryButtonEdgeOneConstraint: NSLayoutConstraint?
- var libraryButtonEdgeTwoConstraint: NSLayoutConstraint?
- var libraryButtonGravityConstraint: NSLayoutConstraint?
-
- var flashButtonEdgeConstraint: NSLayoutConstraint?
- var flashButtonGravityConstraint: NSLayoutConstraint?
-
- var cameraOverlayEdgeOneConstraint: NSLayoutConstraint?
- var cameraOverlayEdgeTwoConstraint: NSLayoutConstraint?
- var cameraOverlayWidthConstraint: NSLayoutConstraint?
- var cameraOverlayCenterConstraint: NSLayoutConstraint?
-
- let cameraView : CameraView = {
- let cameraView = CameraView()
- cameraView.translatesAutoresizingMaskIntoConstraints = false
- return cameraView
- }()
- let cameraOverlay : CropOverlay = {
- let cameraOverlay = CropOverlay()
- cameraOverlay.translatesAutoresizingMaskIntoConstraints = false
- cameraOverlay.showsButtons = false
- return cameraOverlay
- }()
-
- let cameraButton : UIButton = {
- let button = UIButton(frame: CGRect(x: 0, y: 0, width: 64, height: 64))
- button.translatesAutoresizingMaskIntoConstraints = false
- button.isEnabled = false
- button.setImage(UIImage(named: "cameraButton",
- in: CameraGlobals.shared.bundle,
- compatibleWith: nil),
- for: .normal)
- button.setImage(UIImage(named: "cameraButtonHighlighted",
- in: CameraGlobals.shared.bundle,
- compatibleWith: nil),
- for: .highlighted)
- return button
- }()
-
- let closeButton : UIButton = {
- let button = UIButton()
- button.translatesAutoresizingMaskIntoConstraints = false
- button.setImage(UIImage(named: "closeButton",
- in: CameraGlobals.shared.bundle,
- compatibleWith: nil),
- for: .normal)
- return button
- }()
-
- let swapButton : UIButton = {
- let button = UIButton()
- button.translatesAutoresizingMaskIntoConstraints = false
- button.setImage(UIImage(named: "swapButton",
- in: CameraGlobals.shared.bundle,
- compatibleWith: nil),
- for: .normal)
- return button
- }()
-
- let libraryButton : UIButton = {
- let button = UIButton()
- button.translatesAutoresizingMaskIntoConstraints = false
- button.setImage(UIImage(named: "libraryButton",
- in: CameraGlobals.shared.bundle,
- compatibleWith: nil),
- for: .normal)
- return button
- }()
-
- let flashButton : UIButton = {
- let button = UIButton(frame: CGRect(x: 0, y: 0, width: 44, height: 44))
- button.translatesAutoresizingMaskIntoConstraints = false
- button.setImage(UIImage(named: "flashAutoIcon",
- in: CameraGlobals.shared.bundle,
- compatibleWith: nil),
- for: .normal)
- return button
- }()
-
- let containerSwapLibraryButton : UIView = {
- let view = UIView()
- view.translatesAutoresizingMaskIntoConstraints = false
- return view
- }()
-
- private let allowsLibraryAccess: Bool
-
- public init(croppingParameters: CroppingParameters = CroppingParameters(),
- allowsLibraryAccess: Bool = true,
- allowsSwapCameraOrientation: Bool = true,
- allowVolumeButtonCapture: Bool = true,
- completion: @escaping CameraViewCompletion) {
- self.croppingParameters = croppingParameters
- self.allowsLibraryAccess = allowsLibraryAccess
- self.allowVolumeButtonCapture = allowVolumeButtonCapture
- super.init(nibName: nil, bundle: nil)
- onCompletion = completion
- cameraOverlay.isHidden = !croppingParameters.isEnabled || !croppingParameters.cameraOverlay
- cameraOverlay.isUserInteractionEnabled = false
- libraryButton.isEnabled = allowsLibraryAccess
- libraryButton.isHidden = !allowsLibraryAccess
- swapButton.isEnabled = allowsSwapCameraOrientation
- swapButton.isHidden = !allowsSwapCameraOrientation
- }
-
- required public init?(coder aDecoder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
- open override var prefersStatusBarHidden: Bool {
- return true
- }
-
- open override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
- return UIStatusBarAnimation.slide
- }
-
- /**
- * Configure the background of the superview to black
- * and add the views on this superview. Then, request
- * the update of constraints for this superview.
- */
- open override func loadView() {
- super.loadView()
- view.backgroundColor = UIColor.black
- [cameraView,
- cameraOverlay,
- cameraButton,
- closeButton,
- flashButton,
- containerSwapLibraryButton].forEach({ view.addSubview($0) })
- [swapButton, libraryButton].forEach({ containerSwapLibraryButton.addSubview($0) })
- view.setNeedsUpdateConstraints()
- }
-
- private func updateOverlayConstraints() {
- let portrait = UIApplication.shared.statusBarOrientation.isPortrait
- let padding : CGFloat = portrait ? 16.0 : -16.0
- removeCameraOverlayEdgesConstraints()
- configCameraOverlayEdgeOneContraint(portrait, padding: padding)
- configCameraOverlayEdgeTwoConstraint(portrait, padding: padding)
- configCameraOverlayWidthConstraint(portrait)
- configCameraOverlayCenterConstraint(portrait)
- }
-
- /**
- * Setup the constraints when the app is starting or rotating
- * the screen.
- * To avoid the override/conflict of stable constraint, these
- * stable constraint are one time configurable.
- * Any other dynamic constraint are configurable when the
- * device is rotating, based on the device orientation.
- */
- override open func updateViewConstraints() {
- if !didUpdateViews {
- configCameraViewConstraints()
- didUpdateViews = true
- }
-
- let statusBarOrientation = UIApplication.shared.statusBarOrientation
- let portrait = statusBarOrientation.isPortrait
- removeCameraButtonConstraints()
- configCameraButtonEdgeConstraint(statusBarOrientation)
- configCameraButtonGravityConstraint(portrait)
-
- removeCloseButtonConstraints()
- configCloseButtonEdgeConstraint(statusBarOrientation)
- configCloseButtonGravityConstraint(statusBarOrientation)
-
- removeContainerConstraints()
- configContainerEdgeConstraint(statusBarOrientation)
- configContainerGravityConstraint(statusBarOrientation)
-
- removeSwapButtonConstraints()
- configSwapButtonEdgeConstraint(statusBarOrientation)
- configSwapButtonGravityConstraint(statusBarOrientation)
- removeLibraryButtonConstraints()
- configLibraryEdgeButtonConstraint(statusBarOrientation)
- configLibraryGravityButtonConstraint(statusBarOrientation)
-
- configFlashEdgeButtonConstraint(statusBarOrientation)
- configFlashGravityButtonConstraint(statusBarOrientation)
-
- updateOverlayConstraints()
-
- super.updateViewConstraints()
- }
-
- /**
- * Add observer to check when the camera has started,
- * enable the volume buttons to take the picture,
- * configure the actions of the buttons on the screen,
- * check the permissions of access of the camera and
- * the photo library.
- * Configure the camera focus when the application
- * start, to avoid any bluried image.
- */
- open override func viewDidLoad() {
- super.viewDidLoad()
- setupActions()
- checkPermissions()
- cameraView.configureFocus()
- cameraView.configureZoom()
-
- if let device = AVCaptureDevice.default(for: .video) {
- if !device.hasFlash {
- flashButton.isEnabled = false
- flashButton.isHidden = true
- }
- }
- }
- /**
- * Start the session of the camera.
- */
- open override func viewWillAppear(_ animated: Bool) {
- super.viewWillAppear(animated)
- cameraView.startSession()
- addCameraObserver()
- addRotateObserver()
- if allowVolumeButtonCapture {
- setupVolumeControl()
- }
- }
-
- /**
- * Enable the button to take the picture when the
- * camera is ready.
- */
- open override func viewDidAppear(_ animated: Bool) {
- super.viewDidAppear(animated)
- if cameraView.session?.isRunning == true {
- notifyCameraReady()
- }
- }
- open override func viewWillDisappear(_ animated: Bool) {
- super.viewWillDisappear(animated)
- NotificationCenter.default.removeObserver(self)
- volumeControl = nil
- }
- /**
- * This method will disable the rotation of the
- */
- override open func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
- super.viewWillTransition(to: size, with: coordinator)
- lastInterfaceOrientation = UIApplication.shared.statusBarOrientation
- if animationRunning {
- return
- }
- CATransaction.begin()
- CATransaction.setDisableActions(true)
- coordinator.animate(alongsideTransition: { [weak self] animation in
- self?.view.setNeedsUpdateConstraints()
- }, completion: { _ in
- CATransaction.commit()
- })
- }
-
- /**
- * Observer the camera status, when it is ready,
- * it calls the method cameraReady to enable the
- * button to take the picture.
- */
- private func addCameraObserver() {
- NotificationCenter.default.addObserver(
- self,
- selector: #selector(notifyCameraReady),
- name: NSNotification.Name.AVCaptureSessionDidStartRunning,
- object: nil)
- }
-
- /**
- * Observer the device orientation to update the
- * orientation of CameraView.
- */
- private func addRotateObserver() {
- NotificationCenter.default.addObserver(
- self,
- selector: #selector(rotateCameraView),
- name: UIDevice.orientationDidChangeNotification,
- object: nil)
- }
-
- @objc internal func notifyCameraReady() {
- cameraButton.isEnabled = true
- }
-
- /**
- * Attach the take of picture for any volume button.
- */
- private func setupVolumeControl() {
- volumeControl = VolumeControl(view: view) { [weak self] _ in
- guard let enabled = self?.cameraButton.isEnabled, enabled else {
- return
- }
- self?.capturePhoto()
- }
- }
-
- /**
- * Configure the action for every button on this
- * layout.
- */
- private func setupActions() {
- cameraButton.action = { [weak self] in self?.capturePhoto() }
- swapButton.action = { [weak self] in self?.swapCamera() }
- libraryButton.action = { [weak self] in self?.showLibrary() }
- closeButton.action = { [weak self] in self?.close() }
- flashButton.action = { [weak self] in self?.toggleFlash() }
- }
-
- /**
- * Toggle the buttons status, based on the actual
- * state of the camera.
- */
- private func toggleButtons(enabled: Bool) {
- [cameraButton,
- closeButton,
- swapButton,
- libraryButton].forEach({ $0.isEnabled = enabled })
- }
-
- @objc func rotateCameraView() {
- cameraView.rotatePreview()
- updateViewConstraints()
- }
-
-
-
- func setTransform(transform: CGAffineTransform) {
- closeButton.transform = transform
- swapButton.transform = transform
- libraryButton.transform = transform
- flashButton.transform = transform
- }
-
- /**
- * Validate the permissions of the camera and
- * library, if the user do not accept these
- * permissions, it shows an view that notifies
- * the user that it not allow the permissions.
- */
- private func checkPermissions() {
- if AVCaptureDevice.authorizationStatus(for: AVMediaType.video) != .authorized {
- AVCaptureDevice.requestAccess(for: AVMediaType.video) { granted in
- DispatchQueue.main.async() { [weak self] in
- if !granted {
- self?.showNoPermissionsView()
- }
- }
- }
- }
- }
-
- /**
- * Generate the view of no permission.
- */
- private func showNoPermissionsView(library: Bool = false) {
- let permissionsView = PermissionsView(frame: view.bounds)
- let title: String
- let desc: String
-
- if library {
- title = localizedString("permissions.library.title")
- desc = localizedString("permissions.library.description")
- } else {
- title = localizedString("permissions.title")
- desc = localizedString("permissions.description")
- }
-
- permissionsView.configureInView(view, title: title, description: desc, completion: { [weak self] in self?.close() })
- }
-
- /**
- * This method will be called when the user
- * try to take the picture.
- * It will lock any button while the shot is
- * taken, then, realease the buttons and save
- * the picture on the device.
- */
- internal func capturePhoto() {
- guard let output = cameraView.imageOutput,
- let connection = output.connection(with: AVMediaType.video) else {
- return
- }
-
- if connection.isEnabled {
- toggleButtons(enabled: false)
- cameraView.capturePhoto { [weak self] image in
- guard let image = image else {
- self?.toggleButtons(enabled: true)
- return
- }
- self?.saveImage(image: image)
- }
- }
- }
-
- internal func saveImage(image: UIImage) {
- let spinner = showSpinner()
- cameraView.preview.isHidden = true
- if allowsLibraryAccess {
- _ = SingleImageSaver()
- .setImage(image)
- .onSuccess { [weak self] asset in
- self?.layoutCameraResult(asset: asset)
- self?.hideSpinner(spinner)
- }
- .onFailure { [weak self] error in
- self?.toggleButtons(enabled: true)
- self?.showNoPermissionsView(library: true)
- self?.cameraView.preview.isHidden = false
- self?.hideSpinner(spinner)
- }
- .save()
- } else {
- layoutCameraResult(uiImage: image)
- hideSpinner(spinner)
- }
- }
-
- internal func close() {
- onCompletion?(nil, nil)
- onCompletion = nil
- }
-
- internal func showLibrary() {
- let imagePicker = CameraViewController.imagePickerViewController(croppingParameters: croppingParameters) { [weak self] image, asset in
- defer {
- self?.dismiss(animated: true, completion: nil)
- }
- guard let image = image, let asset = asset else {
- return
- }
- self?.onCompletion?(image, asset)
- }
-
- present(imagePicker, animated: true) { [weak self] in
- self?.cameraView.stopSession()
- }
- }
-
- internal func toggleFlash() {
- cameraView.cycleFlash()
-
- guard let device = cameraView.device else {
- return
- }
-
- let image = UIImage(named: flashImage(device.flashMode),
- in: CameraGlobals.shared.bundle,
- compatibleWith: nil)
-
- flashButton.setImage(image, for: .normal)
- }
-
- internal func swapCamera() {
- cameraView.swapCameraInput()
- flashButton.isHidden = cameraView.currentPosition == AVCaptureDevice.Position.front
- }
-
- internal func layoutCameraResult(uiImage: UIImage) {
- cameraView.stopSession()
- startConfirmController(uiImage: uiImage)
- toggleButtons(enabled: true)
- }
-
- internal func layoutCameraResult(asset: PHAsset) {
- cameraView.stopSession()
- startConfirmController(asset: asset)
- toggleButtons(enabled: true)
- }
-
- private func startConfirmController(uiImage: UIImage) {
- let confirmViewController = ConfirmViewController(image: uiImage, croppingParameters: croppingParameters)
- confirmViewController.onComplete = { [weak self] image, asset in
- defer {
- //In iOS13, the volume changed notification channel is being called when dismissing this controller
- //Since this controller comes back into view momentarily here, before being dismissed, another
- //photo is being taken and the camera shutter sound occurs. As a workaround, the volume control
- //is deinitialized here to remove the notification channel.
- self?.volumeControl = nil
- self?.dismiss(animated: true, completion: nil)
- }
-
- guard let image = image else {
- return
- }
-
- self?.onCompletion?(image, asset)
- self?.onCompletion = nil
- }
- confirmViewController.modalTransitionStyle = UIModalTransitionStyle.crossDissolve
- confirmViewController.modalPresentationStyle = .fullScreen
- present(confirmViewController, animated: true, completion: nil)
- }
-
- private func startConfirmController(asset: PHAsset) {
- let confirmViewController = ConfirmViewController(asset: asset, croppingParameters: croppingParameters)
- confirmViewController.onComplete = { [weak self] image, asset in
- defer {
- self?.volumeControl = nil
- self?.dismiss(animated: true, completion: nil)
- }
- guard let image = image, let asset = asset else {
- return
- }
- self?.onCompletion?(image, asset)
- self?.onCompletion = nil
- }
- confirmViewController.modalTransitionStyle = UIModalTransitionStyle.crossDissolve
- confirmViewController.modalPresentationStyle = .fullScreen
- present(confirmViewController, animated: true, completion: nil)
- }
- private func showSpinner() -> UIActivityIndicatorView {
- let spinner = UIActivityIndicatorView()
- spinner.style = .white
- spinner.center = view.center
- spinner.startAnimating()
-
- view.addSubview(spinner)
- view.bringSubviewToFront(spinner)
-
- return spinner
- }
-
- private func hideSpinner(_ spinner: UIActivityIndicatorView) {
- spinner.stopAnimating()
- spinner.removeFromSuperview()
- }
-
- }
|