I have a video recorder in swift built with AVFoundation. I tried to manually register magnification gestures and zoom in/out the camera. It seems like my calculation for the zoom level is off when the gesture begins.
The zoom in/out is not smooth and quite buggy. If I continuously zoom in-out without releasing then the zoom is added to the camera smoothly (not very smooth but works). However when I begin the gesture the camera either zooms in completely or zooms out, and this is mainly the problem I'm facing.
import SwiftUI
import SwiftUI
import AVKit
import AVFoundation
struct HomeStory: View {
   @StateObject var cameraModel = CameraViewModel()
   @GestureState private var scale: CGFloat = 1.0
   @State private var previousScale: CGFloat = 1.0
   
   var body: some View {
       ZStack(alignment: .bottom) {
           CameraStoryView()
               .environmentObject(cameraModel)
               .clipShape(RoundedRectangle(cornerRadius: 30, style: .continuous))
               .gesture(MagnificationGesture()
                   .updating($scale, body: { (value, state, _) in
                       state = value
                   })
                   .onChanged { value in
                       let delta = value / previousScale
                       cameraModel.zoom(delta)
                       previousScale = value
                   }
               )
       }
   }
}
struct CameraStoryView: View {
   @EnvironmentObject var cameraModel: CameraViewModel
   var body: some View {
       
       GeometryReader { proxy in
           let size = proxy.size
           
           CameraPreview(size: size)
               .environmentObject(cameraModel)
       }
   }
}
struct CameraPreview: UIViewRepresentable {
   @EnvironmentObject var cameraModel : CameraViewModel
   var size: CGSize
   
   func makeUIView(context: Context) ->  UIView {
       let view = UIView()
       
       cameraModel.preview = AVCaptureVideoPreviewLayer(session: cameraModel.session)
       cameraModel.preview.frame.size = size
       
       cameraModel.preview.videoGravity = .resizeAspectFill
       view.layer.addSublayer(cameraModel.preview)
       
       DispatchQueue.global(qos: .userInitiated).async {
           cameraModel.session.startRunning()
       }
       
       return view
   }
   
   func updateUIView(_ uiView: UIView, context: Context) { }
}
class CameraViewModel: NSObject, ObservableObject, AVCaptureFileOutputRecordingDelegate, AVCapturePhotoCaptureDelegate {
   @Published var session = AVCaptureSession()
   @Published var alert = false
   @Published var output = AVCaptureMovieFileOutput()
   @Published var preview: AVCaptureVideoPreviewLayer!
   @Published var isRecording: Bool = false
   @Published var recordedURLs: [URL] = []
   @Published var previewURL: URL?
   @Published var showPreview: Bool = false
   @Published var recordedDuration: CGFloat = 0
   @Published var maxDuration: CGFloat = 20
   
   @Published var capturedImage: UIImage?
   @Published var photoOutput = AVCapturePhotoOutput()
   @Published var flashMode: AVCaptureDevice.FlashMode = .off
   var currentCameraPosition: AVCaptureDevice.Position = .back
   
