nburk nburk - 2 months ago 8
Swift Question

How to implement Singleton that takes data on initilisation in Swift?

In this post, it is very nicely explained how Singletons should be implemented in Swift, essentially it can be done with two lines:

class TheOneAndOnlyKraken {
static let sharedInstance = TheOneAndOnlyKraken()
private init() {} //This prevents others from using the default '()' initializer for this class.
}


However, what happens if my Singleton is supposed to be initalised with some data? Maybe it needs to encapsulate an API Key or other data that it can only receive from the outside. An example could look as follows:

class TheOneAndOnlyKraken {
let secretKey: String
static let sharedInstance = TheOneAndOnlyKraken()
private init() {} //This prevents others from using the default '()' initializer for this class.
}


In that situation, we can't make the initializer private because we will have to create an initializer that takes a
String
as an argument to satisfy the compiler:

init(secretKey: String) {
self.secretKey = secretKey
}


How can that be saved and we still make sure that we have a thread-safe instantiation of the singleton? Is there a way how we can avoid using
dispatch_once
or would we have to essentially default back to the Objective-C way where we use
dispatch_once
to make sure that the initializer indeed only gets called once?

Answer

First, note that the ObjC way you're implying is not thread-correct. It may be "safe" in that it doesn't crash and does not generate undefined behavior, but it silently ignores subsequent initializations with differing configuration. That is not expected behavior. Readers that are known to occur after the write will not receive the written data. That fails consistency. So put aside theories that such a pattern was correct.

So what would be correct? Correct would be something like this:

import Dispatch

class TheOneAndOnlyKraken {
    static let sharedInstanceQueue: DispatchQueue = {
        let queue = DispatchQueue(label: "kraken")
        queue.suspend()
        return queue
    }()

    private static var _sharedInstance: TheOneAndOnlyKraken! = nil
    static var sharedInstance: TheOneAndOnlyKraken {
        var result: TheOneAndOnlyKraken!
        sharedInstanceQueue.sync {
            result = _sharedInstance
        }
        return result
    }

    // until this is called, all readers will block
    static func initialize(withSecret secretKey: String) {
        // It is a programming error to call this twice. If you want to be able to change
        // it, you'll need another queue at least.
        precondition(_sharedInstance == nil)
        _sharedInstance = TheOneAndOnlyKraken(secretKey: secretKey)
        sharedInstanceQueue.resume()
    }

    private var secretKey: String
    private init(secretKey: String) {
        self.secretKey = secretKey
    }
}

This requires a single explicit call to TheOneAndOnlyKraken.intialize(withSecret:). Until someone makes that call, all requests for sharedInstance will block. A second call to initialize will crash.

Comments