Build a Custom iOS Camera that Replicates System Camera App

Brightec developed iOS camera that replicates system camera app 1

How to create a custom camera view controller that replicates the functionality and UI that’s found in the system camera app.

Custom iOS Camera View Controller: An Introduction

With most things in life, there's an 'easy way' (not actually the way) and a 'hard way' (actually the way to do it properly).

The same principle usually follows in app development. Cheats and shortcuts are often sought after by developers, but they rarely prove fruitful in the long term.

So first of all, let's explore the easy way.

Building a Custom iOS Camera View: The Easy Way

If you want to let users take pictures in your app, you could use the built-in UIImagePickerController and you’d have a working solution with only a few lines of code:

- (void)showNativeCamera
{
 if (![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) return;

 UIImagePickerController *imagePickerController = [[UIImagePickerController alloc] init];
 imagePickerController.sourceType = UIImagePickerControllerSourceTypeCamera;
 imagePickerController.delegate = self;

 [self presentViewController:imagePickerController animated:YES completion:nil];
}

- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
 UIImage *image = info[UIImagePickerControllerOriginalImage];

 [self dismissViewControllerAnimated:YES completion:nil];
}

Building a Custom iOS Camera View: The Hard Way (in 5 Simple Steps)

Unfortunately, it’s difficult to customise the UI of this controller and you don’t get all the functionality like opening the image gallery.

An alternative is to build your own camera view controller. We could call this the 'hard way' but don't worry, we’ll go through it all in this post and in 5 steps it'll all be complete.

1. Build the UI

A good place to start is the UI. We build a xib file containing all the UI elements that we want to replicate, in this case the flash mode; camera picker (front/back); image gallery access; shutter button and cancel button.

image00.png

All these elements need outlets in the code, as well as some properties to store our state:

@interface CameraViewController () 
@property (weak, nonatomic) IBOutlet UIView *topBarView;
@property (weak, nonatomic) IBOutlet UIView *bottomBarView;
@property (weak, nonatomic) IBOutlet UIView *cameraContainerView;
@property (weak, nonatomic) IBOutlet UIButton *flashButton;
@property (weak, nonatomic) IBOutlet UIView *flashModeContainerView;
@property (weak, nonatomic) IBOutlet UIButton *flashAutoButton;
@property (weak, nonatomic) IBOutlet UIButton *flashOnButton;
@property (weak, nonatomic) IBOutlet UIButton *flashOffButton;
@property (weak, nonatomic) IBOutlet UIButton *cameraButton;
@property (weak, nonatomic) IBOutlet UIButton *openPhotoAlbumButton;
@property (weak, nonatomic) IBOutlet UIButton *takePhotoButton;
@property (weak, nonatomic) IBOutlet UIButton *cancelButton;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *cameraViewTopConstraint;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *cameraViewBottomConstraint;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *bottomBarHeightConstraint;

@property (strong, nonatomic) UIVisualEffectView *blurView;

@property (nonatomic, strong) AVCaptureSession *session;
@property (nonatomic, strong) UIView *capturePreviewView;
@property (nonatomic, strong) AVCaptureVideoPreviewLayer *capturePreviewLayer;
@property (nonatomic, strong) NSOperationQueue *captureQueue;
@property (nonatomic, assign) UIImageOrientation imageOrientation;
@property (assign, nonatomic) AVCaptureFlashMode flashMode;
@end
,>

To keep the UI consistent with the camera app, we apply a couple of tweaks in viewDidLoad:

 // 3.5" and 4" devices have a smaller bottom bar
 if (CGRectGetHeight([UIScreen mainScreen].bounds) <= 568.0f) {
 self.bottomBarHeightConstraint.constant = 91.0f;
 [self.bottomBarView layoutIfNeeded];
 }

 // 3.5" devices have the top and bottom bars over the camera view
 if (CGRectGetHeight([UIScreen mainScreen].bounds) == 480.0f) {
 self.cameraViewTopConstraint.constant = -CGRectGetHeight(self.topBarView.frame);
 self.cameraViewBottomConstraint.constant = -CGRectGetHeight(self.bottomBarView.frame);
 [self.cameraContainerView layoutIfNeeded];
 }

2. Capturing an Image

As the focus of this project is to replicate the UI and functionality of the system camera app, it made sense to find an existing implementation of the code to actually open the camera device and capture the image. In this case I used JPSImagePickerController.

The main methods involved are:

  • EnableCapture - this creates an operation to start capturing, assigns a completion block and adds the operation to the queue.

  • CaptureOperation - The operation creates a session, assigns an input to it, configures the device with focus and flash modes, creates a preview of the input and define the output settings.

  • OperationCompleted - Adds the preview to the UI and starts the session.

3. Handling orientation

One of the bits of functionality that is worth explaining is how we handle orientation. Unlike a normal UI where views keep their hierarchy regardless of orientation e.g. a view pinned to the top is always at the top, in the camera app the top/bottom bars in portrait become left/right bars in landscape.

To replicate this we need to stop the OS from rotating the UI by implementing shouldAutorotate and supportedInterfaceOrientations.

This gives us the effect we want, however, as commented below, it has the side effect that the OS always thinks the phone is in portrait. This isn’t quite how the system camera app works but is only a minor issue.

- (BOOL)shouldAutorotate
{
 // We'll rotate the UI elements manually.
 // The downside of this is that the device is technically always in portrait
 // which means that the Control Center always pulls up from the edge with
 // the home button even when the device is landscape
 return NO;
}


