Is it incorrect to use Array.forEach with strict concurrency?

In my UIKit code I have this pattern of using forEach function to add subviews e.g.

[
  view1,
  view2
].forEach(stackView.addArrangedSubviews)

However now with Strict Concurrency enabled I get a warning

Converting function value of type '@MainActor (UIView) -> Void' to '(UIView) throws -> Void' loses global actor 'MainActor'; this is an error in the Swift 6 language mode

Is it considered wrong now to use forEach to operate on MainActor isolated objects?

3 Likes

As someone who has also leveraged this pattern, I'm looking forward to hearing how to circumvent this.

Is it considered wrong now to use forEach to operate on MainActor isolated objects?

No. But you need to make sure that the forEach is executed in a MainActor isolated context.

I'm not sure what the issue with your code is exactly as I'm lacking the relevant context of where this function is called, but while this reproduces the warning in a Swift Playground:

@MainActor var stackView = UIStackView()
@MainActor var view1 = UIView()
@MainActor var view2 = UIView()

Task {
    await [
      view1,
      view2
    ]
    // WARNING: Converting function value of type '@MainActor (UIView)
    // -> Void'  to '(UIView) throws -> Void' loses global actor 
    // 'MainActor';  this is an error in the Swift 6 language mode
    .forEach(stackView.addArrangedSubview)
}

Or, in a more general case:

let fooClosure: @MainActor (String) -> Void = { _ in }

// WARNING: Converting function value of type '@MainActor (String)
// -> Void'  to '(String) throws -> Void' loses global actor
// 'MainActor';  this is an error in the Swift 6 language mode
["apple", "orange"].forEach(fooClosure) // ❌

Since forEach will inherit the isolation context of its caller, you can fix the issue by annotating the call site to ensure you're in the correct isolation context, by any method you want. For example:

let fooClosure: @MainActor (String) -> Void = { _ in }

Task { @MainActor in
    ["apple", "orange"].forEach(fooClosure) // ✅
}

If the code that is calling this forEach is in a function, you can annotate the entire function instead:

@MainActor func fooFunction() {
    ["apple", "orange"].forEach(fooClosure) // ✅
}

If the issue is that the forEach was already being called in a MainActor-isolated context but the compiler isn't able to infer it (for example: a completion handler from a framework that specifies that it'll always be called on the main thread, but isn't annotated with @MainActor yet), you can use MainActor.assumeIsolated { ... }:

MainActor.assumeIsolated {
    ["apple", "orange"].forEach(fooClosure) // ✅ as long as this is actually called in the Main Actor
}

TL;DR: Make sure the compiler knows that the forEach is called from the Main Actor.

4 Likes

Array is not isolated to @MainActor and forEach function signature is:

@inlinable public func forEach(_ body: (Element) throws -> Void) rethrows

In Swift 6 the addArrangedSubview is expected to be:

@MainActor (UIView) -> Void

So, it tries to convert @MainActor (UIView) -> Void to (UIView) -> Void to pass it to forEach.

You can write your extension with @MainActor forEach.
Or update code like this:

[v1, v2].forEach { stackView.addArrangedSubview { $0 } }
3 Likes

Just to add that while it’s not isolated by itself, its API is synchronous, implementation as well, and passed closure will be running in whatever context they called, e.g. on main actor in case of example. That’s a long standing “oddity” in warnings, that often is misleading, but sometimes it actually warns about the problem (SwiftUI has had(?) this with Button’s action, for example, as it could led to isolation violations in some cases).

3 Likes

This is how I generally create views in UIKit, that's the core that triggers the warning in question:

class LoginViewController: UIViewController {
    lazy var foobar: UIStackView = {
        let stackView = UIStackView()
        stackView.translatesAutoresizingMaskIntoConstraints = false

        [
            usernameTextField,
            passwordTextField,
        ].forEach(stackView.addArrangedSubview) // Converting function value of type '@MainActor (UIView) -> Void' to '(UIView) throws -> Void' loses global actor 'MainActor'; this is an error in the Swift 6 language mode

        return stackView
    }()
...

This is how I generally create views in UIKit, that's the core that triggers the warning in question:

Simply add a @MainActor in to the closure you use to initialize foobar:

class LoginViewController: UIViewController {
    lazy var foobar: UIStackView = { @MainActor in // <- Here
        let stackView = UIStackView()
        stackView.translatesAutoresizingMaskIntoConstraints = false

        [
            usernameTextField,
            passwordTextField,
        ].forEach(stackView.addArrangedSubview) // ✅

        return stackView
    }()
...

This fixes the issue in Xcode 16.0. Now both the closure inside forEach and its caller are Main Actor isolated so you're no longer losing Main Actor isolation by passing a Main Actor isolated closure (stackView.addArrangedSubview) to a non-isolated caller (forEach).

2 Likes

This definitely seems like a bug: lazy properties of @MainActor isolated classes are already initialized on the main actor, but the compiler doesn't seem to be able to see that fact. This has the same bug (plus the fact that it thinks there's a throwing closure in here):

@MainActor
class SomeClass {
    lazy var string: Void = {
        ["one", "two"].forEach(printString) // Errors
    }()
}

@MainActor
func printString(_ input: String) {
    print("\(input)string")
}

which produces both

Call can throw, but it is not marked with 'try' and the error is not handled

and

Converting function value of type '@MainActor (String) throws -> Void' to '(String) throws -> Void' loses global actor 'MainActor'

which both seems like bugs.

5 Likes

Hmm you're right, I hadn't realized that closures used to init lazy properties in a @MainActor annotated classes also inherit the Main Actor isolation :man_bowing:

Something else pointint to his being a bug is that it's also possible to make the warning/error disappear by changing this line in your example:

["one", "two"].forEach(printString) // ❌

With this:

["one", "two"].forEach { printString($0) }  // ✅

Which I think should be equivalent.

2 Likes

Thanks! :pray: I got a workaround and a confirmation that it is likely a bug, how awesome is that!
FYI I reported this weird diagnostic message here: Feedback wanted: confusing concurrency diagnostic messages - #5 by ddenis

2 Likes

I'm pretty sure the partially-applied function behaving differently than a closure is equivalent to the root cause of this problem: