Highlight
Swift Async Algorithms is an open source package that provides advanced algorithms such as Zip, Merge, Debounce, and Chunk for AsyncSequence. It also uses the Clock API of Swift 5.7 to implement time-based flow control.
Core Content
From callback to AsyncSequence
In your instant messaging app, the arrival of messages is currently driven by callbacks. Each account has its own message callback, and manual coordination is required when there are multiple accounts. The code is full of nested completion handlers, and state management becomes increasingly complex.
(01:15) Swift’s AsyncSequence turns asynchronous value streaming intofor-await-inThe sequence to traverse. The Swift Async Algorithms package provides more advanced algorithms on this basis.
struct Account {
var messages: AsyncStream<Message>
}
actor AccountManager {
var primaryAccount: Account
var secondaryAccount: Account?
}
protocol MessagePreview {
func displayPreviews(_ manager: AccountManager) async
}
Key points:
AsyncStreamConvert callbacks to AsyncSequence, preserving message order -for-await-inSyntax makes asynchronous traversal look like synchronous code
Zip: Concurrently wait for multiple asynchronous results
(02:16) When users send videos, you need to generate previews and transcode at the same time. The two operations are performed independently, but they need to appear in pairs when uploading.
The previous approach was to useTaskGrouporasync letManual coordination. The Zip algorithm directly solves this problem:
// upload attachments of videos and previews such that every video has a preview
// that are created concurrently so that neither blocks each other.
for try await (vid, preview) in zip(videos, previews) {
try await upload(vid, preview)
}
Key points:
zipConcurrently traverse two AsyncSequences to generate tuples- If either party completes first, it will wait for the other party and will not block.
- If any sequence throws an error, the entire Zip will be rethrow
Merge: Merge multiple message flows
(04:04) Your App supports multiple accounts, and each account has its own message flow. Users want to see all messages in one interface, no matter which account they come from.
The Merge algorithm combines multiple AsyncSequences of the same type into one:
// Display previews of messages from either the primary or secondary account
for try await message in merge(primaryAccount.messages, secondaryAccount.messages) {
displayPreview(message)
}
Key points:
mergeRequires all input sequences to have elements of the same type- The output order depends on which sequence produces the value first
- When an error occurs in any sequence, iterations of other sequences will be cancelled.
Clock API: Working with time
(05:43) Dealing with time in asynchronous code has always been a hassle.DispatchQueue.asyncAfterNot flexible enough,TimerRequires manual management of lifecycle.
Swift 5.7 introduces three time primitives: Clock, Instant, and Duration:
// Sleep until a given deadline
let clock = SuspendingClock()
var deadline = clock.now + .seconds(3)
try await clock.sleep(until: deadline)
Key points:
ClockThe protocol definesnowandsleep(until:)two primitives -Durationsupport.seconds()、.milliseconds()etc. construction method
The two built-in Clocks have different behaviors:
let clock = SuspendingClock()
let elapsed = await clock.measure {
await someLongRunningWork()
}
// Elapsed time reads 00:05.40
let clock = ContinuousClock()
let elapsed = await clock.measure {
await someLongRunningWork()
}
// Elapsed time reads 00:19.54
(06:56)SuspendingClockPause timing when the machine is sleeping,ContinuousClockKeep running like a stopwatch. If your job is suspended for 14 seconds, SuspendingClock only counts 5.4 seconds and ContinuousClock counts 19.54 seconds.
Key points:
- Used for animation and device-related time logic
SuspendingClock- For human time measurementContinuousClock
Debounce: Search input debounce
(08:27) When users type quickly in the search box, you don’t want every character change to trigger a search request.
The Debounce algorithm waits for a silent period before emitting a value:
class SearchController {
let searchResults = AsyncChannel<SearchResult>()
func search<SearchValues: AsyncSequence>(_ searchValues: SearchValues)
where SearchValues.Element == String
{
let queries = searchValues
.debounce(for: .milliseconds(300))
for await query in queries {
let results = try await performSearch(query)
await channel.send(results)
}
}
}
Key points:
debounce(for:)Used by defaultContinuousClock- When events are pouring in quickly, the timer will be reset with each new event- The last value will only be emitted if there are no new events for 300 milliseconds
Chunk: Send messages in batches
(09:46) When users type quickly, instead of sending separate requests for each message, it is better to save a batch and send them together.
The Chunked algorithm divides sequences into chunks by time or quantity:
let batches = outboundMessages.chunked(
by: .repeating(every: .milliseconds(500))
)
let encoder = JSONEncoder()
for await batch in batches {
let data = try encoder.encode(batch)
try await postToServer(data)
}
Key points:
chunked(by:)Supports chunking by time, quantity, and content- Errors will be rethrowed, keeping code safe
- 500 millisecond batch, which not only reduces the number of requests but also prevents users from waiting too long
Return to collection from AsyncSequence
(10:47) Sometimes you need to convert an AsyncSequence back to a normal collection. Swift Async Algorithms provides initializers:
// Create a message with awaiting attachments to be encoded
init<Attachments: AsyncSequence>(_ attachments: Attachments) async rethrows {
self.attachments = try await Array(attachments)
}
Key points:
Array(asyncSequence)、Set(asyncSequence)、Dictionary(asyncSequence)All supported- only works with limited known AsyncSequences
- Allows you to migrate your code step by step instead of changing it all to asynchronous at once
Detailed Content
Concurrency implementation details of Zip
Zip is not a simple sequential traversal. It internally waits concurrently for the next value of each input sequence:
// Pseudocode showing Zip's concurrency behavior
let videoStream = transcodeVideos() // AsyncSequence<Video>
let previewStream = generatePreviews() // AsyncSequence<Preview>
for try await (video, preview) in zip(videoStream, previewStream) {
// video and preview come from the same index position
// If video is ready first, it waits for preview, and vice versa
try await upload(video, preview)
}
Key points:
- Zip internally uses structured concurrency to read multiple sequences simultaneously
- When any sequence ends, Zip also ends (will not wait for remaining elements of other sequences)
- Error propagation is immediate and does not wait for other sequences of current operations to complete
Choice of Merge and Zip
// Zip: values must be paired, and counts must match
for try await (a, b) in zip(streamA, streamB) {
// a and b are the Nth elements
}
// Merge: use whichever produces a value first, and counts can differ
for try await value in merge(streamA, streamB) {
// value comes from streamA or streamB, depending on which produces first
}
(05:08) Merge requires that the element types of all sequences are the same, and the output is a single element stream. Zip produces tuples, retaining element type information for each sequence.
Clock’s measure method
let clock = ContinuousClock()
let duration = await clock.measure {
// Work to measure
await fetchData()
await processData()
}
print("Elapsed time: \(duration)")
Key points:
measureAutomatically record start and end times- returned
DurationCan be printed or compared directly -Exceptions in closures will propagate upward
Core Takeaways
1. Replace callback-driven messaging system with AsyncStream
Migrate existing delegate/callback messaging system to AsyncStream. Each message source becomes an AsyncSequence, and the consumer usesfor-await-inTraverse. Merge is used for multi-source merging, and Zip is used for pairing operations.
Entrance:AsyncStream、AsyncThrowingStream
2. Replace Timer and GCD delay with Debounce + Clock
All useTimer.scheduledTimerorDispatchQueue.asyncAfterThe implemented anti-shake and throttling logic can be replaced by Debounce/Throttle of Swift Async Algorithms. The code is more concise and the life cycle is automatically managed.
Entrance:searchQuery.debounce(for: .milliseconds(300))
3. Use Chunk to implement message batch processing
For scenarios such as chat apps, log uploads, and data analysis, high-frequency small requests can be converted into batch requests. The Chunked algorithm supports two strategies: time window and quantity, which is much simpler than manual buffer maintenance.
Entrance:messages.chunked(by: .repeating(every: .seconds(1)))
4. Choose the right Clock
Check all time-related logic in the project and distinguish between “device-oriented” and “human-oriented” scenarios. SuspendingClock is used for animation and timeout detection; ContinuousClock is used for user waiting time and billing logic.
Entrance:SuspendingClock()、ContinuousClock()
Related Sessions
- What’s new in Swift — Overview of new features in Swift 5.7, including the introduction background of the Clock API
- Meet distributed actors in Swift — Combining distributed actors with AsyncSequence to build a real-time system
- Design protocol interfaces in Swift — Swift 5.7 generics improvements, AsyncSequence benefits from
some/anygrammar
Comments
GitHub Issues · utterances