Search code examples
iosswiftavfoundationzoominguipangesturerecognizer

Pan gesture (hold/drag) zoom on camera like Snapchat


I am trying to replicate Snapchat camera's zoom feature where once you have started recording you can drag your finger up or down and it will zoom in or out accordingly. I have been successful with zooming on pinch but have been stuck on zooming with the PanGestureRecognizer.

Here is the code I've tried the problem is that I do not know how to replace the sender.scale that I use for pinch gesture recognizer zooming. I'm using AVFoundation. Basically, I'm asking how I can do the hold zoom (one finger drag) like in TikTok or Snapchat properly.

let minimumZoom: CGFloat = 1.0
let maximumZoom: CGFloat = 15.0
var lastZoomFactor: CGFloat = 1.0
var latestDirection: Int = 0

@objc func panGesture(_ sender: UIPanGestureRecognizer) {

    let velocity = sender.velocity(in: doubleTapSwitchCamButton)
    var currentDirection: Int = 0

    if velocity.y > 0 || velocity.y < 0 {

                let originalCapSession = captureSession
                var devitce : AVCaptureDevice!

                let videoDeviceDiscoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera, .builtInDuoCamera], mediaType: AVMediaType.video, position: .unspecified)
                let devices = videoDeviceDiscoverySession.devices
                devitce = devices.first!

                guard let device = devitce else { return }

                    // Return zoom value between the minimum and maximum zoom values
                    func minMaxZoom(_ factor: CGFloat) -> CGFloat {
                        return min(min(max(factor, minimumZoom), maximumZoom), device.activeFormat.videoMaxZoomFactor)
                        }

                        func update(scale factor: CGFloat) {
                            do {

                                try device.lockForConfiguration()
                                defer { device.unlockForConfiguration() }
                                device.videoZoomFactor = factor
                            } catch {
                                print("\(error.localizedDescription)")
                            }
                        }

//These 2 lines below are the problematic ones, pinch zoom uses this one below, and the newScaleFactor below that is a testing one that did not work.
                let newScaleFactor = minMaxZoom(sender.scale * lastZoomFactor)
                       //let newScaleFactor = CGFloat(exactly: number + lastZoomFactor)


                    switch sender.state {

                    case .began: fallthrough                
                    case .changed: update(scale: newScaleFactor!)                
                    case .ended:
                        lastZoomFactor = minMaxZoom(newScaleFactor!)
                        update(scale: lastZoomFactor)

                    default: break
                    }

        } else {

        }

        latestDirection = currentDirection

    }

Solution

  • You can use the gesture recogniser's translation property, to calculate the displacement in points, and normalise this displacement as a zoom factor.

    Fitting this into your code, you could try:

     ... somewhere in your view setup code, i.e. viewDidLoad.... 
         let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(panGesture))
         button.addGestureRecognizer(panGestureRecognizer)
    
     private var initialZoom: CGFloat = 1.0
     @objc func panGesture(_ sender: UIPanGestureRecognizer) {
    
        // note that 'view' here is the overall video preview
        let velocity = sender.velocity(in: view)
    
        if velocity.y > 0 || velocity.y < 0 {
    
           let originalCapSession = captureSession
           var devitce : AVCaptureDevice!
    
           let videoDeviceDiscoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera, .builtInDuoCamera], mediaType: AVMediaType.video, position: .unspecified)
           let devices = videoDeviceDiscoverySession.devices
           devitce = devices.first!
    
           guard let device = devitce else { return }
    
            let minimumZoomFactor: CGFloat = 1.0
            let maximumZoomFactor: CGFloat = min(device.activeFormat.videoMaxZoomFactor, 10.0) // artificially set a max useable zoom of 10x
    
            // clamp a zoom factor between minimumZoom and maximumZoom
            func clampZoomFactor(_ factor: CGFloat) -> CGFloat {
                return min(max(factor, minimumZoomFactor), maximumZoomFactor)
            }
    
            func update(scale factor: CGFloat) {
                do {
    
                    try device.lockForConfiguration()
                    defer { device.unlockForConfiguration() }
                    device.videoZoomFactor = factor
                } catch {
                    print("\(error.localizedDescription)")
                }
            }
    
            switch sender.state {
    
            case .began:
                initialZoom = device.videoZoomFactor
                startRecording() /// call to start recording your video
    
            case .changed:
    
                // distance in points for the full zoom range (e.g. min to max), could be view.frame.height
                let fullRangeDistancePoints: CGFloat = 300.0
    
                // extract current distance travelled, from gesture start
                let currentYTranslation: CGFloat = sender.translation(in: view).y
    
                // calculate a normalized zoom factor between [-1,1], where up is positive (ie zooming in)
                let normalizedZoomFactor = -1 * max(-1,min(1,currentYTranslation / fullRangeDistancePoints))
    
                // calculate effective zoom scale to use
                let newZoomFactor = clampZoomFactor(initialZoom + normalizedZoomFactor * (maximumZoomFactor - minimumZoomFactor))
    
                // update device's zoom factor'
                update(scale: newZoomFactor)
    
            case .ended, .cancelled:
                stopRecording() /// call to start recording your video
                break
    
            default:
                break
            }
        }
    }