Actors in Swift 5.5 ♀️
Actor isolation and re-entrancy are now implemented in the Swift stdlib. So, Apple recommends using the model for concurrent logic with many new concurrency features to avoid data races. Instead of lock-based synchronisation (lots of boilerplate), we now have a much cleaner alternative.
Some UIKit
classes, including UIViewController
and UILabel
, now have out of the box support for @MainActor
. So we only need to use the annotation in custom UI-related classes. For example, in the code above, myImageView.image
would automatically be dispatched on the main queue. However, the UIImage.init(named:)
call is not automatically dispatched on the main thread outside of a view controller.
In the general case, @MainActor
is useful for concurrent access to UI-related state, and is the easiest to do even though we can manually dispatch too. I've outlined potential solutions below:
Solution 1
The simplest possible. This attribute could be useful in UI-Related classes. Apple have made the process much cleaner using the @MainActor
method annotation:
@MainActor func setImage(thumbnailName: String) {
myImageView.image = UIImage(image: thumbnailName)
}
This code is equivalent to wrapping in DispatchQueue.main.async
, but the call site is now:
await setImage(thumbnailName: "thumbnail")
Solution 2
If you have Custom UI-related classes, we can consider applying @MainActor
to the type itself. This ensures that all methods and properties are dispatched on the main DispatchQueue
.
We can then manually opt out from the main thread using the nonisolated
keyword for non-UI logic.
@MainActor class ListViewModel: ObservableObject {
func onButtonTap(...) { ... }
nonisolated func fetchLatestAndDisplay() async { ... }
}
We don't need to specify await
explicitly when we call onButtonTap
within an actor
.
Solution 3 (Works for blocks, as well as functions)
We can also call functions on the main thread outside an actor
with:
func onButtonTap(...) async {
await MainActor.run {
....
}
}
Inside a different actor
:
func onButtonTap(...) {
await MainActor.run {
....
}
}
If we want to return from within a MainActor.run
, simply specify that in the signature:
func onButtonTap(...) async -> Int {
let result = await MainActor.run { () -> Int in
return 3012
}
return result
}
This solution is slightly less cleaner than the above two solutions which are most suited for wrapping an entire function on the MainActor
. However, actor.run
also allows for inter threaded code between actor
s in one func
(thx @Bill for the suggestion).
Solution 4 (Block solution that works within non-async functions)
An alternative way to schedule a block on the @MainActor
to Solution 3:
func onButtonTap(...) {
Task { @MainActor in
....
}
}
The advantage here over Solution 3 is that the enclosing func
doesn't need to be marked as async
. Do note however that this dispatches the block later rather than immediately as in Solution 3.
Summary
Actors make Swift code safer, cleaner and easier to write. Don't overuse them, but dispatching UI code to the main thread is a great use case. Note that since the feature is still in beta, the framework may change/improve further in the future.
Since we can easily use the actor
keyword interchangeably with class
or struct
, I want to advise limiting the keyword only to instances where concurrency is strictly needed. Using the keyword adds extra overhead to instance creation and so doesn't make sense when there is no shared state to manage.
If you don't need a shared state, then don't create it unnecessarily. struct
instance creation is so lightweight that it's better to create a new instance most of the time. e.g. SwiftUI
.