Hello,
I have a lot of apps and I am currently trying to port them over to Swift 6. I thought that this process should be relatively simple but I have to admit that I have a lot of trouble to understand how the Concurrency system works.
Let's start with some code that shows how I am currently working when it comes to asynchronous work in my apps:
- I have a
Model
that is marked with@Observable
. - Inside this model, a
Controller
is hosted. - The
Controller
has its ownControllerDelegate
. - The
Model
has asearch
function. Inside this function a lot of IO stuff is executed. This can take a lot of time. Because of this fact, I am doing this in a separateThread
.
I all is put together, it looks a little bit like this:
@main
struct OldExampleApp : App {
@State private var model = Model()
var body: some Scene {
WindowGroup {
ContentView()
.environment(self.model)
}
}
}
struct ContentView: View {
@Environment(Model.self) private var model
var body: some View {
if self.model.isSearching {
ProgressView()
}
else {
Button("Start") {
self.model.search()
}
}
}
}
protocol ControllerDelegate : AnyObject {
func controllerDidStart()
func controllerDidEnd()
}
class Controller {
weak var delegate: ControllerDelegate?
func search() {
let thread = Thread {
DispatchQueue.main.async {
self.delegate?.controllerDidStart()
}
// Do some very complex stuff here. Let's use sleep to simulate this.
Thread.sleep(forTimeInterval: 2.0)
DispatchQueue.main.async {
self.delegate?.controllerDidEnd()
}
}
thread.start()
}
}
@Observable
class Model {
private(set) var isSearching = false
var controller = Controller()
init() {
self.controller.delegate = self
}
func search() {
self.controller.search()
}
}
extension Model : ControllerDelegate {
func controllerDidStart() {
self.isSearching = true
}
func controllerDidEnd() {
self.isSearching = false
}
}
This works perfectly fine and by that I mean:
- The task is run in the background.
- The main thread is not blocked. The main window can be dragged around, no beach ball cursor etc.
Now comes the Swift 6 part:
- I want to merge the
Model
andController
into one class (Model
). - I still want the
Model
to beObservable
. - I want to run arbitrary code in the
Model
. This means that the code is not necessarily a prime candidate forawait
like getting data from a web server etc. - The main thread should not be blocked, so the main window should still be movable while the app calculates data in the background.
I have this example:
struct ContentView: View {
@Environment(Model.self) private var model
var body: some View {
if self.model.controller.isSearching
{
ProgressView()
}
else
{
Button("Search") {
Task {
await self.model.controller.heavyWork()
}
}
}
}
}
@Observable
final class Model : Sendable
{
@MainActor var controller = AsyncController()
init()
{
}
}
@Observable
@MainActor
class AsyncController
{
private(set) var isSearching = false
public func heavyWork() async
{
self.isSearching = true
Swift.print(Date.now)
let i = self.slowFibonacci(34)
Swift.print(i)
Swift.print(Date.now)
self.isSearching = false
}
func slowFibonacci(_ n: Int) -> Int
{
if n <= 1 {
return n
}
let x = slowFibonacci(n - 1)
let y = slowFibonacci(n - 2)
return x + y
}
}
I come from a C# background and my expectation is that when I use a Task
with await
, the main thread is not blocked and the Code that is called inside the Task
runs in the background.
It seems like the function is run in the background, but the UI is not updated. Because I set the isSearching
flag to true, I would expect that the app would display the ProgressView
- but it does not.
I changed the code to this:
public func heavyWork() async
{
self.isSearching = true
Swift.print(Date.now)
let i = await self.slowFibonacci(20)
Swift.print(i)
Swift.print(Date.now)
self.isSearching = false
}
func slowFibonacci(_ n: Int) async -> Int
{
let task = Task { () -> Int in
if n <= 1 {
return n
}
let x = await slowFibonacci(n - 1)
let y = await slowFibonacci(n - 2)
return x + y
}
return await task.value
}
This seems to work - but is this correct? I have this pattern implemented in one of my apps and there the main thread is blocked when the code is run.
So I think it all comes down to this:
- Is it possible, to run a arbitrary code block (without an await in it) in a Task, that can be awaited so the main thread is not blocked?
- The class (or actor?) that contains the function that is called via await should be
Observable
. - Or should I simply keep my Swift 5 code and move on? :D
Regards, Sascha
my expectation is that when I use a Task with await, the main thread is not blocked
I think this is your fundamental misconception. I actually just explained this over on another thread. Have a read of that and then get back to me here if you’re still stuck.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"