swiftswiftuiasync-awaittaskactor

Swift does not know at what Actor is a Task?


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.


Solution

  • 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:

    1. All async staff is sync. If you want to run it async use Task. (but even you can't use it without task).

    2. 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

    1. 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.