@Observable macro conflicting with @MainActor

TLDR: Do updates within @Observable objects not need to be on the Main Thread or Main Actor?


Prior to iOS 17, it was common to run into the following warning when updating an @Published value inside an ObservableObject:

Publishing changes from background threads is not allowed; make sure to publish values from the main thread

The following code is an example of this:

actor Actor1 {
    func getTitle() async -> String {
        "Hello, world!"
    }
}

final class ObservableObject1: ObservableObject {
    
    let actor = Actor1()
    @Published var title = "Hello"
    
    func updateTitle() {
        Task {
            title = await actor.getTitle()
        }
    }
}

struct View1: View {

    @StateObject var object = ObservableObject1()
    
    var body: some View {
        Text(object.title)
            .onAppear {
                object.updateTitle()
            }
    }
}

One solution I've used for this was to mark the ObservableObject as @MainActor.

@MainActor final class ObservableObject1: ObservableObject {
}

When I update to the iOS 17 @Observed macro, the warning disappears without requiring @MainActor:

@Observable final class ObservableObject2 {
    
    let actor = Actor1()
    var title = "Hello"
    
    func updateTitle() {
        Task {
            title = await actor.getTitle()
        }
    }
}

struct View2: View {

    @State var object = ObservableObject2()
    
    var body: some View {
        Text(object.title)
            .onAppear {
                object.updateTitle()
            }
    }
}

However, if I put print(Thread.current) statements inside the Task in each of these examples, it seems that the @Observable implementation is updating without being on the Main Thread.

The following example...

@Observable final class ObservableObject2 {
    
    let actor = Actor1()
    var title = "Hello"
    
    func updateTitle() {
        print(Thread.current)
        Task {
            title = await actor.getTitle()
            print(Thread.current)
        }
    }
}

...will print...

<_NSMainThread: 0x600001710000>{number = 1, name = main}
<NSThread: 0x60000174c180>{number = 6, name = (null)}

... but marking the Task as...

Task { @MainActor in 
}

...will print...

<_NSMainThread: 0x600001708000>{number = 1, name = main}
<_NSMainThread: 0x600001708000>{number = 1, name = main}

Further, if I try to mark the @Observable as @MainActor, I run into the following error:

Call to main actor-isolated initializer 'init()' in a synchronous nonisolated context

So my guess is that this is by design so that developers don't need to worry about thread safety as much, but is there an explanation for this?

  • Do objects using @Observable macro not need to be on the Main Thread?
  • Is something happening within the @Observable macro that is updating on the Main Thread?
  • Is the SwiftUI View getting updates from threads other then the Main Thread?

Thanks in advance,

Nick

6 Likes