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:
- 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?
- 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?