Search code examples
animationswiftuiavfoundationactor

Calling into AVFoundation on a background thread interferes with SwiftUI animation


I have an app with an animation stutter that I've distilled down to the following reproduction:

struct ContentView: View {
    var body: some View {
        ZStack {
            AnimatingCircle()
            Button("Trigger animation stutter") {
                Task {
                    await MyActor().go()
                }
            }
        }
    }
}

private struct AnimatingCircle: View {
    @State var shouldAnimate = false
    var body: some View {
        Circle()
            .fill(.red)
            .animation(.easeInOut.repeatForever(autoreverses: true)) {
                $0.scaleEffect(shouldAnimate ? 1.5 : 0.5)
            }
            .onAppear {
                shouldAnimate = true
            }
    }
}

final actor MyActor {
    let captureSession = AVCaptureSession()

    func go() {
        let microphone = AVCaptureDevice.DiscoverySession(
            deviceTypes: [.microphone],
            mediaType: .audio,
            position: .unspecified
        ).devices.first!
        captureSession.addInput(try! AVCaptureDeviceInput(device: microphone))
        captureSession.addOutput(AVCaptureAudioDataOutput())
        captureSession.startRunning()
    }
}

When the button is tapped, there is a very noticeable stutter in the circle animation. I have verified that all the AVFoundation calls I make are on a background thread. I'm at a loss for how to work around this.

First, how is it possible that the AVFoundation code is interfering with the animation? Does it imply that something in the workings of AVFoundation is dispatching back to the main thread?

Second, any ideas on working around this?

Tap here to see the animation result (the animation is annoying):

Update 1

I appreciate the scrutiny of my use of Tasks and Actors, but that is not the cause. The same bug is exhibited with this code:

Button("Trigger animation stutter") {
    DispatchQueue.global().async {
        go()
    }
}

func go() {
    let captureSession = AVCaptureSession()
    let microphone = AVCaptureDevice.DiscoverySession(
        deviceTypes: [.microphone],
        mediaType: .audio,
        position: .unspecified
    ).devices.first!
    captureSession.addInput(try! AVCaptureDeviceInput(device: microphone))
    captureSession.addOutput(AVCaptureAudioDataOutput())
    captureSession.startRunning()
}

Solution

  • Wow, it has something to do with the debugger. I did not see that one coming.

    If I turn off 'debug executable', the stutter is completely gone. This is good news in that customers of the live app will not encounter the degraded experience.

    Turn off 'Debug executable'

    I spent so much time on this. I modified my audio code to follow the guide Capturing stereo audio from built-in microphones hoping that the bug was in AVCaptureSession and not present in AVAudioSession. Same exact result.

    So if you are building with AVCaptureSession or AVAudioSession and you experience apparent main thread blocking, try unchecking this box before you spend a bunch of time refactoring.