Problem with structured concurrency and observation

I'm using structured concurrency to handle a bunch of async tasks. I want to update the UI with the completed count as soon as they're done, but I'm getting a warning. Not sure how to fix it though.

import Observation

func asyncStuff() async {
  
}

@Observable
public final class Foo {
  public var count = 0
  
  @MainActor
  func updateCount() {
    count += 1
  }
  
  public func foo() async throws {
    try await withThrowingTaskGroup(of: Void.self) { group in
      var maxConcurrency = 8
      
      for index in 0..<10000 {
        
        if maxConcurrency <= 0 {
          try await group.next()
          
          maxConcurrency += 1
        }
        
        group.addTask {
          await asyncStuff()
          // Capture of 'self' with non-sendable type 'Foo' in a `@Sendable` closure
          // The problem is because Foo isn't Sendable, and since it has a count variable, I can't just declare it as Sendable.
          await self.updateCount()
        }
        
        maxConcurrency -= 1
      }
    }
  }
}

You mean the access to count inside the task? If I recall correctly, Observable doesn't impose any isolation requirements on the type it decorates, but SwiftUI does require all observations to happen on MainActor. Therefore your Foo essentially has to be @MainActor. Thus count is @MainActor. So, to modify it from inside your tasks you need to do something like:

await MainActor.run {
    self.count += 1
}

…or, if you don't need it to be synchronised (which is usually the case):

DispatchQueue.main.async {
    self.count += 1
}

I'm sorry, I accidentally clicked on publish earlier. The issue I encountered is that accessing the variable count within group.addTask is not allowed. Xcode warns me: Capture of 'self' with non-sendable type 'Foo' in a @Sendable closure.

Perhaps I should rethink the concurrent implementation, but I'm not sure how to proceed.

With a few small modifications, this compiles without warning for me. I've added some comments inline to explain the changes, but the short version is that Foo does need to be Sendable, so count needs to be isolated to @MainActor.

import Observation

func asyncStuff() async {

}

@Observable
public final class Foo: Sendable { // We're sendable if all mutable properties are either sendable or isolated.
  @MainActor // Required to have the same isolation as `updateCount()`
  public var count = 0

  // Adding `@MainActor` to `count` causes the synthesized default init to have
  // the incorrect isolation, so explicitly declare an empty nonisolated init.
  public init() {}

  @MainActor
  func updateCount() {
    count += 1
  }

  public func foo() async throws {
    try await withThrowingTaskGroup(of: Void.self) { group in
      var maxConcurrency = 8

      for index in 0..<10000 {

        if maxConcurrency <= 0 {
          try await group.next()

          maxConcurrency += 1
        }

        group.addTask {
          await asyncStuff()
          // No longer a problem because `self` is sendable.
          await self.updateCount()
        }

        maxConcurrency -= 1
      }
    }
  }
}