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!
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.
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.
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.
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.
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.
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!