`@MainActor` ignored by the compiler

Here is an example that I thought would work as advertised, but it's not.

import SwiftUI

class Model: ObservableObject {
  @Published
  var data: Data?
}

struct TestView: View {
  @ObservedObject
  var _model = Model()

  @MainActor
  func apply(_ data: Data) {
    // runtime warning:
    // Publishing changes from background threads is not allowed; make
    // sure to publish values from the main thread (via operators like
    // receive(on:)) on model updates.

    _model.data = data

    // runtime crash
    dispatchPrecondition(condition: .onQueue(.main))
  }

  func fetchData() async throws {
    let urlString = "https://google.com"
    guard case let url? = URL(string: urlString) else {
      return
    }
    let (data, response) = try await URLSession.shared.data(from: url)
    guard
      case let httpResponse? = response as? HTTPURLResponse,
      httpResponse.statusCode == 200
    else {
      return
    }

    // note there is NO `await` here as the compiler does not ask for it at all
    apply(data)
  }

  var body: some View {
    Text("\(_model.data.map(\.count) ?? 0)")
      .task {
        do {
          try await fetchData()
        } catch {
          print(error)
        }
      }
  }
}

I can try adding await infant of the apply call, but this results into a No 'async' operations occur within 'await' expression warning.

I can apply two workarounds:

  • Make apply(_:) method async and add await infant of it as the compiler correctly would ask for, but this doesn't make sense as this should be a non-suspendible synchronous method just to hop back to the main thread and sign the value.
  • I can move @MainActor to the entire type and make fetchData as nonisolated, which works as intended, but the original example should also work and require the await keyword.

Edit:
It smells like a bug so I filed a report: [SR-14764] `@MainActor` ignored by the compiler · Issue #57114 · apple/swift · GitHub

One additional complication I noticed in @Ben_Cohen's WWDC presentation: Views are only inferred to be on the MainActor when they access shared state like @Environment or @EnvironmentObject. I'm really not sure how this would work, or if it's actually part of the issue here. Does anyone have any insight here?

Well I don't remember that part but I can tell that a known implicitly nonisolated method calls into an MainActor protected method and does not explicitly require await (or even with explicit keyword) which makes the compiler not hop threads and proceeds executing code meant for the main thread on a backround thread. That's clearly a bug to me.

Turns out that this is a compiler bug that was fixed as part of Swift 6 stricter concurrency checking by DougGregor · Pull Request #40388 · apple/swift · GitHub.

Doug

2 Likes