Swift - Singleton without global access
Clash Royale CLAN TAG#URR8PPP
Swift - Singleton without global access
I want to create a Swift Singleton without global access. The pattern which I want to create is to assure that always just one instance of a class exists but this class should not be accessible over the usual global MyClass.shared
syntax. The reason for this is that I want the class to be fully and correctly testable (which is not really possible with global Singletons). I will then use dependency injection to pass the single instance from viewcontroller to viewcontroller. So the "access" issue is solved without a global static instance.
MyClass.shared
What I could do is to do basically - nothing. Just create a normal class and trust on the discipline of all developers to not instantiate this class again and again but use it only injected as a dependency. But I would rather have some compiler enforced pattern which prohibits this.
So the requirement is:
My first attempt to solve this was something like this:
class WebService
private static var instances = 0
init()
assertSingletonInstance()
private func assertSingletonInstance()
#if DEBUG
if UserDefaults.standard.bool(forKey: UserDefaultsKeys.isUnitTestRunning.rawValue) == false
WebService.instances += 1
assert(WebService.instances == 1, "Do not create multiple instances of this class. Get it thru the shared dependencies in your module.")
#endif
Remark: passing an argument during launch creates a user defaults value which can be checked during runtime. This is how I know that the current run is a unit test.
Generally this pattern works quite good. My only problem with this - I have to duplicate this code over and over again for every possible singleton. Which is not nice. I would prefer a reusable solution.
One solution for this was to create a Protocol Extension:
protocol Singleton
static var instances: Int get set
func assertSingletonInstance()
extension Singleton
// Call this assertion in init() to check for multiple instances of one type.
func assertSingletonInstance()
if UserDefaults.standard.bool(forKey: UserDefaultsKeys.isUnitTestRunning.rawValue) == false
Self.instances += 1
assert(Self.instances == 1, "Do not create multiple instances of this class. Get it thru the shared dependencies in your module.")
#endif
And then use it in this way:
class WebService: Singleton )
static var instances = 0
init()
assertSingletonInstance()
The problem with this approach is that the instances
variable is not private
. So someone could just set this variable to 0 before instantiating the class and the check would not work anymore.
instances
private
The other attempt was a Singleton
base class. In this case a private static var instances
can be used.
Singleton
private static var instances
class Singleton
private static var instances = 0
required init()
assertSingletonInstance()
private func assertSingletonInstance()
#if DEBUG
if UserDefaults.standard.bool(forKey: UserDefaultsKeys.isUnitTestRunning.rawValue) == false
Singleton.instances += 1
assert(Singleton.instances == 1, "Do not create multiple instances of this class. Get it thru the shared dependencies in your module.")
#endif
The problem with this approach is - it does not work. Incrementing Singleton.instance
adds 1 to the static instances
of the Singleton
type and not to the class which derives from the Singleton
base class.
Singleton.instance
static instances
Singleton
Singleton
Now I am left with either doing nothing and relying on the discipline and understanding of all developers or at least use the protocol extension with internal
or public
access.
internal
public
A sample implementation can be found here.
Maybe someone has better ideas for a really clean solution to this problem. I appreciate any hints or a discussion about it. Thanks.
internal
instance
2 Answers
2
You can use an atomic flag (for thread safety) to mark the singleton as being instantiated:
class Singleton
static private var hasInstance = atomic_flag()
init()
// use precondition() instead of assert() if you want the crashes to happen in Release builds too
assert(!atomic_flag_test_and_set(&type(of: self).hasInstance), "Singleton here, don't instantiate me more than once!!!")
deinit
atomic_flag_clear(&type(of: self).hasInstance)
You mark the singleton as allocated in init
, and you reset the flag in deinit
. This allows you on one hand to have only one instance (if the original instance doesn't get deallocated), and on the other hand to have multiple instances, as long as they don't overlap.
init
deinit
App code: assuming that you'll keep a reference to the singleton, somewhere, that you inject downstream, then deinit
should never be called, which leads to only one possible allocation.
deinit
Unit testing code: if the unit tests properly do the cleanup (the tested singleton gets deallocated after every test), then there will be only one living instance at a certain point in time, which won't trigger the assertion failure.
You would need to synchronize that, because you could end up with multiple objects due to the race condition
– Alexander
yesterday
Good point, @Alexander, updated the answer to include thread safety.
– Cristik
yesterday
That's awesome, thanks!
– Darko
10 hours ago
In response to Cristik's answer:
This is a really nice solution! The type(of: self)
solves the base class problem. And releasing the thing in deinit is a great idea to allow the whole thing in unit tests. You are right - I keep the references of all the Singletons "upstream" and inject them afterwards. Perfect.
type(of: self)
I have created a template based on this ideas with a serial queue for the possible race condition problem. I assume this is a better solution then the atomic_flag and more "Swiftish".
Playground code:
import Foundation
class Singleton
static private var instances = 0
// Sync the access to instances
private var serialQueue = DispatchQueue(label: "com.yourcompany.app.singletoncheck")
init()
serialQueue.sync
type(of: self).instances += 1
assert(type(of: self).instances == 1, "Do not create multiple instances of this class living at the same time.")
deinit
type(of: self).instances = 0
class Derived: Singleton
var a: Derived? = Derived()
//a = nil // release to prevent the assertion from failing
var b: Derived? = Derived() // assertion fails here, works!
And here is an even more interesting solution which can be used everywhere without any special knowledge and no assertions at all. It uses a failable initializer.
Playground code:
import Foundation
class Singleton
static private var instances = 0
// Sync the access to instances
let serialQueue = DispatchQueue(label: "com.yourcompany.app.singletoncheck")
// This failable initializer assures that at the same time only one instance of this class exists.
init?()
var singleInstance = false
serialQueue.sync
type(of: self).instances += 1
if type(of: self).instances == 1
singleInstance = true
if !singleInstance
return nil
deinit
serialQueue.sync
type(of: self).instances = 0
class Derived: Singleton
var a = 0
func increment()
serialQueue.sync
a += 1
print(a)
var a = Derived()
a?.increment() // call to synchonized version of increment
//a = nil //either a or b is alive
var b = Derived()
print (a) //prints Optional(__lldb_expr_15.Derived)
print (b) //prints nil
This is in my opinion the "real" Singleton, described by the Gang of four. Global access was only an implementation detail at that time.
So in comparison to the usual Singleton pattern it:
So it has all the advantages of a Singleton, but without the usual problems.
Note that you also need to ensure thread safetiness in deinit.
– Cristik
9 hours ago
Actually, on second thought, you might not need it, since you'd only end up in unit tests to execute
deinit
, and unit tests usually run on main.– Cristik
9 hours ago
deinit
By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.
What if you put all your dependencies in a separate module, which builds a library/framework, with only a single exposed "Globals" protocol, with a default instance. The globals protocol provides access to all dependencies (db, web apis, w/e), which could all have
internal
initializers. From there, it would be easy to make a "MockGlobals" object, which is set to that globalinstance
– Alexander
yesterday