- (UIInterfaceOrientationMask)supportedInterfaceOrientations
{
 return UIInterfaceOrientationMaskPortrait;
}

Having blocked the system rotation, we can improve the UI by manually rotating the individual UI elements. This can be done by applying a transform to each element based on the orientation:

- (void)updateOrientation
{
 UIDeviceOrientation deviceOrientation = [UIDevice currentDevice].orientation;

 CGFloat angle;
 switch (deviceOrientation) {
 case UIDeviceOrientationPortraitUpsideDown:
 angle = M_PI;
 break;

 case UIDeviceOrientationLandscapeLeft:
 angle = M_PI_2;
 break;

 case UIDeviceOrientationLandscapeRight:
 angle = - M_PI_2;
 break;

 default:
 angle = 0;
 break;
 }

 [UIView animateWithDuration:.3 animations:^{
 self.flashButton.transform = CGAffineTransformMakeRotation(angle);
 self.flashAutoButton.transform = CGAffineTransformMakeRotation(angle);
 self.flashOnButton.transform = CGAffineTransformMakeRotation(angle);
 self.flashOffButton.transform = CGAffineTransformMakeRotation(angle);
 self.cameraButton.transform = CGAffineTransformMakeRotation(angle);
 self.openPhotoAlbumButton.transform = CGAffineTransformMakeRotation(angle);
 self.takePhotoButton.transform = CGAffineTransformMakeRotation(angle);
 self.cancelButton.transform = CGAffineTransformMakeRotation(angle);
 }];
}

This method needs to be called from viewDidLoad and when the UIDeviceOrientationDidChangeNotification is sent.

4. Flash Mode

Each of the flash mode buttons calls the same method which simply updates a state variable recording the flash mode that the user has chosen, then calls a couple of helper methods to update the camera and the UI.

- (IBAction)flashModeButtonWasTouched:(UIButton *)sender
{
 if (sender == self.flashAutoButton) {
 self.flashMode = AVCaptureFlashModeAuto;
 } else if (sender == self.flashOnButton) {
 self.flashMode = AVCaptureFlashModeOn;
 } else {
 self.flashMode = AVCaptureFlashModeOff;
 }

 [self updateFlashlightState];

 [self toggleFlashModeButtons];
}

- (void)updateFlashlightState
{
 if (![self currentDevice]) return;

 self.flashAutoButton.selected = self.flashMode == AVCaptureFlashModeAuto;
 self.flashOnButton.selected = self.flashMode == AVCaptureFlashModeOn;
 self.flashOffButton.selected = self.flashMode == AVCaptureFlashModeOff;

 AVCaptureDevice *device = [self currentDevice];
 NSError *error = nil;
 BOOL success = [device lockForConfiguration:&error];
 if (success) {
 device.flashMode = self.flashMode;
 }
 [device unlockForConfiguration];
}

- (void)toggleFlashModeButtons
{
 [UIView animateWithDuration:0.3f animations:^{
 self.flashModeContainerView.alpha = self.flashModeContainerView.alpha == 1.0f ? 0.0f : 1.0f;
 self.cameraButton.alpha = self.cameraButton.alpha == 1.0f ? 0.0f : 1.0f;
 }];
}

5. Switching Camera

To switch between the front and back cameras we need to get the new capture device, remove the current device from the session then add the new one. To give a better transition and to replicate the camera app, we add a blur view and flip the UI before swapping the device.

- (IBAction)cameraButtonWasTouched:(UIButton *)sender
{
 if (!self.session) return;
 [self.session stopRunning];

 // Input Switch
 NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
 AVCaptureDeviceInput *input = self.session.inputs.firstObject;

 AVCaptureDevice *newCamera = nil;

 if (input.device.position == AVCaptureDevicePositionBack) {
 newCamera = [self frontCamera];
 } else {
 newCamera = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
 }

 // Should the flash button still be displayed?
 dispatch_async(dispatch_get_main_queue(), ^{
 self.flashButton.hidden = !newCamera.isFlashAvailable;
 });

 // Remove previous camera, and add new
 [self.session removeInput:input];
 NSError *error = nil;

 input = [AVCaptureDeviceInput deviceInputWithDevice:newCamera error:&error];
 if (!input) return;
 [self.session addInput:input];
 }];
 operation.completionBlock = ^{
 dispatch_async(dispatch_get_main_queue(), ^{
 if (!self.session) return;
 [self.session startRunning];
 [self.blurView removeFromSuperview];
 });
 };
 operation.queuePriority = NSOperationQueuePriorityVeryHigh;

 // disable button to avoid crash if the user spams the button
 self.cameraButton.enabled = NO;

 // Add blur to avoid flickering
 self.blurView.hidden = NO;
 [self.capturePreviewView addSubview:self.blurView];
 [self.blurView autoPinEdgesToSuperviewEdges];
 
 // Flip Animation
 [UIView transitionWithView:self.capturePreviewView
 duration:0.5f
 options:UIViewAnimationOptionTransitionFlipFromLeft | UIViewAnimationOptionAllowAnimatedContent
 animations:nil
 completion:^(BOOL finished) {
 self.cameraButton.enabled = YES;
 [self.captureQueue addOperation:operation];
 }];
}

It's all on GitHub

We’ve covered the main aspects of building a custom iOS camera that replicates a system camera app in this post but there will be other bits of code that weren’t explicitly mentioned here.

All of the code in a working project, as well as a Swift translation, is available on GitHub.


Looking for something else?

Search over 400 blog posts from our team

Want to hear more?

Subscribe to our monthly digest of blogs to stay in the loop and come with us on our journey to make things better!