Swift Actors and old dependencies

593 Views Asked by At

I am learning swift and trying to use the new concurrency features introduced by Swift 5.5, and to make my code conform to the new requirements that will eventually be introduced by Swift 6.

My project uses SwiftUI & MVVM as well as some dependencies (GRDB and Supabase) to sync data between a local and a remote database. I read a lot on actors and as both of those dependencies are acting as "data managers" and accessed thorough my app I feel they need to be implemented using an actor. That might be my first mistake?

Both dependencies above make heavy use of classes that inherit one another and that do not conform to the sendable protocol which creates obvious issues when trying to turn my current implementation into actors.

As an example, my initial Supabase implementation was as follow:

actor SupabaseManager {
    private let supabaseKey: String = "A Key"
    private let supabaseURL: String = "A URL"

    internal let db: SupabaseClient

    init(supabaseKey: String?, supabaseURL: String?) {
        db = SupabaseClient(supabaseUrl: supabaseURL ?? self.supabaseURL, supabaseKey: supabaseKey ?? self.supabaseKey) //Cannot access property 'db' here in non-isolated initializer; this is an error in Swift 6
    }
}

After trying a couple of things (such as making my init async, which brought other obvious issues), I was able to successfully implement Supabase by making the db a Singleton and getting rid of the init all together, which I don't really like as I have been using dependency injection as much as possible but can live with.

actor SupabaseManager {
    private static let supabaseKey: String = "A Key"
    private static let supabaseURL: String = "A URL"

    internal static let db: SupabaseClient = .init(supabaseUrl: supabaseURL, supabaseKey: supabaseKey)
}

Even with a successful db variable implementation, I still run into other issues such as the below.

    func fetchSync<Data_Type>(lastUpdate: Update) async throws -> [Data_Type]
        where Data_Type: SyncRecord_Protocol
    {
        let query = db.database.from(Data_Type.databaseTableName)
            .select()
            .gt(column: Data_Type.lastUpdateColumnName, value: lastUpdate.timeStamp.formatted(.iso8601))
        do {
            let response = try await query.execute() //  Non-sendable type 'CountOption?' exiting actor-isolated context in call to non-isolated instance method 'execute(head:count:)' cannot cross actor boundary
                                                     //  Non-sendable type 'PostgrestResponse' returned by call from actor-isolated context to non-isolated instance method 'execute(head:count:)' cannot cross actor boundary
            let data = try response.decoded(to: [Data_Type].self, using: supabaseDecoder())
            return data
        }
        catch {
            throw error
        }
    }

One way I found to make it work is to make the database function nonisolated and to only make it accessible through some sort of wrapping function such as what is shown below. This does not throw any error, but I am not sure if I am once again defeating the purpose of the actor

    func fetchSyncWrapper<DataType>(lastUpdate: Update) async throws -> [DataType]
        where DataType: SyncRecord_Protocol
    {
        return try await fetchSync(lastUpdate: lastUpdate)
    }

    nonisolated func fetchSync<Data_Type>(lastUpdate: Update) async throws -> [Data_Type]
        where Data_Type: SyncRecord_Protocol
    { ... }

Questions I now have are:

  1. Is it even possible to adapt an older class type dependency to work with an actor, or if I have to wait for an update of the package I am using?
  2. I though of making an extension to the Supabase client class to make it @unchecked Sendable, but I feel it defeats the purpose of the whole actor thing?
1

There are 1 best solutions below

0
On

Thanks for your answers.

I ended up doing the following:

  1. I found out GRDB had already updated their package and created a GRDBSendable type. As the types introduced by GRDB are conform to Sendable, I didn't have any issue.
  2. Supabase is behind on their implementation of Sendable, and they use a lot of subclasses related to postgREST. I made the SupabaseClientclass @unchecked Sendable to facilitate the init of my wrapper. I then made an isolated function that wraps a nonisolated one to wrap everything in an actor and to make it a Sendable that can be used in .task and along with other concurrency features.