Hey folks, I'm working with Swift 6.2's approachable concurrency and I'm really enjoying the changes! I have both @MainActor set as the default isolation and nonisolated(nonsending) as the default for async functions.
What feels weird: When I mark an async function with @concurrent, any async functions I call from it automatically run on the caller's actor (an actor in the background in this case). But sync functions still default to @MainActor and need an explicit nonisolated annotation. I thought approachable concurrency meant both would just use whatever actor context they're called from? Maybe I'm not doing this right?
Here's an example of what I mean:
import HealthKit
struct HealthKitService {
let store = HKHealthStore()
// Async function DOES NOT NEED explicit `nonisolated(nonsending)` to run on caller's actor
private func isAuthorizationRequestUnnecessary(for type: HKQuantityType) async throws -> Bool {
return try await self.store.statusForAuthorizationRequest(
toShare: Set([type]),
read: Set([type]),
) == .unnecessary
}
@concurrent
func fetchStepStatistics() async throws -> [HKStatistics] {
let stepType = HKQuantityType(.stepCount)
guard try await self.isAuthorizationRequestUnnecessary(for: stepType) else {
throw AuthorizationRequestNecessaryError()
}
// ... more code ...
}
// Non-async function DOES NEED explicit `nonisolated` to run on caller's actor
nonisolated
private func isSharingAuthorized(for type: HKQuantityType) -> Bool {
return self.store.authorizationStatus(for: type) == .sharingAuthorized
}
@concurrent
func createStepSample(date: Date, value: Double) async throws -> Void {
let stepType = HKQuantityType(.stepCount)
guard self.isSharingAuthorized(for: stepType) else {
throw AuthorizationRequestNecessaryError()
}
// ... more code ...
}
}
When isAuthorizationRequestUnnecessary (async) is called from a @concurrent function, it just works—it runs on the caller's actor automatically. But if I remove nonisolated from isSharingAuthorized (sync) and try to call it from my @concurrent function, I get:
Main actor-isolated instance method 'isSharingAuthorized(for:)' cannot be called from outside of the actor
I get that there's a compiler flag that makes async functions use the caller's actor, but I thought the whole point of approachable concurrency was that functions would default to using whatever actor context they're called from? It just feels a bit asymmetric that async functions get this behavior but sync functions still need the explicit annotation.
Am I missing something here, or is this just how it works?