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.
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:
Array
for these user interactions;append
values to that array;removeFirst
a value from that array; andE.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.