I am trying to create a Data Access Layer in SwiftUI. I have the following implementation. But for @Entry line it keeps saying "Call to main actor-isolated initializer 'init(container:)' in a synchronous nonisolated context". What can I do to resolve this issue so I can access dataAccess in my view.
import Foundation
import SwiftData
@MainActor
protocol DataAccess {
func getBudgets() throws -> [BudgetPlain]
func addBudget(name: String, limit: Double)
}
@Entry var dataAccess: DataAccess = BudgetSwiftDataAccess()
@MainActor
class BudgetSwiftDataAccess: DataAccess {
var container: ModelContainer
var context: ModelContext
init(container: ModelContainer = try! ModelContainer(for: Budget.self, configurations: ModelConfiguration(isStoredInMemoryOnly: false))) {
self.container = container
self.context = container.mainContext
}
func getBudgets() throws -> [BudgetPlain] {
let budgets = try context.fetch(FetchDescriptor<Budget>())
return budgets.map(BudgetPlain.init)
}
func addBudget(name: String, limit: Double) {
let budget = Budget(name: name, limit: limit)
context.insert(budget)
}
}
UPDATE:
Still having the same issues with actor-isolated.
extension EnvironmentValues {
@Entry var dataAccess: DataAccess = BudgetSwiftDataAccess()
}
@MainActor
class BudgetSwiftDataAccess: DataAccess {
var container: ModelContainer
var context: ModelContext
@MainActor
init(container: ModelContainer = ModelContainer.default()) {
self.container = container
self.context = container.mainContext
}
func getBudgets() throws -> [BudgetPlain] {
let budgets = try context.fetch(FetchDescriptor<Budget>())
return budgets.map(BudgetPlain.init)
}
func addBudget(name: String, limit: Double) {
let budget = Budget(name: name, limit: limit)
context.insert(budget)
}
}
I recall, that there was an issue with the macro @Entry
. I fear, for now, you need to not use the @Entry
macro for values which are associated to a global actor.
So, I would recommend to declare the environment value the "old" way:
@MainActor
protocol DataAccess {
func getBudgets() throws -> [BudgetPlain]
func addBudget(name: String, limit: Double)
}
@MainActor
class BudgetSwiftDataAccess: DataAccess {
...
}
Then, explicitly declare the defaultValue
and the environment value (dataAccess
) on the MainActor:
struct DataAccessKey: EnvironmentKey {
@MainActor static let defaultValue: any DataAccess = BudgetSwiftDataAccess()
}
extension EnvironmentValues {
@MainActor var dataAccess: DataAccessKey {
get { self[DataAccessKey] }
set { self[DataAccessKey] = newValue }
}
}
** Note **
You probably want to use an optional for the default value and use nil
as the default in your use case!
As Sweeper pointed out, this code will not compile with Swift 6. The main reason for this is, that the compiler is now very strict, when it comes to initialising global variables - which needs to be explicitly made thread-safe.
To make the code valid for Swift 6, we can declare the environment value Optional, and additionally we have to declare any concrete environment value conforming to DataAccess
also conforming to Sendable
:
@MainActor
protocol DataAccess: Sendable {
...
}
struct DataAccessKey: EnvironmentKey {
static let defaultValue: (any DataAccess)? = nil
}
extension EnvironmentValues {
var dataAccess: (any DataAccess)? {
get { self[DataAccessKey] }
set { self[DataAccessKey] = newValue }
}
}
Note, the defaultValue
is a "non-isolated requirement" given by the EnvironmentKey
protocol, which means, it needs to be valid to initialise the global defaultValue
when running on any thread. We cannot instantiate a DataAccess
instance here, since it is declared to be isolated on the MainActor. However, it's safe to assign a nil
value. Since the default value will be assigned non-isolated, the concrete value crosses isolation boundaries, and thus needs to conform to Sendable (Swift 6).
The code above also compiles with Swift 5.9.
For Swift 6 only
For Swift 6, we can use the macro @Entry
, which really provides a nice solution:
We do not need to declare DataAccess
conform to Sendable
for this case (it may need to for other reasons, though). And, we can assign a default value, which is not nil:
@MainActor
protocol DataAccess {
...
}
@MainActor
class BudgetSwiftDataAccess: DataAccess {
...
}
extension EnvironmentValues {
@Entry var dataAccess: any DataAccess = BudgetSwiftDataAccess()
}
Note, macro @Entry
is available since iOS 15.0, macOS 12, but the above code requires the Swift 6 compiler. It fails with Swift 5.9.
If you use an optional environment value you can also use macro @Entry
with Swift 5.9:
extension EnvironmentValues {
@Entry var dataAccess: (any DataAccess)? = nil
}
IMHO, this is probably the way you should go.
Many thanks to Sweeper, pointing out that the original answer does not compile with Swift 6.