@preconcurrency usage in swift explained

Published on: May 28, 2024

When you enable strict concurrency checks for your existing projects, it’s likely that Xcode will present loads of warnings and/or errors when you compile your project for the first time. In this post, I’d like to take a look at a specific kind of error that relates to code that you didn’t write.

The @preconcurrency declaration can be added to:

  • functions
  • types
  • protocols
  • imports

Let’s take a look at all of these areas to fully understand how @preconcurrency helps us enable strict concurrency checks even if we can’t update all of our dependencies just yet.

@preconcurrency imports

To be specific, Xcode will sometimes offer a message that reads a lot like this:

Add @preconcurrency to suppress Sendable-related warnings from module MyModule

This error tells us that we’re importing a module that doesn’t appear to completely adhere to modern concurrency rules just yet. Since this might not be a module that you own, Xcode offers you the ability to silence strict concurrency warnings and errors coming from this module.

You can do this by adding @preconcurrency to your import statement:

@preconcurrency import MyModule

By doing this, Xcode will know that any warnings related to types coming from MyModule should be suppressed.

If MyModule is not a module that you own, it makes a lot of sense to suppress warnings; you can’t fix them anyway.

Note that this won’t suppress warnings related to code from MyModule that is Sendable or up-to-date with modern concurrency. So if you see warnings related to concurrency on a module that you’ve marked with @preconurrency, you’ll want to fix those warnings because they’re correct.

Adding @preconcurrency to types, functions, and more

Alternatively, you might be working on a module that has adopted Swift Concurrency and you’ve fixed your warnings. If that’s the case, you might want to add @preconcurrency to some of your declarations to ensure that code that depends on your module doesn’t break.

Adopting Swift Concurrency will mean that your module’s ABI changes and that some older code might not be able to use your modules if that older code doesn’t also adopt Swift Concurrency.

If this is the situation you’re in, you might have updated some of your code from this:

public class CatalogViewModel {
  public private(set) var books: [Book] = []

  public init() {}

  func loadBooks() {
    // load books
  }
}

To this:

@MainActor
public final class CatalogViewModel {
  public private(set) var books: [Book] = []

  public init() {}

  public func loadBooks() {
    // load books
  }
}

If you have pre-concurrency code that uses this class, it might look a bit like this:

class TestClass {
  func run() {
    let obj = CatalogViewModel()
    obj.loadBooks()
  }
}

Unfortunately adding @MainActor to our class in the module makes it so that we can’t use our view model unless we dispatch to the main actor ourselves. The compiler will show an error that looks a bit like this:

Call to main actor-isolated initializer 'init()' in a synchronous nonisolated context.
Call to main actor-isolated instance method 'loadBooks()' in a synchronous nonisolated context.

This tells us that in order to interact with CatalogViewModel, we’ll need to update our project to use the main actor. This will often snowball into more and more code updates which makes the changes in our module severely breaking.

We can apply @preconcurrency to our view model to allow code that hasn’t been updated to interact with our view model as if it was never main actor annotated:

@preconcurrency @MainActor 
public final class CatalogViewModel {
  public private(set) var books: [Book] = []

  public init() {}

  public func loadBooks() {
    // load books
  }
}

Note that the above only works for projects that do not enable strict concurrency checking

With the @preconcurrency annotation in place for our entire class, the compiler will strip the @MainActor annotation for projects that have their concurrency checking set to minimal. If you’re using strict concurrency checks, the compiler will still emit errors for not using CatalogViewModel with the main actor.

In Summary

With @preconcurrency, we can import old modules into new code and we can allow usage of new code in old projects. It’s a great way to start to incrementally adopt strict concurrency as the release of Swift 6 comes closer and closer.

Adding @preconcurrency to your imports is very useful when you’re importing modules that have not yet been updated for strict concurrency.

Adding @preconcurrency to declarations that you’ve annotated as @Sendable, @MainActor, or otherwise updated in a way that makes it impossible to use them in non-concurrent code can make a lot of sense for library authors.

Subscribe to my newsletter