iOS — How to Integrate Camera APIs using SwiftUI — Part 2

Effortless Camera APIs Integration in SwiftUI: Leverage the Power of Your Device's Camera
Dec 14 2023 · 8 min read

Background

Welcome back to the continuation of our journey with SwiftUI and Camera APIs!

In the First Part of this comprehensive guide, we took you through the foundational steps of seamlessly integrating SwiftUI with Camera APIs. We covered essential aspects, including the attachment of a camera preview to your UI, enabling flash support, focusing controls, zoom functionalities, and the crucial process of capturing and storing preview images.

If you missed the first part, don’t worry! You can read it from this given link,

In this part, we’re going to delve deeper into the exploration of camera settings! We’ll dive deeper into our camera app’s settings about adjusting exposure, switching camera lenses, and fine-tuning color temperature with SwiftUI.

Whether you’re an experienced developer or just starting your journey, we’re here to simplify the utilization of Camera APIs for all skill levels.

We are what we repeatedly do. Excellence, then, is not an act, but a habit. Try out Justly and start building your habits today!
 

Introduction

In this guide, our focus remains centered on SwiftUI as we expand our existing project to integrate all camera setting options from the previous section that we created with our last part.

Here is the setting view UI which we are going to design throughout this blog,

The source code is available on the GitHub.

Get started!

Let’s start with adding a handy setting button to the top-right corner, to grant access to the camera preview’s additional functionalities.

Let’s first add one flag to ViewModel to manage the setting view’s appearance.

@Published var showSettingView = false

Now, update the top bar to add a setting button in which we have added a flash button, here is the updated code.

HStack(spacing: 0) {
    Button(action: {
        viewModel.switchFlash()
   }, label: {
        Image(systemName: viewModel.isFlashOn ? "bolt.fill" : "bolt.slash.fill")
          .font(.system(size: 20, weight: .medium, design: .default))
    })
    .accentColor(viewModel.isFlashOn ? .yellow : .white)
    .frame(maxWidth: .infinity, alignment: .trailing)
 
    Button(action: {
        viewModel.showSettingView.toggle()
    }, label: {
        Image(systemName: "gearshape.fill")
          .font(.system(size: 20, weight: .medium, design: .default))
    })
    .accentColor(viewModel.showSettingView ? .accentColor : .white)
    .frame(maxWidth: .infinity, alignment: .trailing)
    .padding(.trailing, 10)
}
.padding(.vertical, 5)

Let’s update the main view’s code to display the settings view and the Focus view in the content view to accommodate the settings view when the Focus view is active.

if showFocusView {
   FocusView(position: $focusLocation)
      .scaleEffect(isScaled ? 0.8 : 1)
      .onAppear {
         withAnimation(.spring(response: 0.4, dampingFraction: 0.6, blendDuration: 0)) {
            self.isScaled = true
            self.viewModel.showSettingView = false
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {
               self.showFocusView = false
               self.isScaled = false
            }
         }
      }
} else if viewModel.showSettingView {
   // open a setting overlay view
   SettingView()
}

Let’s design the setting options overlay view,

struct SettingView: View {
   var body: some View {
      VStack {
          HStack {
            Text("Camera Configurations")
             .font(.title3)
             .frame(maxWidth: .infinity, alignment: .center)
          }
          .frame(height: 40)
          .background(Color.black.opacity(0.3))
          .padding(.horizontal, -16)

           Spacer()
             .frame(height: 10)
      }
      .padding(.horizontal, 16)
      .background(Color.black.opacity(0.4))
      .padding(.horizontal, 16)
   }
}

We'll add all the setting options individually along with the further implementation.

Adjust Exposure Composition

Let’s introduce the exposure Composition feature by enabling users to adjust exposure levels for capturing optimal shots. We’ll provide a slider interface, allowing smooth adjustments for exposure compensation.

CameraManager Class Updates

Add a new method setExposureCompensation to adjust the exposure compensation value:

func setExposureCompensation(value: Float) {
    // Retrieve the device associated with the current video input
    guard let device = self.videoDeviceInput?.device else { return }
    
    // Ensure the device supports continuous auto exposure mode
    if device.isExposureModeSupported(.continuousAutoExposure) {
        do {
            // Lock the device configuration to apply exposure compensation
            try device.lockForConfiguration()
            
            // Set the exposure compensation value
            device.setExposureTargetBias(value, completionHandler: nil)
            
            // Unlock the device configuration after applying changes
            device.unlockForConfiguration()
        } catch {
            // Handle errors in adjusting exposure compensation
            print("Failed to adjust exposure compensation: \(error)")
        }
    }
}

ViewModel Updates

Add exposure-related properties and a subscription to handle exposure value changes:

@Published var exposure: Float = 0

var cancelables = Set<AnyCancellable>()

func setupBindings() {
    $exposure.sink { [weak self] value in
        self?.cameraManager.setExposureCompensation(value: value)
    }.store(in: &cancelables)
}

UI Changes

Add an ExposureView component, featuring a slider for adjusting exposure levels:

