Adding Swift Concurrency capabilities to 3rd party projects
Example: introducing a new function to FeedKit allowing us to fetch arbitrary number of feeds in parallel using async/await
Is Swift Concurrency ready to adopt? Yes, because
Xcode 13.2 brings support for Swift Concurrency in applications that deploy to macOS Catalina 10.15, iOS 13, tvOS 13, and watchOS 6 or newer. This support includes async/await, actors, global actors, structured concurrency, and the task APIs.
However, your project dependencies might not (yet) support async/await.
No problem, I will show you how to to apply the modern Swift Concurrency in such a scenario.
As an example, I use FeedKit.
It's an RSS, Atom and JSON Feed parser written in Swift.
It supports asynchronous processing by passing the results, once available, back to the caller through a completion handler.
// Build a URL pointing to an RSS, Atom or JSON Feed.
let feedURL = URL(string: "http://images.apple.com/main/rss/hotnews/hotnews.rss")!
// Get an instance of `FeedParser`
let parser = FeedParser(URL: feedURL)
// Parse asynchronously, not to block the UI.
parser.parseAsync(queue: DispatchQueue.global(qos: .userInitiated)) { (result) in
// Do your thing, then back to the Main thread
DispatchQueue.main.async {
// ..and update the UI
}
}
FeedKit is a wonderful framework and works like a charm but it does not support async/await.
So let's add an async function through a Swift extension π
import FeedKit
extension FeedParser {
func asyncParse() async -> Result<Feed, ParserError> {
await withCheckedContinuation { continuation in
self.parseAsync(queue: DispatchQueue(label: "my.concurrent.queue", attributes: .concurrent)) { result in
continuation.resume(returning: result)
}
}
}
The implementation uses a CheckedContinuation.
But how to create an async function that can fetch a dynamic number of feeds?
func feeds(for feedURLs: [URL]) async -> [Feed] {
let taskResult = await withTaskGroup(of: Optional<Feed>.self, returning: [Feed].self) { group in
for feedURL in feedURLs {
group.addTask {
let parser = FeedParser(URL: feedURL)
let result = await parser.asyncParse()
switch result {
case let .success(feed):
return (feed)
case .failure(_):
return (nil)
}
}
}
var result: [Feed] = []
for await feedResult in group {
if let feed = feedResult {
result.append(feed)
}
}
return result
}
return taskResult
}
This function can run work in parallel thanks to Swift concurrency's TaskGroup:
Done!
Here is a simplified example usable in a SwiftUI-based iOS application.
import FeedKit
import SwiftUI
struct ContentView: View {
@State var parsedCount: Int = 0
var body: some View {
Text("Parsed Feeds: \(parsedCount)")
.task { // perform when this view appears
let feeds = await self.feeds(for: [
URL(string: "http://images.apple.com/main/rss/hotnews/hotnews.rss")!,
URL(string: "https://blog.eidinger.info/rss.xml")!
])
parsedCount = feeds.count
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
extension ContentView {
func feeds(for feedURLs: [URL]) async -> [Feed] {
let taskResult = await withTaskGroup(of: Optional<Feed>.self, returning: [Feed].self) { group in
for feedURL in feedURLs {
group.addTask {
let parser = FeedParser(URL: feedURL)
let result = await parser.asyncParse()
switch result {
case let .success(feed):
return (feed)
case .failure(_):
return (nil)
}
}
}
var result: [Feed] = []
for await feedResult in group {
if let feed = feedResult {
result.append(feed)
}
}
return result
}
return taskResult
}
}
extension FeedParser {
func asyncParse() async -> Result<Feed, ParserError> {
await withCheckedContinuation { continuation in
self.parseAsync(queue: DispatchQueue(label: "FeedKit.concurrent.queue", attributes: .concurrent)) { result in
continuation.resume(returning: result)
}
}
}
}