   func zoom(_ delta: CGFloat) {
       if currentCameraPosition == .back {
           guard let device = AVCaptureDevice.default(for: .video) else { return }
           do {
               try device.lockForConfiguration()
               
               let currentZoomFactor = device.videoZoomFactor
               var newZoomFactor = currentZoomFactor * delta
               
               newZoomFactor = max(1.0, min(newZoomFactor, 3.0))
               
               device.videoZoomFactor = newZoomFactor
               device.unlockForConfiguration()
           } catch {
               print("Error zooming camera: \(error.localizedDescription)")
           }
       } else {
           guard let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) else { return }
           do {
               try device.lockForConfiguration()
               
               let currentZoomFactor = device.videoZoomFactor
               var newZoomFactor = currentZoomFactor * delta
               
               newZoomFactor = max(1.0, min(newZoomFactor, 3.0))
               
               device.videoZoomFactor = newZoomFactor
               device.unlockForConfiguration()
           } catch {
               print("Error zooming camera: \(error.localizedDescription)")
           }
       }
   }
   
   func flipCamera() {
       // Create a discovery session to find all available video devices
       let discoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: .unspecified)
       // Get all available video devices
       let videoDevices = discoverySession.devices
       // Check if there is more than one video device
       guard videoDevices.count > 1 else {
           return // If not, return early
       }
       // Get the current input
       guard let currentVideoInput = session.inputs.first as? AVCaptureDeviceInput else {
           return
       }
       // Get the new camera position
       let newCameraPosition: AVCaptureDevice.Position = (currentCameraPosition == .back) ? .front : .back
       // Find the new camera device
       if let newCamera = videoDevices.first(where: { $0.position == newCameraPosition }) {
           // Create a new video input
           do {
               let newVideoInput = try AVCaptureDeviceInput(device: newCamera)
               // Remove the current input
               session.removeInput(currentVideoInput)
               // Add the new input
               if session.canAddInput(newVideoInput) {
                   session.addInput(newVideoInput)
                   currentCameraPosition = newCameraPosition
               } else {
                   // Handle the case where adding the new input fails
                   print("Failed to add new camera input")
               }
           } catch {
               // Handle any errors that occur while creating the new input
               print("Error creating new camera input: \(error.localizedDescription)")
           }
       }
   }
   
   func takePhoto() {
       let photoSettings = AVCapturePhotoSettings()
       self.photoOutput.capturePhoto(with: photoSettings, delegate: self)
   }
   
   func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
       if let error = error {
           print("Error capturing photo: \(error.localizedDescription)")
           return
       }
       if let imageData = photo.fileDataRepresentation(), let capturedImage = UIImage(data: imageData) {
           self.capturedImage = capturedImage
       }
   }
   
   func checkPermission(){
       
       switch AVCaptureDevice.authorizationStatus(for: .video) {
       case .authorized:
           setUp()
           return
       case .notDetermined:
           AVCaptureDevice.requestAccess(for: .video) { (status) in
               
               if status{
                   self.setUp()
               }
           }
       case .denied:
           self.alert.toggle()
           return
       default:
           return
       }
   }
   
   func setUp(){
       
       do{
           self.session.beginConfiguration()
           let cameraDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back)
           let videoInput = try AVCaptureDeviceInput(device: cameraDevice!)
           let audioDevice = AVCaptureDevice.default(for: .audio)
           let audioInput = try AVCaptureDeviceInput(device: audioDevice!)
           
           // MARK: Audio Input
           
           if self.session.canAddInput(videoInput) && self.session.canAddInput(audioInput){
               self.session.addInput(videoInput)
               self.session.addInput(audioInput)
           }
           if self.session.canAddOutput(self.output){
               self.session.addOutput(self.output)
           }
           
           if self.session.canAddOutput(self.photoOutput) {
               self.session.addOutput(self.photoOutput)
           }
           
           self.session.commitConfiguration()
       }
       catch{
           print(error.localizedDescription)
       }
   }
   
   func startRecording(){
       // MARK: Temporary URL for recording Video
       let tempURL = NSTemporaryDirectory() + "\(Date()).mov"
       output.startRecording(to: URL(fileURLWithPath: tempURL), recordingDelegate: self)
       isRecording = true
   }
   
   func stopRecording(){
       output.stopRecording()
       isRecording = false
   }
   
   func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {
       if let error = error {
           print(error.localizedDescription)
           return
       }
       
       // CREATED SUCCESSFULLY
       print(outputFileURL)
       self.recordedURLs.append(outputFileURL)
       if self.recordedURLs.count == 1{
           self.previewURL = outputFileURL
           return
       }
       
       // CONVERTING URLs TO ASSETS
       let assets = recordedURLs.compactMap { url -> AVURLAsset in
           return AVURLAsset(url: url)
       }
       
       self.previewURL = nil
       // MERGING VIDEOS
       Task {
           await mergeVideos(assets: assets) { exporter in
               exporter.exportAsynchronously {
                   if exporter.status == .failed{
                       // HANDLE ERROR
                       print(exporter.error!)
                   }
                   else{
                       if let finalURL = exporter.outputURL{
                           print(finalURL)
                           DispatchQueue.main.async {
                               self.previewURL = finalURL
                           }
                       }
                   }
               }
           }
       }
   }
   
   func mergeVideos(assets: [AVURLAsset],completion: @escaping (_ exporter: AVAssetExportSession)->()) async {
       
       let compostion = AVMutableComposition()
       var lastTime: CMTime = .zero
       
       guard let videoTrack = compostion.addMutableTrack(withMediaType: .video, preferredTrackID: Int32(kCMPersistentTrackID_Invalid)) else{return}
       guard let audioTrack = compostion.addMutableTrack(withMediaType: .audio, preferredTrackID: Int32(kCMPersistentTrackID_Invalid)) else{return}
       
       for asset in assets {
           // Linking Audio and Video
           do {
               try await videoTrack.insertTimeRange(CMTimeRange(start: .zero, duration: asset.load(.duration)), of: asset.loadTracks(withMediaType: .video)[0], at: lastTime)
               // Safe Check if Video has Audio
               if try await !asset.loadTracks(withMediaType: .audio).isEmpty {
                   try await audioTrack.insertTimeRange(CMTimeRange(start: .zero, duration: asset.load(.duration)), of: asset.loadTracks(withMediaType: .audio)[0], at: lastTime)
               }
           }
           catch {
               print(error.localizedDescription)
           }
           
           // Updating Last Time
           do {
               lastTime = try await CMTimeAdd(lastTime, asset.load(.duration))
           } catch {
               print(error.localizedDescription)
           }
       }
       
       // MARK: Temp Output URL
       let tempURL = URL(fileURLWithPath: NSTemporaryDirectory() + "Reel-\(Date()).mp4")
       
       // VIDEO IS ROTATED
       // BRINGING BACK TO ORIGNINAL TRANSFORM
       
       let layerInstructions = AVMutableVideoCompositionLayerInstruction(assetTrack: videoTrack)
       
       // MARK: Transform
       var transform = CGAffineTransform.identity
       transform = transform.rotated(by: 90 * (.pi / 180))
       transform = transform.translatedBy(x: 0, y: -videoTrack.naturalSize.height)
       layerInstructions.setTransform(transform, at: .zero)
       
       let instructions = AVMutableVideoCompositionInstruction()
       instructions.timeRange = CMTimeRange(start: .zero, duration: lastTime)
       instructions.layerInstructions = [layerInstructions]
       
       let videoComposition = AVMutableVideoComposition()
       videoComposition.renderSize = CGSize(width: videoTrack.naturalSize.height, height: videoTrack.naturalSize.width)
       videoComposition.instructions = [instructions]
       videoComposition.frameDuration = CMTimeMake(value: 1, timescale: 30)
       
       guard let exporter = AVAssetExportSession(asset: compostion, presetName: AVAssetExportPresetHighestQuality) else{return}
       exporter.outputFileType = .mp4
       exporter.outputURL = tempURL
       exporter.videoComposition = videoComposition
       completion(exporter)
   }
}
				
                        
Try and adjust your
zoom(_ delta: CGFloat)method to make sure the zoom level changes smoothly.That would consolidate the device selection for front and back cameras. And you can use
device.activeFormat.videoMaxZoomFactorto determine the maximum zoom level, which provides device-specific zoom limits.In your
HomeStoryview, you would need to update the gesture handling to reset thepreviousScalewhen the gesture ends:That should make sure each new gesture starts with a fresh scale.
For another approach, see also "Pinch to zoom camera" (the 2022 answer, more for UIKit than SwiftUI, but with some ideas).