swiftdata - @Entry Keyword in SwiftUI and Actor-Isolated Initializer - Stack Overflow

I am trying to create a Data Access Layer in SwiftUI. I have the following implementation. But for @Ent

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 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)
    }
    
}
Share Improve this question edited Mar 27 at 16:05 Joakim Danielson 52.2k5 gold badges33 silver badges71 bronze badges asked Mar 27 at 14:06 john doejohn doe 9,72223 gold badges94 silver badges178 bronze badges 7
  • @Entry should not be used at the top level. I assume this is in an EnvironmentValues extension? Surely you don't want to use the actual model container as the default value for this environment value? – Sweeper Commented Mar 27 at 14:14
  • Thanks! Yes. I can pass in container: .preview in BudgetSwiftDataAccess but still how do I make sure that Entry line is not giving me any issues but Swift actors etc. – john doe Commented Mar 27 at 14:15
  • Actually it is ;) You need to extension EnvironmentValues { @Entry var dataAccess: DataAccess = BudgetSwiftDataAccess() } – CouchDeveloper Commented Mar 27 at 14:15
  • Thanks @CouchDeveloper. Totally fot about adding extension part. Unfortunately it still gives error regarding call to main actor-isolated initializer etc. – john doe Commented Mar 27 at 14:17
  • 1 Ah, I now recall that there was an issue. I put an answer for your issue. Can you please check? – CouchDeveloper Commented Mar 27 at 14:46
 |  Show 2 more comments

2 Answers 2

Reset to default 1

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!

Update Swift 6

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.

The default value for an environment value should not be your actual model container. You should create the real thing separately, and inject it into the environment with .environment(\.dataAccess, ...).

You can simply create a "dummy" implementation of the DataAccess protocol, and use that as the default value.

struct DummyDataAccess: DataAccess {
    func getBudgets() throws -> [BudgetPlain] { [] }
    func addBudget(name: String, limit: Double) { }
    
    // a 'nonisolated init() {}' is implicitly declared here...
}
extension EnvironmentValues {
    @Entry var dataAccess: any DataAccess = DummyDataAccess()
}

Also consider using nil as the default value. This way, the view can know that the environment value has not been injected properly, instead of silently doing nothing.

extension EnvironmentValues {
    @Entry var dataAccess: (any DataAccess)? = nil
}

发布者:admin,转转请注明出处:http://www.yc00.com/questions/1744082757a4555607.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信