Lawrence Gimenez

Create a custom camera preview using Swift

If you are looking to create your own camera on iOS, I will demonstrate how I did it on my end.


First of all you need to subclass UIView and inherit its properties. We will use this Swift file for our camera preview.

import UIKit
import AVFoundation

class CameraPreviewView: UIView {
    override class var layerClass: AnyClass {
        return AVCaptureVideoPreviewLayer.self
    var videoPreviewLayer: AVCaptureVideoPreviewLayer {
        return layer as! AVCaptureVideoPreviewLayer
    var session: AVCaptureSession? {
        get {
            return videoPreviewLayer.session
        set {
            videoPreviewLayer.session = newValue

AVCaptureVideoPreviewLayer is a subclass of CALayer that you use to display video as it’s captured by an input device.

Storyboard UI

Drag a UIView that should fill up the whole screen. Add the necessary constraints at each border. Then place a UIButton at the lower part and also add the necessary constraints. This UIButton should serve as the record button.

Take note that the UIButton should be on top of the camera preview UIView as not to hide it once the preview is shown.

Click the UIView and add CameraPreviewView as the custom class.


In our ViewController above, create a file called CaptureViewController.swift and use it as the custom class. Then, wire the camera preview UIView and UIButton outlets. Then import AVFoundation and add the necessary classes like the code below.

import UIKit
import AVFoundation
import OSLog

class CaptureViewController: UIViewController {

    @IBOutlet weak var cameraPreviewView: CameraPreviewView!
    @IBOutlet weak var captureButton: UIButton!
    // An object that manages capture activity and coordinates the flow of data from input devices to capture outputs
    private let captureSession = AVCaptureSession()
    // A capture output that records video and audio to a QuickTime movie file.
    private let videoCaptureOutput = AVCaptureMovieFileOutput()
    // A capture input that provides media from a capture device to a capture session.
    private var activeCaptureDeviceInput: AVCaptureDeviceInput!
    private let logger = Logger()
    private let sessionQueue = DispatchQueue(label: "Capture Session")

    override func viewDidAppear(_ animated: Bool) {

    private func initCaptureSession() {
        cameraPreviewView.session = captureSession
        // First of all ask permission from the user
        if AVCaptureDevice.authorizationStatus(for: .video) == .authorized {
            // Start capturing video
        } else if AVCaptureDevice.authorizationStatus(for: .video) == .notDetermined {
            // Request permission
            AVCaptureDevice.requestAccess(for: .video, completionHandler: { [self]
                granted in
                if granted {
                    logger.debug("Video capture device granted? \(granted)")

    private func startVideoSession() {
        captureSession.sessionPreset = .hd4K3840x2160
        // Setup camera
        let videoDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back)
        guard let videoDeviceInput = try? AVCaptureDeviceInput(device: videoDevice!) else {
        if captureSession.canAddInput(videoDeviceInput) {
            activeCaptureDeviceInput = videoDeviceInput
        // Setup microphone
        let audioDevice = AVCaptureDevice.default(for: .audio)
        guard let audioDeviceInput = try? AVCaptureDeviceInput(device: audioDevice!) else {
        if captureSession.canAddInput(audioDeviceInput) {
        // Setup movie output
        if captureSession.canAddOutput(videoCaptureOutput) {
        DispatchQueue.main.async {
            self.cameraPreviewView.videoPreviewLayer.connection?.videoOrientation = .portrait


That's it. Try running it on a connected device and not on the emulator.

[jetpack_subscription_form show_subscribers_total="false" button_on_newline="false" custom_font_size="16px" custom_border_radius="0" custom_border_weight="1" custom_padding="15" custom_spacing="10" submit_button_classes="" email_field_classes="" show_only_email_and_button="true"]