'Can I use actors in Swift to always call a function on the main thread?
I recently saw that Swift had introduced concurrency support with the Actor model in Swift 5.5. This model enables safe concurrent code to avoid data races when we have a shared, mutable state.
I want to avoid main thread data races in my app's UI. For this, I am wrapping DispatchQueue.main.async
at the call site wherever I set a UIImageView.image
property or a UIButton
style.
// Original function
func setImage(thumbnailName: String) {
myImageView.image = UIImage(named: thumbnailName)
}
// Call site
DispatchQueue.main.async {
myVC.setImage(thumbnailName: "thumbnail")
}
This seems unsafe because I have to remember to dispatch the method manually on the main queue. The other solution looks like:
func setImage(thumbnailName: String) {
DispatchQueue.main.async {
myImageView.image = UIImage(named: thumbnailName)
}
}
But this looks like a lot of boilerplate, and I wouldn't say I like using this for complex functions with more than one level of nesting.
The release of Swift support for Actors looks like a perfect solution for this. So, is there a way to make my code safer, i.e. always call UI functions on the main thread using Actors?
Solution 1:[1]
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.
Bonus Notes
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
.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 |