Skip to main content
The video recording feature allows users to capture short-form videos using the device camera with an animated record button and real-time preview.

Architecture

The recording system consists of:
  • MediaViewController: Main controller for the recording interface
  • CameraManager: Manages AVCaptureSession and video recording
  • RecordButton: Custom animated button for recording control

Camera Setup

The CameraManager initializes the capture session with both video and audio inputs:
CameraManager.swift:96-156
fileprivate func setupCamera(completion: @escaping RegularCompletionBlock){
    captureSession = AVCaptureSession()
    
    guard let device = AVCaptureDevice.default(AVCaptureDevice.DeviceType.builtInWideAngleCamera, for: .video, position: .back) else { return }
    captureDevice = device
    
    guard let audioDevice = AVCaptureDevice.default(for: .audio) else { return }
    
    var deviceInput: AVCaptureDeviceInput!
    var audioDeviceInput: AVCaptureDeviceInput!
    do {
        deviceInput = try AVCaptureDeviceInput(device: captureDevice)
        guard deviceInput != nil else {
            print("error: cant get deviceInput")
            return
        }
        audioDeviceInput = try AVCaptureDeviceInput(device: audioDevice)
        guard audioDeviceInput != nil else {
            print("error: cant get audioDeviceInput")
            return
        }
        
        sessionQueue.async {
            if let session = self.captureSession {
                session.beginConfiguration()
                session.sessionPreset = .high
                
                // Add video
                if session.canAddInput(deviceInput){
                    session.addInput(deviceInput)
                }
                // Add audio
                if session.canAddInput(audioDeviceInput){
                    session.addInput(audioDeviceInput)
                }
                
                
                self.movieOutput = AVCaptureMovieFileOutput()
                if session.canAddOutput(self.movieOutput!){
                    session.addOutput(self.movieOutput!)
                }
                
                if let connection = self.movieOutput!.connection(with: .video) {
                    if connection.isVideoStabilizationSupported {
                        connection.preferredVideoStabilizationMode = .auto
                    }
                }
                
                self.setupPreviewLayer()
                session.commitConfiguration()
                session.startRunning()
                completion()
            }
        }
        
    } catch let error as NSError {
        deviceInput = nil
        print("Device Input Error: \(error.localizedDescription)")
    }
}
The session is configured on a dedicated serial queue (sessionQueue) to avoid blocking the main thread during camera setup.

Preview Layer

The preview layer displays the camera feed:
CameraManager.swift:60-76
func addPreviewLayerToView(view: UIView){
    if cameraAndAudioAccessPermitted {
        if let previewLayer = previewLayer {
            previewLayer.removeFromSuperlayer()
        }
        setupCamera {
            self.embeddingView = view
            DispatchQueue.main.async {
                guard let previewLayer = self.previewLayer else { return }
                previewLayer.frame = view.layer.bounds
                view.clipsToBounds = true
                view.layer.addSublayer(previewLayer)
            }
        }
    }
}

Recording Flow

1
Step 1: Start Recording
2
When the user presses the record button, recording starts and saves to a temporary file:
3
func startRecording(){
    movieOutput?.startRecording(to: tempFilePath, recordingDelegate: self)
}
4
The temporary file path is dynamically generated:
5
fileprivate var tempFilePath: URL {
    get{
        let tempURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("tempMovie\(Date())").appendingPathExtension(VIDEO_FILE_EXTENSION)
        return tempURL
    }
}
6
Step 2: Stop Recording
7
When the button is released, recording stops:
8
func stopRecording(){
    captureSession?.stopRunning()
    if let output = self.movieOutput, output.isRecording {
        output.stopRecording()
    }
}
9
Step 3: Recording Complete
10
The delegate is called when recording finishes:
11
func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {
    if let error = error {
        print("Saving video failed: \(error.localizedDescription)")
    } else {
        delegate.finishRecording(outputFileURL, error)
    }
}

RecordButton Animation

The record button features a custom animation that transforms when pressed:

Button Structure

The button consists of two layers:
RecordButton.swift:26-44
override func awakeFromNib() {
    super.awakeFromNib()
    originalCenter = self.center
    
    outerLayer = CAShapeLayer()
    outerLayer.frame = self.bounds
    outerLayer.cornerRadius = originalRadius
    outerLayer.backgroundColor = UIColor.clear.cgColor
    outerLayer.borderColor = UIColor.Red.withAlphaComponent(0.5).cgColor
    outerLayer.borderWidth = outerLineWidth
    self.layer.addSublayer(outerLayer)
    
    innerLayer = CAShapeLayer()
    let offset = outerLineWidth + spacing
    innerLayer.frame = CGRect(x: offset, y: offset, width: (originalRadius - offset) * 2, height: (originalRadius - offset) * 2)
    innerLayer.cornerRadius = originalRadius - offset
    innerLayer.backgroundColor = UIColor.Red.cgColor
    self.layer.addSublayer(innerLayer)
}

Start Recording Animation

When recording starts, the button transforms from a circle to a rounded square:
RecordButton.swift:49-70
func startRecordingAnimation(){
    removeAnimations()
    
    let outerBorderColorAnimation = RecordAnimation(keyPath: "borderColor")
    outerBorderColorAnimation.fromValue = outerLayer.borderColor
    outerBorderColorAnimation.toValue = UIColor.Red.cgColor
    let outerScaleAnimation = RecordAnimation(keyPath: "transform.scale")
    outerScaleAnimation.fromValue = 1
    outerScaleAnimation.toValue = 1.5
    
    outerLayer.add(outerBorderColorAnimation, forKey: "outerBackgroundColorAnimation")
    outerLayer.add(outerScaleAnimation, forKey: "outerScaleAnimation")
    
    let innerCornerRadiusAnimation = RecordAnimation(keyPath: "cornerRadius")
    innerCornerRadiusAnimation.fromValue = innerLayer.cornerRadius
    innerCornerRadiusAnimation.toValue = CGFloat(5)
    let innerScaleAnimation = RecordAnimation(keyPath: "transform.scale")
    innerScaleAnimation.fromValue = 1
    innerScaleAnimation.toValue = 0.5
    innerLayer.add(innerCornerRadiusAnimation, forKey: "innerCornerRadiusAnimation")
    innerLayer.add(innerScaleAnimation, forKey: "innerScaleAnimation")
}

Stop Recording Animation

When recording stops, the button animates back to its original state:
RecordButton.swift:76-101
func stopRecodingAnimation(){
    let outerBorderColorAnimation2 = RecordAnimation(keyPath: "borderColor")
    outerBorderColorAnimation2.fromValue = UIColor.Red.cgColor
    outerBorderColorAnimation2.toValue = UIColor.Red.withAlphaComponent(0.5).cgColor
    let outerScaleAnimation2 = RecordAnimation(keyPath: "transform.scale")
    outerScaleAnimation2.fromValue = 1.5
    outerScaleAnimation2.toValue = 1

    outerLayer.add(outerBorderColorAnimation2, forKey: "outerBackgroundColorAnimation2")
    outerLayer.add(outerScaleAnimation2, forKey: "outerScaleAnimation2")

    let innerCornerRadiusAnimation2 = RecordAnimation(keyPath: "cornerRadius")
    innerCornerRadiusAnimation2.fromValue = CGFloat(5)
    innerCornerRadiusAnimation2.toValue = CGFloat( originalRadius - outerLineWidth - spacing)
    let innerScaleAnimation2 = RecordAnimation(keyPath: "transform.scale")
    innerScaleAnimation2.fromValue = 0.5
    innerScaleAnimation2.toValue = 1
    innerLayer.add(innerCornerRadiusAnimation2, forKey: "innerCornerRadiusAnimation2")
    innerLayer.add(innerScaleAnimation2, forKey: "innerScaleAnimation2")
    
    UIView.animate(withDuration: TimeInterval(animationDuration), animations: { [weak self] in
        guard let self = self else { return }
        self.center = self.originalCenter!
    })
}
The RecordAnimation class is a custom CABasicAnimation subclass with pre-configured properties:
class RecordAnimation: CABasicAnimation {
    override init() {
        super.init()
        duration = 0.7
        fillMode = .forwards
        isRemovedOnCompletion = false
    }
}

Gesture Recognition

The recording is controlled by a long-press gesture:
MediaViewController.swift:252-264
@objc fileprivate func recordBtnPressed(sender: UILongPressGestureRecognizer){
    let location = sender.location(in: self.view)
    switch sender.state {
    case .began:
        startRecording()
    case .changed:
        recordView.locationChanged(location: location)
    case .cancelled, .ended:
        stopRecording()
    default:
        break
    }
}

Saving to Temp Directory

Videos are temporarily stored in the app’s temp directory:
CameraManager.swift:178-183
func removeAllTempFiles(){
    var directory = NSTemporaryDirectory()
    sessionQueue.async {
        directory.removeAll()
    }
}
Always clear temporary files when the view loads to avoid storage buildup:
MediaViewController.swift:54-57
override func viewDidLoad() {
    super.viewDidLoad()
    // Clear files from previous sessions
    cameraManager.removeAllTempFiles()
    setupView()
}

Saving to Photo Library

After recording, videos can be saved to the photo library:
CameraManager.swift:226-251
func saveToLibrary(videoURL: URL){
    func save(){
        sessionQueue.async { [weak self] in
            self?.photoLibrary?.performChanges({
                PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: videoURL)
            }, completionHandler: { (saved, error) in
                if let error = error {
                    print("Saving to Photo Library Failed: \(error.localizedDescription)")
                } else {
                    print("Saved at: \(videoURL)")
                }
            })
        }
    }
    
    if PHPhotoLibrary.authorizationStatus() != .authorized {
        PHPhotoLibrary.requestAuthorization({ status in
            if status == .authorized {
                save()
            }
        })
    } else {
        save()
    }
}

Permission Handling

1
Camera Access
2
@objc func askForCameraAccess(_ completion: @escaping AccessPermissionCompletionBlock){
    AVCaptureDevice.requestAccess(for: .video, completionHandler: { [weak self] access in
        self?.checkIfBothPermissionGranted()
        DispatchQueue.main.async {
            completion(access)
        }
    })
}
3
Microphone Access
4
@objc func askForMicrophoneAccess(_ completion: @escaping AccessPermissionCompletionBlock){
    AVCaptureDevice.requestAccess(for: .audio, completionHandler: { [weak self] access in
        self?.checkIfBothPermissionGranted()
        DispatchQueue.main.async {
            completion(access)
        }
    })
}

Build docs developers (and LLMs) love