3

I am trying to understand what the rule is when calling Task { ... } and, in that task, calling await in terms of threads.

This example works:

struct TaskTestView: View {
    
    let url = URL(string: "https://www.google.com")!
    @State private var message = "Loading..."

    var body: some View {
        Text(message)
            .task {
                /// on MAIN THREAD
                do {
                    var receivedLines = [String]()
                    for try await line in url.lines {
                 /// on MAIN THREAD
                        receivedLines.append(line)
                        message = "Received \(receivedLines.count) lines"
                    }
                } catch {
                    message = "Failed to load"
                }
            }
    }
}

This does not:

struct TaskTestView: View {

    @StateObject var model = TaskTestViewModel()
    
    var body: some View {
        Text(model.message)
            .task {
                /// - here it is on main thread
                await model.refresh()
                /// - here it is NOT on main thread
                print("after refresh: on main?")
            }
    }
}

class TaskTestViewModel:ObservableObject {
    
    let url = URL(string: "https://www.google.com")!
    @Published private(set) var message = "Loading..."
    
    func refresh() async {
        
        do {
            var receivedLines = [String]() // on main thread
            for try await line in url.lines {
                receivedLines.append(line) // NOT on main thread
                message = "Received \(receivedLines.count) lines"
            }
        } catch {
            message = "Failed to load"
        }
        
    }
    
}
  1. Why does the code run on the main thread after line for try await line in url.lines { in the first example?
  2. Why does the code NOT run on the main thread after that same line but in the second example?

How would I know the answer to those questions without running the code and putting a breakpoint to inspect the thread I am on?

Obviously the main issue here is that I want to make sure I update @State variables on main so the view works properly, but, not having this concept clear makes it hard to design proper patterns.

3
  • Here is a good article that can help you better understand avanderlee.com/swift/async-await Commented Feb 16, 2022 at 21:52
  • I've read that article a while back. Good article. However, that link does not address any of this. Not useful. Commented Feb 17, 2022 at 17:00
  • I think it’s because we don’t use view model objects in SwiftUI Commented Feb 22, 2022 at 0:45

1 Answer 1

3

There are not clear rules regarding on which thread the “continuation” will run. WWDC 2021 video Swift concurrency: Behind the scenes talks about continuations and how the thread after the suspension point might be different than the one before.

Trying to diagnose this with traditional debugging statements or breakpoints can be a frustrating exercise. I have encountered situations where the insertion of a few completely unrelated lines of debugging code changed the continuation threading behavior. Bottom line, unless you tell it otherwise, it is free to decide on which thread the continuation will run and it can sometimes defy expectations.

But, if you want to make sure that your observable object updates on the main thread, you can use the @MainActor qualifier:

@MainActor
class TaskTestViewModel: ObservableObject {
    ... 
}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.