Adding Swift Concurrency capabilities to 3rd party projects

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

Β·

3 min read

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)
            }
        }
    }
}

Did you find this article valuable?

Support Marco Eidinger by becoming a sponsor. Any amount is appreciated!