struct ExposureView: View {
    @Binding var value: Float // Binding to control the exposure
    
    var body: some View {
        VStack(spacing: 2) {
            HStack {
                Text("Exposure") // Title
                Slider(value: $value, in: -3...3) // Slider for exposure adjustment
                Text(String(format: "%.1f", value) + "EV") // Display current exposure value with EV suffix
            }
            DividerView() // Divider for visual separation
        }
    }
}

struct DividerView: View {
    var body: some View {
       Divider()
          .background(Color.gray.opacity(0.5))
          .frame(height: 1)
          .padding(.horizontal, -14)
    }
}

Now, let’s update the SettingView,

struct SettingView: View {

    @Binding var exposure: Float

    var body: some View {
        VStack {
            HStack {
              ...
            }

            ExposureView(value: $exposure)
        }
        .padding(.horizontal, 16)
        .background(Color.black.opacity(0.4))
        .padding(.horizontal, 16)
    }
}

As you add properties to SettingView, ensure to update its constructor accordingly for each new property inclusion.

Test these features by adjusting the exposure level using the slider available within the settings view.

Change Camera Lens

Switching between different lenses can significantly alter the visual perspectives of the captured image. Here, we’ll focus on seamlessly changing the lens configuration within our camera app.

CameraManager Updates

// use this in the setupVideoInput method where we are creating camera device with static type
private var captureDeviceType: AVCaptureDevice.DeviceType = .builtInWideAngleCamera

func switchLensTo(newDevice: AVCaptureDevice) {
    do {
        let newVideoInput = try AVCaptureDeviceInput(device: newDevice)
        
        // Remove the existing input
        if let videoDeviceInput {
            session.removeInput(videoDeviceInput)
        }
        
        // Add the new input
        if session.canAddInput(newVideoInput) {
            session.addInput(newVideoInput)
            videoDeviceInput = newVideoInput
            captureDeviceType = newDevice.deviceType
        }
    } catch {
        print("Error switching lens: \(error.localizedDescription)")
    }
}
 
func getCaptureDeviceFrom(selectedLens: Int) -> AVCaptureDevice? {
    let videoDevices = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera, .builtInUltraWideCamera, .builtInTelephotoCamera, .builtInDualCamera, .builtInTripleCamera], mediaType: .video, position: position).devices
    
    return videoDevices.first { device in
       switch selectedLens {
           case 0: // Wide
              return device.deviceType == .builtInWideAngleCamera
           case 1: // Tele
              return device.deviceType == .builtInTelephotoCamera
           case 2: // Ultra Wide
              return device.deviceType == .builtInUltraWideCamera
           case 3: // Dual
              return device.deviceType == .builtInDualCamera
           case 4: // Triple
              return device.deviceType == .builtInTripleCamera
           default:
              return false
       }
    }
 }

ViewModel Updates

Let’s add a subscription of selectedLensIndex in the setupBindings method.

@Published var selectedLensIndex = 0

$selectedLensIndex.sink { [weak self] index in
    guard let self else { return }
    if let device = self.cameraManager.getCaptureDeviceFrom(selectedLens: index) {
        self.cameraManager.switchLensTo(newDevice: device)
    }
}.store(in: &cancelables)

Here, with the change in the lens selection, we are triggering the camera to switch to the desired lens.

UI Updates

struct LensView: View {
    @Binding var lens: Int // Binding to manage the lens selection
    
    let deviceLens: [String] = ["Wide", "Tele", "Ultra Wide", "Dual", "Triple"] // Available lens options
    
    var body: some View {
        VStack(spacing: 5) {
            HStack {
                Text("Lens") // Title
                Spacer()
                Picker("", selection: $lens) {
                    ForEach(0..<deviceLens.count, id: \.self) { index in
                        Text(deviceLens[index])
                    }
                }
                .pickerStyle(.segmented) // Display as segmented picker
                .frame(height: 30)
            }
            .frame(height: 35)
            .padding(.trailing, -10)
            
            DividerView() // Visual separation
        }
        .padding(.top, -3)
    }
}

Now, we can add this view to the setting view wherever you want to add it by providing lensIndex value.

LensView(lens: $lensIndex)

If the selected lens is not available for your current device then it will not show any change in the preview.

You can check for the available device and change the segment value to the specific index. Here are some extra methods for that,

func getAvailableLens() -> AVCaptureDevice.DeviceType {
   let videoDevices = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera, .builtInUltraWideCamera, .builtInTelephotoCamera, .builtInDualCamera, .builtInTripleCamera], mediaType: .video, position: position).devices
   var deviceType: AVCaptureDevice.DeviceType = .builtInWideAngleCamera
 
   for device in videoDevices {
      if device.deviceType == .builtInTelephotoCamera {
         deviceType = .builtInTelephotoCamera
         break
      } else if device.deviceType == .builtInDualCamera {
         deviceType = .builtInDualCamera
         break
      } else {
         deviceType = .builtInWideAngleCamera
      }
   }
   return deviceType
}

