Episode #304

Adding a Flash Toggle Camera Control

Series: Camera Capture and Detection

12 minutes
Published on September 29, 2017

This video is only available to subscribers. Get access to this video and 572 others.

In this episode we extract our camera preview layer into its own view so we can add subviews. We’ll use this to add a flash button control to the UI, which will require us to learn about locking the device and controlling the camera’s "torch".

Links

Creating a Flash Control

For the flash button I want some behavior to be encapsulated in a separate class so we don't muddy up our view controller. This button class will be responsible for controlling how large the tap area of the button should be (not allowing the image to drive how big the button is) as well as toggling between the 2 flash icons.

class FlashButton : UIButton {

    let TapSize = 50

    override class var requiresConstraintBasedLayout: Bool { return true }

    init() {
        super.init(frame: CGRect(x: 0, y: 0, width: TapSize, height: TapSize))
        tintColor = .white
        translatesAutoresizingMaskIntoConstraints = false

        adjustsImageWhenHighlighted = false

        setImage(#imageLiteral(resourceName: "Flash"), for: .normal)
        setImage(#imageLiteral(resourceName: "NoFlash"), for: .selected)

        imageView?.contentMode = .center

        addTarget(self, action: #selector(onFlashTapped), for: .touchUpInside)
    }

    override var intrinsicContentSize: CGSize {
        return CGSize(width: TapSize, height: TapSize)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError()
    }

    @objc func onFlashTapped() {
        isSelected = !isSelected
    }
}

Moving Our Camera Preview Layer Into a New Class

By moving this preview layer as a sublayer of a different view's layer, we can once again properly add subviews to our main view controller's view. This will be useful for drawing the flash button on top.

class CameraView : UIView {
    let previewLayer: AVCaptureVideoPreviewLayer!

    init(captureSession: AVCaptureSession) {
        previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
        previewLayer.backgroundColor = UIColor.black.cgColor
        previewLayer.videoGravity = .resizeAspect

        super.init(frame: UIScreen.main.bounds)

        layer.addSublayer(previewLayer)
        setNeedsLayout()
        layoutIfNeeded()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError()
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        previewLayer.frame = layer.bounds
    }
}

Controlling the Torch

Before we can turn the camera's torch on or off we have to acquire a lock. This call can throw (if the device is busy or another application currently has the lock) so we'll create a quick helper function that will take care of this for us:

func withDeviceLock(on device: AVCaptureDevice, block: (AVCaptureDevice) -> Void) {
        do {
            try device.lockForConfiguration()
            block(device)
            device.unlockForConfiguration()
        } catch {
            // can't acquire lock
        }
    }

To turn the torch on:

    func turnOnTorch(device: AVCaptureDevice) {
        guard device.hasTorch else { return }
        withDeviceLock(on: device) {
            try? $0.setTorchModeOn(level: AVCaptureDevice.maxAvailableTorchLevel)
        }
    }

And to turn it off:

    func turnOffTorch(device: AVCaptureDevice) {
        guard device.hasTorch else { return }
        withDeviceLock(on: device) {
            $0.torchMode = .off
        }
    }

This episode uses Xcode 9.0-beta6, Swift 4.