Is this @MainActor singleton implementation considered clean?

Hey everyone,

I’m wondering if this is a clean or recommended way to implement a @MainActor singleton in Swift:

@MainActor
class Singleton {

    nonisolated static let a = Singleton()

    nonisolated init() {
    }
  
    func updateUI() {
        print("UI updated on main thread")
    }
}

I noticed that if I don’t provide a custom init and just use the default one, that initializer is also nonisolated.

The problem I’m running into is that I’m trying to use dependency injection inside an actor that depends on this singleton, and I’m not sure if this pattern is correct or could lead to issues with actor isolation.

So my questions are:
→ Is this pattern considered “clean” or idiomatic when using @MainActor?
→ Is there a better approach for defining a @MainActor singleton when I also need dependency injection inside another actor?

Here’s a simplified example:

@MainActor
class Singleton { }

actor Test {
    init(a: Singleton = Singleton()) { }
}

:white_check_mark: This compiles just fine.
But if I add a custom initializer:

@MainActor
class Singleton {
    init() {}
}

actor Test {
    init(a: Singleton = Singleton()) { }
}

:cross_mark: Now it doesn’t compile anymore.
From what I understand, that means the custom init becomes nonisolated by default.

However, if I make the initializer explicitly nonisolated, it works again:

@MainActor
class Singleton {
    nonisolated init() {}
}

actor Test {
    init(a: Singleton = Singleton()) { }
}

I want to know if that’s actually a clean or safe approach.
In my case, the singleton holds shared, immutable state across the app, but its methods need to run on the main actor (for example, UI updates).

I am using swit 6 with strict concurent
Thanks!

With strict concurrency, you can assume that if the compiler does not error (and you’re not using unsafe or @unchecked constructs), that it’s relatively safe to do this. However, if you start e.g. calling into @MainActor code from within that nonisolated init, you’ll need to mark it @MainActor. If that happens, you may need to update the Test initializer to be async and explicitly await the Singleton init, e.g.

actor Test {
    init(a: Singleton? = nil) async {
        let realA = a ?? await Singleton()
    }
}

Marking the initializer async will force other callers initializing your actor to wait for the hop onto the main actor.

1 Like

I cannot say anything about concurrency, but regarding the question "Is ... singleton implementation considered clean?" I can say the following:

The whole point of a singleton is to have just one instance of it. I believe this is why you have the nonisolated static let a = Singleton() property in the Singleton class. This single instance is the one that should be used outside the class, e.g. Singleton.a, and not Singleton(), which creates a new instance of the class. I would rename the a property to shared, or something like instance or singleInstance, or sharedInstance, and make init private to prohibit creating instances of the Singleton class outside the class itself.

3 Likes