Suppose I have a class like this in a shared library I’m building:
final class ModelContext
{
func save() {
…
}
}
This thing serves the same role as SwiftData’s context: it manages a connection to a database and translates an object graph to and from the database representation.
It is always tied to an Actor. In one case that’s the MainActor so that the UI can fetch/update model objects, like this:
@MainActor
final class ModelController
{
// inherits MainActor
var context: ModelContext
}
And in some cases, it’ll be used on a specific actor for long-running background tasks like imports or exports:
actor BackgroundModelActor
{
var context: ModelContext
}
How do I express to Swift that ModelContext will be isolated to some actor that’s known at compile time and that code like this should inherit that actor’s isolation? —>
final class ModelContext
{
var autosaveTask: Task<Void, Never>? = nil
func save() {
…
}
init() {
// Nooooooope: sending closure risks disaster, etc.
autosaveTask = Task { [weak self] in
while !Task.isCancelled {
self?.save()
// sleep for a bit, etc.
}
}
}
}
I’ve seen the isolation inheritance pitch, but that seems to apply to functions. I need something similar for the class declaration: “every ModelContext is bound to some Actor, so inherit that Actor when you run the timer task.”
final class ModelContext {
var autoSaveTask: Task<Void, any Error>?
func save() {}
func startSaving(each interval: ContinuousClock.Duration,
isolation: isolated (any Actor)? = #isolation) {
precondition(autoSaveTask == nil)
autoSaveTask = Task {
while !Task.isCancelled {
try await Task.sleep(for: interval)
// prints either MainActor or BackgroundModelActor
print(isolation ?? "not isolated")
save()
}
}
}
}
actor BackgroundModelActor {
let context = ModelContext()
func startSavingContext() {
context.startSaving(each: .milliseconds(100))
}
}
@MainActor
final class ModelController {
let context = ModelContext()
func startSavingContext() {
context.startSaving(each: .milliseconds(200))
}
}
Is this the behavior you are looking for? Either way I don't think you can capture self in a property initializer so your example will not compile as is. You can set up the task at initialization if you want though (by passing isolation to the init).
@jameesbrown Yea, that works, but it forces the thing using ModelContext to start the timer. Your suggestion is a little neater than what I did (just moved the whole Task out to ModelController or BackgroundModelActor.)
I was hoping for something self-contained, if that makes sense, because there may be other places in ModelContext where I want to use Concurrency. If I were to do this:
@MainActor
final class ModelContext {
…
}
Then the compiler knows everything is Main-Actor bound and I don’t have concurrency errors about non-Sendable stuff.
What I want is the equivalent of:
@AnyActor
final class ModelContext {
…
}
Meaning, “It doesn’t have to be the MainActor, necessarily, but this class must be isolated to SOME Actor and everything should go through that Actor.” But I take it this isn’t possible right now.
I don't think you can capture self in a property initializer so your example will not compile as is.
Yea, I shortened it because I’m typing on an iPhone. The property is declared as Task<Void, Never>? and the task itself is assigned during init after self is all ready to go.
It doesn’t compile, but that’s because I can’t tell the compiler to inherit the Actor where the ModelContext instance was initialized, so the Task is just going to run on any of the cooperative threads, which would indeed be a mess.