123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439 |
- import UIKit
- import AVFoundation
- /// Delegate to handle camera setup and video capturing.
- protocol CameraViewControllerDelegate: class {
- func cameraViewControllerDidSetupCaptureSession(_ controller: CameraViewController)
- func cameraViewControllerDidFailToSetupCaptureSession(_ controller: CameraViewController)
- func cameraViewController(_ controller: CameraViewController, didReceiveError error: Error)
- func cameraViewControllerDidTapSettingsButton(_ controller: CameraViewController)
- func cameraViewController(
- _ controller: CameraViewController,
- didOutput metadataObjects: [AVMetadataObject]
- )
- }
- /// View controller responsible for camera controls and video capturing.
- public final class CameraViewController: UIViewController {
- weak var delegate: CameraViewControllerDelegate?
- /// Focus view type.
- public var barCodeFocusViewType: FocusViewType = .animated
- public var showsCameraButton: Bool = false {
- didSet {
- cameraButton.isHidden = showsCameraButton
- }
- }
- /// `AVCaptureMetadataOutput` metadata object types.
- var metadata = [AVMetadataObject.ObjectType]()
- // MARK: - UI proterties
- /// Animated focus view.
- public private(set) lazy var focusView: UIView = self.makeFocusView()
- /// Button to change torch mode.
- public private(set) lazy var flashButton: UIButton = .init(type: .custom)
- /// Button that opens settings to allow camera usage.
- public private(set) lazy var settingsButton: UIButton = self.makeSettingsButton()
- // Button to switch between front and back camera.
- public private(set) lazy var cameraButton: UIButton = self.makeCameraButton()
- // Constraints for the focus view when it gets smaller in size.
- private var regularFocusViewConstraints = [NSLayoutConstraint]()
- // Constraints for the focus view when it gets bigger in size.
- private var animatedFocusViewConstraints = [NSLayoutConstraint]()
- // MARK: - Video
- /// Video preview layer.
- private var videoPreviewLayer: AVCaptureVideoPreviewLayer?
- /// Video capture device. This may be nil when running in Simulator.
- private var captureDevice: AVCaptureDevice?
- /// Capture session.
- private lazy var captureSession: AVCaptureSession = AVCaptureSession()
- // Service used to check authorization status of the capture device
- private let permissionService = VideoPermissionService()
- /// The current torch mode on the capture device.
- private var torchMode: TorchMode = .off {
- didSet {
- guard let captureDevice = captureDevice, captureDevice.hasFlash else { return }
- guard captureDevice.isTorchModeSupported(torchMode.captureTorchMode) else { return }
- do {
- try captureDevice.lockForConfiguration()
- captureDevice.torchMode = torchMode.captureTorchMode
- captureDevice.unlockForConfiguration()
- } catch {}
- flashButton.setImage(torchMode.image, for: UIControlState())
- }
- }
- private var frontCameraDevice: AVCaptureDevice? {
- return AVCaptureDevice.devices(for: .video).first(where: { $0.position == .front })
- }
- private var backCameraDevice: AVCaptureDevice? {
- return AVCaptureDevice.default(for: .video)
- }
- // MARK: - Initialization
- deinit {
- stopCapturing()
- NotificationCenter.default.removeObserver(self)
- }
- // MARK: - View lifecycle
- public override func viewDidLoad() {
- super.viewDidLoad()
- view.backgroundColor = .black
- videoPreviewLayer = AVCaptureVideoPreviewLayer(session: self.captureSession)
- videoPreviewLayer?.videoGravity = .resizeAspectFill
- guard let videoPreviewLayer = videoPreviewLayer else {
- return
- }
- view.layer.addSublayer(videoPreviewLayer)
- view.addSubviews(settingsButton, flashButton, focusView, cameraButton)
- torchMode = .off
- focusView.isHidden = true
- setupCamera()
- setupConstraints()
- setupActions()
- }
- public override func viewDidAppear(_ animated: Bool) {
- super.viewDidAppear(animated)
- setupVideoPreviewLayerOrientation()
- animateFocusView()
- }
- public override func viewWillTransition(to size: CGSize,
- with coordinator: UIViewControllerTransitionCoordinator) {
- super.viewWillTransition(to: size, with: coordinator)
- coordinator.animate(
- alongsideTransition: { [weak self] _ in
- self?.setupVideoPreviewLayerOrientation()
- },
- completion: ({ [weak self] _ in
- self?.animateFocusView()
- }))
- }
- // MARK: - Video capturing
- func startCapturing() {
- guard !isSimulatorRunning else {
- return
- }
- torchMode = .off
- captureSession.startRunning()
- focusView.isHidden = false
- flashButton.isHidden = captureDevice?.position == .front
- cameraButton.isHidden = !showsCameraButton
- }
- func stopCapturing() {
- guard !isSimulatorRunning else {
- return
- }
- torchMode = .off
- captureSession.stopRunning()
- focusView.isHidden = true
- flashButton.isHidden = true
- cameraButton.isHidden = true
- }
- // MARK: - Actions
- private func setupActions() {
- flashButton.addTarget(
- self,
- action: #selector(handleFlashButtonTap),
- for: .touchUpInside
- )
- settingsButton.addTarget(
- self,
- action: #selector(handleSettingsButtonTap),
- for: .touchUpInside
- )
- cameraButton.addTarget(
- self,
- action: #selector(handleCameraButtonTap),
- for: .touchUpInside
- )
- NotificationCenter.default.addObserver(
- self,
- selector: #selector(appWillEnterForeground),
- name: NSNotification.Name.UIApplicationWillEnterForeground,
- object: nil
- )
- }
- /// `UIApplicationWillEnterForegroundNotification` action.
- @objc private func appWillEnterForeground() {
- torchMode = .off
- animateFocusView()
- }
- /// Opens setting to allow camera usage.
- @objc private func handleSettingsButtonTap() {
- delegate?.cameraViewControllerDidTapSettingsButton(self)
- }
- /// Swaps camera position.
- @objc private func handleCameraButtonTap() {
- swapCamera()
- }
- /// Sets the next torch mode.
- @objc private func handleFlashButtonTap() {
- torchMode = torchMode.next
- }
- // MARK: - Camera setup
- /// Sets up camera and checks for camera permissions.
- private func setupCamera() {
- permissionService.checkPersmission { [weak self] error in
- guard let strongSelf = self else {
- return
- }
- DispatchQueue.main.async { [weak self] in
- self?.settingsButton.isHidden = error == nil
- }
- if error == nil {
- strongSelf.setupSessionInput(for: .back)
- strongSelf.setupSessionOutput()
- strongSelf.delegate?.cameraViewControllerDidSetupCaptureSession(strongSelf)
- } else {
- strongSelf.delegate?.cameraViewControllerDidFailToSetupCaptureSession(strongSelf)
- }
- }
- }
- /// Sets up capture input, output and session.
- private func setupSessionInput(for position: AVCaptureDevice.Position) {
- guard !isSimulatorRunning else {
- return
- }
- guard let device = position == .front ? frontCameraDevice : backCameraDevice else {
- return
- }
- do {
- let newInput = try AVCaptureDeviceInput(device: device)
- captureDevice = device
- // Swap capture device inputs
- captureSession.beginConfiguration()
- if let currentInput = captureSession.inputs.first as? AVCaptureDeviceInput {
- captureSession.removeInput(currentInput)
- }
- captureSession.addInput(newInput)
- captureSession.commitConfiguration()
- flashButton.isHidden = position == .front
- } catch {
- delegate?.cameraViewController(self, didReceiveError: error)
- return
- }
- }
- private func setupSessionOutput() {
- guard !isSimulatorRunning else {
- return
- }
- let output = AVCaptureMetadataOutput()
- captureSession.addOutput(output)
- output.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
- output.metadataObjectTypes = metadata
- videoPreviewLayer?.session = captureSession
- view.setNeedsLayout()
- }
- /// Switch front/back camera.
- private func swapCamera() {
- guard let input = captureSession.inputs.first as? AVCaptureDeviceInput else {
- return
- }
- setupSessionInput(for: input.device.position == .back ? .front : .back)
- }
- // MARK: - Animations
- /// Performs focus view animation.
- private func animateFocusView() {
- // Restore to initial state
- focusView.layer.removeAllAnimations()
- animatedFocusViewConstraints.deactivate()
- regularFocusViewConstraints.activate()
- view.layoutIfNeeded()
- guard barCodeFocusViewType == .animated else {
- return
- }
- regularFocusViewConstraints.deactivate()
- animatedFocusViewConstraints.activate()
- UIView.animate(
- withDuration: 1.0,
- delay: 0,
- options: [.repeat, .autoreverse, .beginFromCurrentState],
- animations: ({ [weak self] in
- self?.view.layoutIfNeeded()
- }),
- completion: nil
- )
- }
- }
- // MARK: - Layout
- private extension CameraViewController {
- func setupConstraints() {
- if #available(iOS 11.0, *) {
- NSLayoutConstraint.activate(
- flashButton.topAnchor.constraint(
- equalTo: view.safeAreaLayoutGuide.topAnchor,
- constant: 30
- ),
- flashButton.trailingAnchor.constraint(
- equalTo: view.safeAreaLayoutGuide.trailingAnchor,
- constant: -13
- ),
- cameraButton.bottomAnchor.constraint(
- equalTo: view.safeAreaLayoutGuide.bottomAnchor,
- constant: -30
- )
- )
- } else {
- NSLayoutConstraint.activate(
- flashButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 30),
- flashButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -13),
- cameraButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -30)
- )
- }
- let imageButtonSize: CGFloat = 37
- NSLayoutConstraint.activate(
- flashButton.widthAnchor.constraint(equalToConstant: imageButtonSize),
- flashButton.heightAnchor.constraint(equalToConstant: imageButtonSize),
- settingsButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
- settingsButton.centerYAnchor.constraint(equalTo: view.centerYAnchor),
- settingsButton.widthAnchor.constraint(equalToConstant: 150),
- settingsButton.heightAnchor.constraint(equalToConstant: 50),
- cameraButton.widthAnchor.constraint(equalToConstant: 48),
- cameraButton.heightAnchor.constraint(equalToConstant: 48),
- cameraButton.trailingAnchor.constraint(equalTo: flashButton.trailingAnchor)
- )
- setupFocusViewConstraints()
- }
- func setupFocusViewConstraints() {
- NSLayoutConstraint.activate(
- focusView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
- focusView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
- )
- let focusViewSize = barCodeFocusViewType == .oneDimension
- ? CGSize(width: 280, height: 80)
- : CGSize(width: 218, height: 150)
- regularFocusViewConstraints = [
- focusView.widthAnchor.constraint(equalToConstant: focusViewSize.width),
- focusView.heightAnchor.constraint(equalToConstant: focusViewSize.height)
- ]
- animatedFocusViewConstraints = [
- focusView.widthAnchor.constraint(equalToConstant: 280),
- focusView.heightAnchor.constraint(equalToConstant: 80)
- ]
- NSLayoutConstraint.activate(regularFocusViewConstraints)
- }
- func setupVideoPreviewLayerOrientation() {
- guard let videoPreviewLayer = videoPreviewLayer else {
- return
- }
- videoPreviewLayer.frame = view.layer.bounds
- if let connection = videoPreviewLayer.connection, connection.isVideoOrientationSupported {
- switch UIApplication.shared.statusBarOrientation {
- case .portrait:
- connection.videoOrientation = .portrait
- case .landscapeRight:
- connection.videoOrientation = .landscapeRight
- case .landscapeLeft:
- connection.videoOrientation = .landscapeLeft
- case .portraitUpsideDown:
- connection.videoOrientation = .portraitUpsideDown
- default:
- connection.videoOrientation = .portrait
- }
- }
- }
- }
- // MARK: - Subviews factory
- private extension CameraViewController {
- func makeFocusView() -> UIView {
- let view = UIView()
- view.layer.borderColor = UIColor.white.cgColor
- view.layer.borderWidth = 2
- view.layer.cornerRadius = 5
- view.layer.shadowColor = UIColor.white.cgColor
- view.layer.shadowRadius = 10.0
- view.layer.shadowOpacity = 0.9
- view.layer.shadowOffset = CGSize.zero
- view.layer.masksToBounds = false
- return view
- }
- func makeSettingsButton() -> UIButton {
- let button = UIButton(type: .system)
- let title = NSAttributedString(
- string: localizedString("BUTTON_SETTINGS"),
- attributes: [.font: UIFont.boldSystemFont(ofSize: 17), .foregroundColor: UIColor.white]
- )
- button.setAttributedTitle(title, for: UIControlState())
- button.sizeToFit()
- return button
- }
- func makeCameraButton() -> UIButton {
- let button = UIButton(type: .custom)
- button.setImage(imageNamed("cameraRotate"), for: UIControlState())
- button.isHidden = !showsCameraButton
- return button
- }
- }
- // MARK: - AVCaptureMetadataOutputObjectsDelegate
- extension CameraViewController: AVCaptureMetadataOutputObjectsDelegate {
- public func metadataOutput(_ output: AVCaptureMetadataOutput,
- didOutput metadataObjects: [AVMetadataObject],
- from connection: AVCaptureConnection) {
- delegate?.cameraViewController(self, didOutput: metadataObjects)
- }
- }
|