Categories
iOS Swift

Initializing @MainActor type from a non-isolated context in Swift

Recently I was in the middle of working on code where I wanted a type to require @MainActor since the type was an ObservaleObject and makes sense if it always publishes changes on the MainActor. The MainActor type needed to be created by another type which is not a MainActor. How to do it?

This does not work by default, since we are creating the MainActor type from a non-isolated context.

final class ViewPresenter {
init(dataObserver: DataObserver) {
// Call to main actor-isolated initializer 'init(dataObserver:)' in a synchronous nonisolated context
self.viewState = ViewState(dataObserver: dataObserver)
}
let viewState: ViewState
}
@MainActor final class ViewState: ObservableObject {
let dataObserver: DataObserver
init(dataObserver: DataObserver) {
self.dataObserver = dataObserver
}
@Published private(set) var status: Status = .loading
@Published private(set) var contacts: [Contact] = []
}
view raw ViewState.swift hosted with ❤ by GitHub

OK, this does not work. But since the ViewState has a simple init then why not slap nonisolated on the init and therefore not requiring the init to be called on a MainActor. This leads to a warning: “Main actor-isolated property ‘dataObserver’ can not be mutated from a non-isolated context; this is an error in Swift 6”. After digging in Swift forums to understand the error, I learned that as soon as init assigns the dataObserver instance to the MainActor guarded property, then compiler considers that the type is owned by the MainActor now. Since init is nonisolated, compiler can’t ensure that the assigned instance is not mutated by the non-isolated context.

final class ViewPresenter {
init(dataObserver: DataObserver) {
self.viewState = ViewState(dataObserver: dataObserver)
}
let viewState: ViewState
}
@MainActor final class ViewState: ObservableObject {
let dataObserver: DataObserver
nonisolated init(dataObserver: DataObserver) {
// Main actor-isolated property 'dataObserver' can not be mutated
// from a non-isolated context; this is an error in Swift 6
self.dataObserver = dataObserver
}
@Published private(set) var status: Status = .loading
@Published private(set) var contacts: [Contact] = []
}
view raw ViewState.swift hosted with ❤ by GitHub

This warning can be fixed by making the DataObserver type to conform to Sendable protocol which tells the compiler that it is OK, if the instance is mutated from different contexts (of course we need to ensure that the type really is thread-safe before adding the conformance). In this particular case, making the type Sendable was not possible, and I really did not want to go to the land of @unchecked Sendable, so I continued my research. Moreover, having nonisolated init looked like something what does not look right anyway.

Finally, I realized that since the ViewState is @MainActor, then I could make the viewState property @MainActor as well and delay creating the instance until the property is accessed. Makes sense since if I want to access the ViewState and interact with it then I need to be on the MainActor anyway. If the property is lazy var and created using a closure, then we achieve what we want: force the instance creation to MainActor. Probably, code speaks itself more clearly.

final class ViewPresenter {
private let viewStateBuilder: @MainActor () -> ViewState
init(dataObserver: DataObserver) {
self.viewStateBuilder = { ViewState(dataObserver: dataObserver) }
}
@MainActor lazy var viewState: ViewState = viewStateBuilder()
}
@MainActor final class ViewState: ObservableObject {
let dataObserver: DataObserver
init(dataObserver: DataObserver) {
self.dataObserver = dataObserver
}
@Published private(set) var status: Status = .loading
@Published private(set) var contacts: [Contact] = []
}
view raw ViewState.swift hosted with ❤ by GitHub

What I like is that I can keep one of the types fully @MainActor and still manage the creation from a non-isolated context. The downside is having lazy var and handling the closure.

If you want to try my apps, then grab one of the free offer codes for Silky Brew.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

One reply on “Initializing @MainActor type from a non-isolated context in Swift”