You seems to mixing up a lot of different concepts together. Let's try structuring them a bit.
First of all, non-isolated functions can be different as if they are async
or not. Synchronous functions inherit caller isolation by default, asynchronous nonisolated functions run on a generic executor.
Unstructured tasks (Task { /* ... */ }
) inherit isolation of a caller, so if you run it inside isolated (say, global actor) context, it will have same isolation. Exception as for right now is isolated
parameter, but let's put it aside for now.
Now, inside the task (unstructured in that case, but generally any) you can call anything from other isolation domains, as they ensure execution in that isolation domain, and await
make this explicit. So if you create task in non-isolated context and call from there main actor isolated function, it is does not change isolation at all, and required await
here highlight that this task is in different domain. If you have task inside same isolated domain, it won't require await
:
// Task {} omitted as async functions basically do the same here:
nonisolated func runNonisolated() async {
await UIApplication.shared.open(url)
}
@MainActor
func runOnMainActor() {
UIApplication.shared.open(url)
}
Finally, as for mutable state, in your example with open
method there is no mutable state involved you access and mutate. What it says, is that, if you have some state (say, progress bar state) in isolation (e.g. on main actor), you cannot mutate it outside this isolation:
@MainActor
final class LoadingViewModel {
var progress: Double?
}
nonisolated func callNonisolated(viewModel: LoadingViewModel) async {
viewModel.progress = 10.0 // error: main actor-isolated property 'progress' can not be mutated from a non-isolated context
}
But you can allow mutation inside isolation domain:
extension LoadingViewModel {
func update(progress: Double?) {
self.progress = progress
}
}
nonisolated func callNonisolated(viewModel: LoadingViewModel) async {
await viewModel.update(progress: 10.0)
}
Now isolation of LoadingViewModel
to MainActor
ensures that update of progress
happens on main actor, and enforces await
on the call side from different isolation. That is true if we have simply different isolation:
actor X {
func notify(vm: LoadingViewModel) async {
await vm.update(progress: nil)
}
// or
func notifyAndForget(vm: LoadingViewModel) {
Task { await vm.update(progress: nil) }
}
// or even explicitly isolate Task to main actor
func notifyWithExplicitMainActor(vm: LoadingViewModel) {
Task { @MainActor in
vm.update(progress: nil) // note that here we don't need await anymore
}
}
}