Search code examples
iosswiftmultithreadingqueuegrand-central-dispatch

Can DispatchQueue be used to queue user inputs?


In my game, each time a user swipes in a particular direction something on screen animates. Since each animation takes about a second, if the user rapidly swipes on the screen I need a way to "log" all the functions the swipes would trigger and have them execute serially as they keep completing. I thought a Queue would be perfect for this and before implementing my own Queue I encountered DispatchQueue, DispatchGroup, and DispatchWorkItem. It got me wondering if there's a way to use DispatchQueue in conjunction with Groups and Work Items to queue up functions and have them execute one by one as they complete.

Please let me know if DispatchQueue can be used to queue up functions in this way.

I already know about the .userInteractivity global queue and am assuming something needs to be done over there; I just don't know what nor how.

Thanks.


Solution

  • As appealing as it might seem, DispatchQueue is not an ideal tool for your use-case.

    Dispatch queues are a very specific type of queue, a queue of code blocks to be executed in FIFO manner. Unfortunately, dispatch queues are not terribly elegant at managing dependencies between work items that are, themselves, asynchronous. And needless to say, your animations are asynchronous.

    Admittedly, there are (kludgy) techniques to make this work with GCD, but it is not the most natural solution for your use-case. Operation queues handle dependencies between asynchronous tasks a bit better, but are unnecessarily complicated (one would need to create custom asynchronous Operation subclass with all the appropriate KVO notifications for isExecuting, isFinished, etc.).

    One very simple solution, as you suggested, would be to have a your own FIFO queue of some model object that captures the user interaction:

    • Have an Array for these user interactions;
    • On user interaction, append values to that array;
    • When starting an animation, removeFirst a value from that array; and
    • When finishing an animation, recursively call itself, starting the next animation (if any).

    E.g., below, I have a queue/array of points representing where the user tapped in the UI, and animate the position of a view in a FIFO manner. (To keep the code snippet simple, I created both the animated view and tap gesture recognizer in IB.)

    import UIKit
    
    class SimpleViewController: UIViewController {
        private var points: [CGPoint] = []
        private var isRunning = false
    
        @IBOutlet weak var animatedView: UIView!
    
        @IBAction func handleTap(_ gesture: UITapGestureRecognizer) {
            let point = gesture.location(in: view)
            points.append(point)
            moveNext()
        }
    
        func moveNext() {
            guard !points.isEmpty, !isRunning else {
                return
            }
    
            isRunning = true
            let point = points.removeFirst()
    
            UIView.animate(withDuration: 1) { [self] in
                animatedView.center = point
            } completion: { [self] isFinished in
                isRunning = false
                if isFinished { moveNext() }
            }
        }
    }
    

    The details here are not terribly relevant. The basic idea is that one can use an Array as a FIFO queue of user input.


    If you have your heart set on using some framework object to manage your queue, you could consider the following, skipping dispatch/operation queue alternatives entirely.

    • Instead, I would jump directly into Swift concurrency, with async-await, which gracefully handles dependencies between asynchronous tasks. See WWDC 2021 video Meet async/await in Swift.

    • Within Swift concurrency, I would use the “asynchronous sequence” pattern to manage a series of asynchronous tasks. See WWDC 2021 video Meet AsyncSequence.

    • And to be more specific, I would use a particular type of AsyncSequence, namely a AsyncChannel (from the Swift Async Algorithms library) to provide an asynchronous sequence in which your user input can easily add new items to that existing sequence.

    import UIKit
    import AsyncAlgorithms                            // https://github.com/apple/swift-async-algorithms
    
    class ChannelViewController: UIViewController {
        private let pointChannel = AsyncChannel<CGPoint>()
        private var channelTask: Task<Void, Never>?
    
        @IBOutlet weak var animatedView: UIView!
    
        override func viewDidAppear(_ animated: Bool) {
            super.viewDidAppear(animated)
            startChannel()
        }
    
        override func viewDidDisappear(_ animated: Bool) {
            super.viewDidDisappear(animated)
            stopChannel()
        }
    
        @IBAction func handleTap(_ gesture: UITapGestureRecognizer) {
            let point = gesture.location(in: view)
            Task {
                await pointChannel.send(point)
            }
        }
    
        func startChannel() {
            // in case we ever accidentally call this twice
            stopChannel()
    
            // create task …
            channelTask = Task {
                // … that iterates through `CGPoint` sent on the channel
                for await point in pointChannel {
                    await move(to: point)
                }
            }
        }
    
        func stopChannel() {
            channelTask?.cancel()
            channelTask = nil
        }
    
        func move(to point: CGPoint) async {
            await withCheckedContinuation { continuation in
                UIView.animate(withDuration: 1) { [self] in
                    animatedView.center = point
                } completion: { _ in
                    continuation.resume()
                }
            }
        }
    }
    

    If you are unfamiliar with Swift concurrency, you might want get familiar with that before delving into this AsyncChannel approach. But if you are looking for the most logical pattern for processing a sequence of asynchronous tasks, then AsyncChannel is worth consideration.