I am learning async/await
and Task. So i learned that Task basically inherit actor.
Imagine I have a model:
class SomeModel: ObservableObject {
@Published var downloads: [Int] = []
func doSome() async throw {
// MAKE URL
downloads.append(1)
try await /// MAKE REQUEST
downloads.append(data) // after request
}
}
And I have a simple View
struct DownloadView: View {
@EnvironmentObject var model: SomeModel
var body: some View {
List {
// USE model.downloads
}
.task {
do {
try await model.doSome()
} catch {}
}
}
I get an error that I am updating UI from background thread. Thats ok. Then i add
func doSome() async throw {
// MAKE URL
downloads.append(1)
try await /// MAKE REQUEST
await MainActor.run {
downloads.append(data)
} // after request
}
And I have still Error. System does not know that first append is Always main actor? Or this is a security from dummies if some on pus some suspended code before first append?
How I understand hierarchical await is needed for System know what tree of sub jobs need to suspend. But a suspension point is after first append, so it could not pass on different actor.
Code for reproduce: Model
struct DownloadFile: Identifiable {
var id: String { return name }
let name: String
let size: Int
let date: Date
static let mockFiles = [DownloadFile(name: "File1", size: 100, date: Date()),
DownloadFile(name: "File2", size: 100, date: Date()),
DownloadFile(name: "File3", size: 100, date: Date()),
DownloadFile(name: "File4", size: 100, date: Date()),
DownloadFile(name: "File5", size: 100, date: Date()),
DownloadFile(name: "FIle6", size: 100, date: Date())]
static let empty = DownloadFile(name: "", size: 0, date: Date())
}
FirstView
struct ContentView: View {
@State var files: [DownloadFile] = []
let model: ViewModel
@State var selected = DownloadFile.empty {
didSet {
isDisplayingDownload = true
}
}
@State var isDisplayingDownload = false
var body: some View {
NavigationStack {
VStack {
// The list of files available for download.
List {
Section(content: {
if files.isEmpty {
ProgressView().padding()
}
ForEach(files) { file in
Button(action: {
selected = file
}, label: {
Text(file.name)
})
}
})
}
.listStyle(.insetGrouped)
}
.task {
try? await Task.sleep(for: .seconds(1))
files = DownloadFile.mockFiles
}
.navigationDestination(isPresented: $isDisplayingDownload) {
DownloadView(file: selected).environmentObject(model)
}
}
}
}
Second view
struct DownloadView: View {
let file: DownloadFile
@EnvironmentObject var model: ViewModel
@State var result: String = ""
var body: some View {
VStack {
Text(file.name)
if model.downloads.isEmpty {
Button {
Task {
result = try await model.download(file: file)
}
} label: {
Text("-- Download --")
}
}
Text(result)
}
}
}
viewModel
class ViewModel: ObservableObject {
@Published var downloads: [String] = []
func download(file: DownloadFile) async throws -> String {
downloads.append(file.name)
try await Task.sleep(for: .seconds(2))
return "Download Finished"
}
}
NEW UPDATE FOR 31.10.2024
So how I understand When Swift see "await" - it mean that next code could be done on other tread that the colling thread, but it will start like Dispatch.current.aasync. So Task will start after main thread will finish but on another Tread.
Too much text about Actors and Isolation. For someone who don't understand how task is worked, and someone who still think Task.detached
is bad, and have very narrow use-cases.
So what I understand after some experiments and learning. All is simple. Apple did (like every time) old staff from past years in new cover. so:
All async staff is sync. If you want to run it async use Task. (but even you can't use it without task).
Task = DispatchQueue.current.async
- But on every await system could change Thread.
What does it mean?
If you have something like this:
class A {
func first() async {
for i in 0...1000 {
print("🤍 \(i)")
}
}
}
class ViewController: UIViewController {
let a = A()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
Task {
await a.first()
}
second()
}
func second() {
Thread.sleep(forTimeInterval: 5)
for i in 0...1000 {
print("♥️ \(i)")
}
}
}
Second()
will always print first, but First()
could print on other Thread then main
If you need DispatchQueue.global.async
you use Task.detached
await
its like sync on GCD.
This code :
Task {
await a.first()
}
is something like:
DispatchQueue.current.async {
DispatchQueue.global().async {
DispatchQueue.global().sync {
a.first
}
}
}
What does it mean? - If you have @MainActor
functions in your class for updating @Published
properties, and you for example what update a Progress of some background work (for example AsyncSequence
) you must use Task.detached
(what ever Apple say)
Apple problem is that they every time think that all apps in Store is 3 screen MVP whit one button and one endpoint.