Skip to main content

Command Palette

Search for a command to run...

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

Published
3 min read
Adding Swift Concurrency capabilities to 3rd party projects
M

I am a Software Engineer working on open source and enterprise mobile SDKs for iOS and MacOS developers written in Swift. From 🇩🇪 and happily living in 🇺🇸

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

More from this blog

Dev blog post potpourri by senior software engineer Marco Eidinger

149 posts

Hello 👋🏻 , I am a Software Engineer working on open source and enterprise mobile SDKs for iOS and MacOS developers written in Swift. From 🇩🇪 and happily living in 🇺🇸