With the sample code below I have made a test to understand the async and await mechanism in Swift. The sequence of processes is achieved. My test prints to the console do appear in the intended sequence (step 1-4). However, what puzzles me is that the test messages, which shall appear on the UI, do not show up in the intended sequence. Both messages (Message1 and Message2) appear together at the end of the entire process after step 4. So why does Message1 not appear right after step 1 as coded?
import UIKit
class ViewController: UIViewController {
var testasyncDone = false
@IBOutlet weak var MyButton2: UIButton!
@IBOutlet weak var Message1: UITextField!
@IBOutlet weak var Message2: UITextField!
@IBAction func MyButton2pressed(_ sender: UIButton) {
print("MyButton Pressed step 1")
// This first message shall appear right after the button
// is pressed
Message1.text = "In Button action - start"
// The async task is defined and started
testasyncDone = false
Task.detached {
await self.testasync()
print("MyButton Pressed step 4")
}
// The intention of the next lines is to hold the
// processing of the main thread until the async task is
// completed.
var count = 0
repeat {
count = count 1
usleep(100000)
print (count)
} while testasyncDone == false && count < 100
// After the async task is done, the second message shall show up
Message2.text = "In Button action - end"
}
func testasync() async {
print("in testasync step 2")
sleep(2)
print("in testasync step 3")
testasyncDone = true
}
}
CodePudding user response:
You ask:
So why does Message1 not appear right after step 1 as coded?
Because you are blocking the main thread with your repeat-while loop. Your code is a perfect demonstration of why you should never block the main thread, as the UI cannot be updated until you free up the main thread. Any system events will be prevented, too. If you block the main thread for long enough, you even risk having your app unceremoniously killed by the 
You can synchronize this yourself (locks or GCD are traditional mechanisms), or with the new Swift Concurrency system, we would use an actor. See WWDC 2021 video, Protect mutable state with Swift actors.
So, I suspect that this is going to trigger the response, “well, if I can’t block the main thread, then what should I do?”
Let us consider a bunch of alternatives.
If you really need two tasks running in parallel and coordinate this with some state variable, one method counting until the other changes the state of that variable, you could first create an
actorto capture this state:actor TestAsyncState { private var _isDone = false func finish() { _isDone = true } func isDone() -> Bool { _isDone } }Then you could check this actor state:
var testAsyncState = TestAsyncState() @IBAction func didTapButton(_ sender: UIButton) { print("MyButton Pressed step 1") Task.detached { [self] in await MainActor.run { message1.text = "In Button action - start" } try await self.testAsync() print("MyButton Pressed step 4") await testAsyncState.finish() } Task.detached { [self] in // The intention of the next lines is to keep ticking // until the state actor isDone or we reach 100 iterations var count = 0 repeat { count = 1 try await Task.sleep(nanoseconds: NSEC_PER_SEC / 10) print(count) } while await !testAsyncState.isDone() && count < 100 await MainActor.run { message2.text = "In Button action - finished" } } }Or, alternatively, you could bypass this
actorstate variable entirely, and just cancel the counting task when the other finishes:@IBAction func didTapButton(_ sender: UIButton) { print("MyButton Pressed step 1") let tickingTask = Task.detached { [self] in // The intention of the next lines is to keep ticking // until this is canceled or we reach 100 iterations do { var count = 0 repeat { count = 1 try await Task.sleep(nanoseconds: NSEC_PER_SEC / 10) print(count) } while !Task.isCancelled && count < 100 await MainActor.run { message2.text = "In Button action - finished" } } catch { await MainActor.run { message2.text = "In Button action - canceled" } } } Task.detached { [self] in await MainActor.run { message1.text = "In Button action - start" } try await self.testAsync() print("MyButton Pressed step 4") tickingTask.cancel() } }Or, if you just wanted to do something on the main thread when the
asyncmethod is done, just put it after the method that you are awaiting:@IBAction func didTapButton(_ sender: UIButton) { print("MyButton Pressed step 1") Task.detached { [self] in await MainActor.run { message1.text = "In Button action - start" } try await self.testAsync() print("MyButton Pressed step 4") // put whatever you want on the main actor here, e.g. await MainActor.run { message2.text = "In Button action - finished" } } }Or, if you wanted a ticking timer on the main thread and you want to cancel it when the async task is done:
@IBAction func didTapButton(_ sender: UIButton) { print("MyButton Pressed step 1") var count = 0 let timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in count = 1 print(count) } Task.detached { [self] in await MainActor.run { message1.text = "In Button action - start" } try await self.testAsync() print("MyButton Pressed step 4") // put whatever you want on the main actor here, e.g. await MainActor.run { timer.invalidate() message2.text = "In Button action - finished" } } }
There are lots of ways to skin the cat. But, the key is that none of these block the main thread (or any thread, for that matter), but we can initiate whatever needs to happen on the main thread at the end of the async task.
