Hi everyone,
I’ve been working on a testing setup where I encountered some compiler behaviour that has me puzzled. Specifically, it relates to how async/await interacts with static properties in actors. I’m hoping to get a deeper understanding of why this behaviour is considered valid.
Here’s the relevant snippet:
// ensures tests are executed serially, protecting the shared static var
@globalActor
actor TestActor {
static let shared = TestActor()
}
@TestActor
@Suite("KeychainKeys Tests") struct KeychainKeysTests {
// making this an actor to keep the compiler happy, even though this
// will only be used within the TestActor...
actor MockBundle: XcodeBundle {
static var identifier = ""
static func extractIdentifier() throws -> String {
return identifier
}
}
@Test("Valid Bundle Identifier")
func testEncryptionKeyWithValidIdentifier() {
/// Given
// are we not crossing an actor boundary here? MockBundle actor <> TestActor
MockBundle.identifier = "com.example.app"
/// When
let key = KeychainKeys<MockBundle>.encryptionKey
/// Then
let expectedHash = SHA256.hash(data: Data("com.example.app".utf8))
.map { String(format: "%02x", $0) }.joined()
#expect(key == expectedHash, "Encryption key should match the expected hash.")
}
}
My Question
In the above example:
• MockBundle is an actor.
• identifier is declared as a static var in the actor.
My understanding (correct me if I'm wrong here):
- Static properties are not actor-isolated: This means they’re not protected by the actor’s isolation model.
- Static mutable state is inherently global mutable state: The compiler treats MockBundle.identifier as potentially unsafe in concurrent contexts, which is why it enforces concurrency safety checks (e.g., requiring MockBundle to be an actor).
This feels like a “maybe-safe” scenario, where the compiler is satisfied as long as the actor declaration is present, but it doesn’t guarantee thread safety for the static var.
What I’d Like to Understand
• Why does the compiler enforce that MockBundle must be an actor to handle a static var**, but then allow direct access to that** static var without await**?**
• Is this a deliberate design choice, or is there room for improvement in how Swift enforces safety in this situation?
• Should static properties in actors behave differently to align more strictly with the actor isolation model?
I appreciate that the actor model and async/await have strict rules for enforcing concurrency safety, but this particular case feels like a grey area to me. If anyone could clarify the reasoning behind this behaviour, I’d be very grateful!
Thank you in advance for your insights.