Video Recorder App with SwiftUI [1] — Set Up

Enebin
6 min readJun 11, 2023

--

Photo by Thomas William on Unsplash

The existence of UIImagePickerController is enough to help apps that simply need to take photos or shoot videos. That's what I thought, too. I believed that by using this API while preparing my personal project app, the video shooting part would be solved like a charm. In conclusion, I was wrong.

UIImagePickerController is an API that provides only very basic functionalities. Thus, it does not allow for detailed adjustments, and this is its biggest disadvantage.

Among the features I wanted to implement was the ability to shoot a video while playing music. To achieve this, I had to eliminate audio input during video recording. I searched in many different ways, but I was disappointed when I finally realized that it was impossible. ‘Do I have to look into AVFoundation from the beginning? That's dreadful!'

Nevertheless, I successfully developed the app and deepened my understanding of AVFoundation through numerous refactoring and modularization processes. I didn't want to keep it to myself. So, I decided to write this article to summarize my experience. While it's not a short process, as long as you understand the order of operations correctly, it's not that complex task.

To help you understand, I plan to provide visual materials using an amazing graph tool Mermaid. Unfortunately, Medium can’t render Mermaid, so please understand that I'll be using captured images instead of the original ones.

Then, let’s dive in!

First, please note that the project is based on the following environment:

  • Xcode 14+
  • iOS 14+
  • SwiftUI

Overview

There are mainly three steps to record a video on iOS.

1. Check permission and set up a session

2. Connect Input and Output to AVCaptureSession

3. Start/stop recording

  • (Optional) Register a file in the album roll of the built-in album app

Many parts are omitted on the graph, but the overall flow does not deviate from this. So for now, it’s enough to understand it goes through such a process.

⚠️ Warning
If an error occurs even once during the process, you cannot proceed to the next step. Therefore, it is important to identify where the error occurred. This is also why dealing with videos is difficult.

1. Check permission and set up a session

Permission check

Let’s start from the part requesting permission and processing the result of request.

Step1

First, check if you have written a Privacy description in info.plist. This is something developers have to handle on their own. For more information, please refer to the document.

Step 2

We’re good to go now. The code to request permission is as follows.

struct AuthorizationChecker {
static func checkCaptureAuthorizationStatus() async -> Status {
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
return .permitted

case .notDetermined:
let isPermissionGranted = await AVCaptureDevice.requestAccess(for: .video)
if isPermissionGranted {
return .permitted
} else {
fallthrough
}

case .denied:
fallthrough

case .restricted:
fallthrough

@unknown default:
return .notPermitted
}
}
}

extension AuthorizationChecker {
enum Status {
case permitted
case notPermitted
}
}
  • There’re four basic status for the result of the request, but for convenience, we’ll simplify them into two cases, permitted and notPermitted.

Note
According to the document, along with denied, restricted is also treated as a state where you can't use the camera. Therefore, it returns notPermitted.

Session setup

Now look into how to configure the session. It’s quite easy to set up a session. All you need to do is create an instance of AVCaptureSession and connect the inputs and outputs!

Step1

You can create an instance of AVCaptureSession just by calling its constructor. No arguments are required, nor do you need to handle errors with try.

let session = AVCaptureSession()

Step2

Now, let’s connect the inputs and outputs. Although it’s possible to use separate classes, I’ve added a slight variation to increase readability.

extension AVCaptureSession {
var movieFileOutput: AVCaptureMovieFileOutput? {
let output = self.outputs.first as? AVCaptureMovieFileOutput

return output
}

func addMovieInput() throws -> Self {
// Add video input
guard let videoDevice = AVCaptureDevice.default(for: AVMediaType.video) else {
throw VideoError.device(reason: .unableToSetInput)
}

let videoInput = try AVCaptureDeviceInput(device: videoDevice)
guard self.canAddInput(videoInput) else {
throw VideoError.device(reason: .unableToSetInput)
}

self.addInput(videoInput)

return self
}

func addMovieFileOutput() throws -> Self {
guard self.movieFileOutput == nil else {
// return itself if output is already set
return self
}

let fileOutput = AVCaptureMovieFileOutput()
guard self.canAddOutput(fileOutput) else {
throw VideoError.device(reason: .unableToSetOutput)
}

self.addOutput(fileOutput)

return self
}
}

While it might seem complicated at first glance, it actually has a very simple structure.

  • Call the default method from AVCaptureDevice to bring in a video device, and if it can be connected, call canAddInput to connect it.
  • Create AVCaptureMovieFileOutput and if it can be connected, call canAddOutput to connect it. Note that AVCaptureMovieFileOutput is created directly as an instance, unlike the input case.
  • Pretty simple, right? Now you’re ready to start recording.
  • But as I’ve mentioned before, if there was an error previously, the process may not continue. While throwing a proper error can be cumbersome, it’s worth the trouble in advance for the sake of maintenance convenience.

Connect

Now let’s connect the permission check with the session configuration. If the permission check was done and the result is permitted, we can proceed with the session configuration.

Step1

class VideoContentViewModel: ObservableObject {
init() {
Task {
switch await AuthorizationChecker.checkCaptureAuthorizationStatus() {
case .permitted:
// Setting session
case .notPermitted:
// Hmm...
}
}
}
}
  • It doesn’t matter where, but in this example, we check the permissions in the ViewModel constructor. Of course, the ViewModel must be instantiated in View.
  • The reason for using Task is because the permission check is an asynchronous (async) operation. If you are not familiar with Swift Concurrency, please refer to this document.

Step 2

class VideoContentViewModel: ObservableObject {
let session: AVCaptureSession

init() {
self.session = AVCaptureSession()

Task(priority: .background) {
switch await AuthorizationChecker.checkCaptureAuthorizationStatus() {
case .permitted:
try session
.addMovieInput()
.addMovieFileOutput()
.startRunning()

case .notPermitted:
break
}
}
}
}
  • As shown in the example above, create a session and then run the add{something} methods you created in order. Thanks to returning Self in the extension, you can create a beautiful chaining syntax.
  • The startRunning method is, as its name implies, a method that starts the session. According to the official documentation, startRunning is a thread-blocking method that can impair responsiveness if run on the main thread. Therefore, it is good to run it in the background. This is why the background priority is given to Task.

Check

  1. Does a pop-up asking for permission appear when you run the app?
  2. Does the app operate normally without crashing?

Wrapping Up

We haven’t connected the preview screen yet, so we can’t see the camera screen.

In the next post, we’ll connect the screen and observe the results. We’ll also implement the startRecording and stopRecording methods to record a video and check if it's saved properly.

Advertisement..👋

Aespa: Easiest camera handling package ever for SwiftUI & UIKit

I’ve created a package that compresses the tedious process of connecting videos into just three lines.

It’s designed to be simple enough for someone who is developing for iOS for the first time and provides essential default settings for video recording. Of course, customization is possible if you want.

It offers beautiful documentation using Swift DoCC and a demo app that provides detailed implementation examples, so feel free to take a look if you’re interested. I would really appreciate it if you could give a star!

--

--