Serial dispatch queues are well suited for managing dependencies between a series of synchronous work items, automatically running them sequentially. But GCD is not well suited for a series of asynchronous work items. And, unfortunately, this is precisely your scenario.
A serial dispatch queue will launch your asynchronous network requests sequentially, but it will not wait for any of those network requests to finish asynchronous before launching the next one. The network requests will end up running in concurrently (which is what you are trying to avoid).
The legacy solution for a series of asynchronous tasks would be an OperationQueue
, where you can wrap the asynchronous task in a custom Operation
subclass. But this subclass will have to perform KVO notifications for isExecuting
, isFinished
, etc., in order to let the operation queue know when one operation finishes and the next one can start. It is also an unnecessarily complicated process. But, if you are interested, see Trying to Understand Asynchronous Operation Subclass.
Sometimes folks would reach for third-party solutions (promises/futures). Or, Combine can handle this well, too. See How To Download Multiple Files Sequentially using NSURLSession downloadTask in Swift for an example of how to constrain concurrency via a number of these technologies. That’s focusing on downloads, but the same basic idea works for posting data, too. And Alamofire has built-in Combine publishers, if that’s the way you wanted to go.
That having been said, the contemporary approach is Swift concurrency, which is written to greatly simply dependencies between tasks that are, themselves, asynchronous. See WWDC 2021’s Meet async/await in Swift, as well as the other videos on that page. And, fortunately, your example is using Alamofire, which has built-in async
-await
renditions of their API, too.
So, personally, I would use Alamofire’s async
-await
renditions of their API (scrupulous avoiding any completion handler based API), and then have a for
-await
-in
loop, iterating through an
AsyncChannel<Message>
which would send the messages.
E.g., here is a MRE of that pattern:
struct ContentView: View {
@StateObject var viewModel = ViewModel()
@State var count = 0
var body: some View {
VStack {
Button("Send") {
Task {
let message = Message(objectID: count, message: "Message \(count)")
count += 1
await viewModel.submit(message)
}
}
}
.padding()
.task {
await viewModel.monitorChannel()
}
}
}
@MainActor
final class ViewModel: ObservableObject {
private let sessionManager = Session()
private let channel = AsyncChannel<Message>()
}
// MARK: - Public interface
extension ViewModel {
/// Start monitoring for messages added to channel
func monitorChannel() async {
for await message in channel {
do {
let result = try await send(message)
print(result)
} catch {
print(error)
}
}
}
/// Submit a message to be posted to server.
///
/// Client calls this when it wants to enqueue a message to be sent.
///
/// - Parameter message: The message to be posted.
func submit(_ message: Message) async {
await channel.send(message)
}
}
// MARK: - Private implementation details
private extension ViewModel {
func send(_ message: Message) async throws -> ServerResponse {
try await sessionManager
.request(
"https://httpbin.org/post",
method: .post,
parameters: message,
encoder: JSONParameterEncoder.default
)
.validate()
.serializingDecodable(ServerResponse.self)
.value
}
}
struct Message: Codable {
let objectID: Int
let message: String
}
struct ServerResponse: Decodable {
let json: Message
}
Don’t get lost in the details there, but focus on the big picture: When the app starts, I start monitoring the AsyncChannel
for new messages to send; and when I want to send a message, I just send
it to that channel, and it will send them out sequentially.
For the sake a completeness, the other solution would be to just set the httpMaximumConnectionsPerHost
to 1
, and that would prevent the underlying URLSession
used by Alamofire from sending more than one request at a time. That is admittedly a fairly narrow solution (limited to just serial network requests to a particular host), where as the above are the more general purpose “how do I perform a series of asynchronous tasks sequentially”, but might be sufficient for your purposes.