Hey, I'm seeing this issue using the compiler in Xcode Version 13.0 beta 3 (13A5192j)
class ViewController: UIViewController {
var value : String? = "Hello"
@IBAction func showAlert(_ sender: Any) {
Task {
defer { value = nil } // ← Property 'value' isolated to global actor 'MainActor' can not be mutated from this context
// ~~await Task.sleep(1)~~ This doesn't even matter, but the await is why I'm using Task
}
}
}
This shouldn't be a problem, right? ViewController inherits from @MainActor UIViewController so all its properties and methods and spawned tasks should also be known to execute under the aegis of the main actor.
The async/await proposal says "A potential suspension point must not occur within a defer block." but that's not the case here since it is running on the same actor, right?
For a bit of context, I am actually awaiting on a withCheckedThrowingContinuation in order to display a file picker, and the API of the file picker doesn't allow for the continuation to be kept local to the withContinuation block. So it has to be saved as a member variable. And I reset that member to nil to indicated that the file picker is done and allow another file picker to be shown; if they try to show a picker and it isn't nil, that's an error. And I want to use defer because if the user cancel, I call the continuation to throw an error and I want the continuation member variable to be set to nil on both the happy and cancellation path.
This hasn't been fixed in the Xcode 13 release version. Whether or not the fix will be in some upcoming point release of Xcode, is it fixed in the separate Swift compiler?
do {
let user = try await network.getUser(id: 1)
} catch error as SomeValidationError {
// handle validation error
} catch error as MyNetworkError {
// handle network error
} catch {
// handle generic error
}
This is all fine and dandy, until you add a loading indicator.
do {
isLoading = true
// defer { isLoading = false } // <- Does not work in Actor context
let user = try await network.getUser(id: 1)
} catch error as SomeValidationError {
isLoading = false
// handle validation error
} catch error as MyNetworkError {
isLoading = false
// handle network error
} catch {
isLoading = false
// handle generic error
}
Now you're forced to either:
Duplicate a lot of code
Bundle all catch into a single one, and do the type-casting inside. (However you still need to duplicate isLoading twice, once in the non-throwing and once in the throwing branch
Go against Swift's error handling and make getUser(id:) non-throwing, but returning a Result
All this because you cannot use defer {} in an Actor context.
swift-driver version: 1.45.2 Apple Swift version 5.6 (swiftlang-5.6.0.323.62 clang-1316.0.20.8)
Target: arm64-apple-macosx12.0
@MainActor
class Foo {
var bar = 0
func frob() {
defer {
bar += 1 // fine
}
Task {
defer {
bar += 1 // error: property 'bar' isolated to global actor 'MainActor' can not be mutated from a non-isolated context
}
bar += 1
}
}
}
I'm seeing the issue directly inside a Task initializer closure as well. I noticed the Changelog (swift/CHANGELOG.md at main · apple/swift · GitHub) uses an example inside an async function - perhaps the Task use case isn't being handled properly?
Another Example to confirm that its related to closures:
@MainActor
class A {
var isLoading: Bool = false
func startTask1() {
Task { @MainActor in
isLoading = true
defer { isLoading = false } // ❌ Error: Property 'isLoading' isolated to global actor 'MainActor' can not be mutated from a non-isolated context
print("Doing task")
}
}
// Extracting the logic to a separate function
func startTask2() {
Task { await startTask2Impl() } // ✅ OK
}
private func startTask2Impl() async {
isLoading = true
defer { isLoading = false } // ✅ OK
print("Doing task")
}
}