func getLensIndexFrom(deviceType: AVCaptureDevice.DeviceType) -> Int {
    switch deviceType {
    case .builtInWideAngleCamera:
       return 0
    case .builtInTelephotoCamera:
       return 1
    case .builtInUltraWideCamera:
       return 2
    case .builtInDualCamera:
       return 3
    case .builtInTripleCamera:
       return 4
    default:
       return 0
    }
 }

Now let’s test the app.


Adjusting Color Temperature

Let’s explore the Color Temperature feature, essential in photography for adjusting the warmth or coolness of the image.

This feature allows users to fine-tune the color temperature of their captured images by adjusting the white balance settings.

CameraManager Class Update

// to handle the camera configurations
func setColorTemperature(temperature: Float) {
    guard let device = self.videoDeviceInput?.device else { return }
    
    guard device.isWhiteBalanceModeSupported(.locked) else {
        print("White balance mode not supported.")
        return
    }
    do {
        try device.lockForConfiguration()
        
        // Set the white balance mode to locked
        device.whiteBalanceMode = .locked
        
        // Set the desired temperature and tint values
        let gains: AVCaptureDevice.WhiteBalanceGains
        if temperature == 0 {
            gains = device.grayWorldDeviceWhiteBalanceGains
        } else {
            let colorTemp = AVCaptureDevice.WhiteBalanceTemperatureAndTintValues(temperature: temperature, tint: 0.0)
            gains = device.deviceWhiteBalanceGains(for: colorTemp)
        }
        device.setWhiteBalanceModeLocked(with: gains)
        device.unlockForConfiguration()
    } catch {
        print("Failed to configure white balance: \(error)")
    }
}

ViewModel Changes

In the ViewModel, we effectively handle changes in the color temperature’s value using selectedTemperatureIndex and selectedTemperatureValue, with ensuring precise adjustments for the camera’s temperature settings.

@Published var selectedTemperatureIndex = 0
@Published var selectedTemperatureValue: Float = 4000

// Handling changes in temperature index and value
// Add this subscriptions to setupBindings method
$selectedTemperatureIndex.sink { [weak self] index in
    if index == 0 {
        self?.selectedTemperatureValue = 4000
    }
}.store(in: &cancelables)

$selectedTemperatureValue.sink { [weak self] temp in
    self?.cameraManager.setColorTemperature(temperature: temp)
}.store(in: &cancelables)

UI Updates

struct ColorTempView: View {
    @Binding var index: Int // Binding to manage the temperature index
    
    var body: some View {
        VStack(spacing: 5) {
            HStack {
                Text("Color Temperature") // Title
                Spacer()
                Picker("", selection: $index) {
                    Text("Auto").tag(0)
                    Text("Fixed").tag(1)
                }
                .pickerStyle(.segmented) // Display as segmented picker
                .frame(height: 30)
            }
            .frame(height: 35)
            .padding(.trailing, -10)
            
            DividerView() // Visual separation
        }
        .padding(.top, -3)
    }
}

struct ColorTempSliderView: View {
    @Binding var value: Float // Binding to control the temperature value
    @Binding var index: Int // Binding to manage the temperature index
    
    var body: some View {
        VStack(spacing: 0) {
            HStack {
                Slider(value: $value, in: 2500...9000) // Slider for temperature adjustment
                    .disabled(index == 0) // Disable for auto mode
                Text(String(format: "%.0f", value) + "K") // Display current temperature value
            }
            .padding(.top, -3)
            
            Spacer()
                .frame(height: 5)
        }
    }
}

Now, let’s update the SettingView,

@Binding var tempIndex: Int
@Binding var tempValue: Float

var body: some View {
  VStack {
    .
    .
    .
    ColorTempView(index: $tempIndex)
   
    ColorTempSliderView(value: $tempValue, index: $tempIndex)
  }
}

With these components integrated, users can adjust the color temperature by adjusting the slider if they want to change from the default temperature, offering them the flexibility to capture images with the desired color tones.

Let’s run the app.

And with that, we are done.

Conclusion

As we conclude we’ve delved into exposure adjustments, fine-tuning color temperature, and the flexibility of switching camera lenses. All of this, seamlessly integrated into SwiftUI, empowers developers of any expertise level to harness the Camera APIs for stunning applications.

Our journey in the world of camera enhancements doesn’t stop here. Future chapters promise more depths to dive into, revealing the intricacies of stabilization modes, frame rates, and resolution settings with the video capturing option. Stay tuned for upcoming updates, as we delve deeper into these advanced functionalities.

Keep an eye out for our forthcoming updates! The exploration continues, and we’re excited to unveil even more enhancements and discoveries!


amisha-i image
Amisha Italiya
iOS developer @canopas | Sharing knowledge of iOS development


amisha-i image
Amisha Italiya
iOS developer @canopas | Sharing knowledge of iOS development


Talk to an expert
get intouch
Our team is happy to answer your questions. Fill out the form and we’ll get back to you as soon as possible
footer
Subscribe Here!
Follow us on
2024 Canopas Software LLP. All rights reserved.