Making video demos: Showing gestures in your iOS app Last Updated 03 August 2022 by Michael Forrest

If you want to avoid adding motion graphics for every tap and swipe in your screen recording during post-production, you should add a feature into your app that displays touches when you record.

First we’ll add a window on top of everything else in your app to display touches. Create a UIWindow, set its windowLevel to .normal + 1 and attach a rootViewController.

Add this code to your sceneDelegate.

if UserDefaults.standard.bool(forKey: "show_touches"){
    let overlay = UIWindow(windowScene: windowScene)
    let touchesController = TouchesViewController()
    overlay.rootViewController = touchesController
    overlay.windowLevel = .normal + 1
    overlay.isHidden = false
    overlay.isUserInteractionEnabled = false
    touchesController.touchesView.listenForTouches(in: window)
    self.overlay = overlay
}

Add the preference into your Settings.plist so it’s accessible under your app in the Settings app.

Next we need our TouchesViewController

class TouchesViewController: UIViewController{
    var touchesView = TouchesView()
    override func viewDidLoad() {
        super.viewDidLoad()
        touchesView.add(to: view)
    }
}

TouchesView attaches pan and tap gestures to our window, and implements gestureRecognizer(:shouldRecognizeSimultaneouslyWith:) to always return true so this doesn’t interfere with anything other controls in our app.

class TouchesView: UIView, UIGestureRecognizerDelegate {
    
    fileprivate var fingers = [UIGestureRecognizer: FingerView]()
    
    // load a 'tap' sound (requires a tap.caf file to be included in the app bundle)
    // or comment out this line and the lines where tapSound.play() is called.
    let tapSound = try! AVAudioPlayer(contentsOf: Bundle.main.url(forResource: "tap", withExtension: "caf")!)

    // pin edge-to-edge
    func add(to view: UIView){
        view.addSubview(self)
        isUserInteractionEnabled = false
        translatesAutoresizingMaskIntoConstraints = false
        leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
        trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
        topAnchor.constraint(equalTo: view.topAnchor).isActive = true
        bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
        
        tapSound.prepareToPlay()
    }
    // called from outside after window has been added 
    func listenForTouches(in view: UIView){
        let tap = UITapGestureRecognizer(target: self, action: #selector(handle(tap:)))
        tap.cancelsTouchesInView = false
        tap.delaysTouchesBegan = false
        tap.delaysTouchesEnded = false
        tap.delegate = self
        view.addGestureRecognizer(tap)

        
        let pan = UIPanGestureRecognizer(target: self, action: #selector(handle(pan:)))
        pan.cancelsTouchesInView = false
        pan.delaysTouchesBegan = false
        pan.delaysTouchesEnded = false
        pan.delegate = self
        view.addGestureRecognizer(pan)
    }
    @objc func handle(tap: UITapGestureRecognizer){
        let position = tap.location(in: self)
        if tap.state == .recognized{
            let finger = FingerView(frame: .zero)
            addSubview(finger)
            finger.frame.origin = position
            finger.hide(delay: 0.1, duration: 0.5)
            tapSound.play()
        }
    }
    
    @objc func handle(pan: UIPanGestureRecognizer){
        if pan.state == .ended || pan.state == .cancelled{
            return
        }
        let position = pan.location(in: self)
        let finger = FingerView(frame: .zero)
        addSubview(finger)
        finger.frame.origin = position
        finger.hide(delay: 0.1, duration: 0.5)
        UIView.animate(withDuration: 0.3) {
            finger.alpha = 0
        }
        
    }
    // don't interfere with any other interactions
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        true
    }
}

Finally, we’ll create our FingerView. Mine programatically draws a set of concentric rings to fit with my app’s design. You could create your own design or subclass UIImageView with an image asset.

The hide() function is called after each view is added in TouchesView’s handle(pan:) and handle(tap:). Adjust the delay and duration parameters at the call site to taste.

class FingerView: UIView{
    let rings:[UIView] = stride(from: 2, to: 41, by: 8).map { (radius:CGFloat) in
        let ring = UIView(frame: CGRect(x: -radius, y: -radius, width: radius * 2, height: radius * 2))
        ring.layer.borderColor = (radius == 2 ? UIColor.macBlue : UIColor.macSecondary).cgColor
        ring.layer.borderWidth = 2
        ring.layer.cornerRadius = radius
        return ring
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        for ring in rings {
            addSubview(ring)
        }
        
    }
    func hide(delay: TimeInterval, duration: TimeInterval, completion: (()->Void)? = nil){
        let fadeDuration = (duration * 0.7 / TimeInterval(rings.count))
        let staggerInterval = fadeDuration * 0.7
        for (index,ring) in rings.enumerated() {
            UIView.animate(withDuration: fadeDuration, delay: TimeInterval(index) * staggerInterval, options: [], animations: {
                ring.alpha = 0
            })
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + duration + delay) {
            self.removeFromSuperview()
            completion?()
        }
    }
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

Now you can run your app, go to Settings on your device and enable “Show Touches”

I have added the screen recording button to Control Center on my phone to quickly initiate screen recordings.

Here’s what it looks like in context!

jump to about 2:00 for app demos

Hope that helps! Feel free to join my mailing list or follow me on Twitter.

Join Squares TV

Sign in

For exclusive access to tools and guides, behind-the-scenes content and email updates.

Sign in with Twitch Sign in with Google Sign in with Facebook

Or register using email: