3

Background

I have a singleton class in my app, declared according following the one line singleton (with a private init()) in this blog post. Specifically, it looks like this:

@objc class Singleton {
    static let Singleton sharedInstance = Singleton()
    @objc dynamic var aProperty = false

    private init() {
    }
}

I would like to bind the state of aProperty to whether a menu item is hidden.

How I tried to solve the problem

Here are the steps I followed to do this:

  1. Go to the Object Library in Interface Builder and add a generic "Object" to my Application scene. In the Identity inspector, configure "Class" to Singleton.

  2. Create a referencing outlet in my App Delegate by Ctrl-dragging from the singleton object in Interface Builder to my App Delegate code. It ends up looking like this:

@IBOutlet weak var singleton: Singleton!
  1. Go to the Bindings inspector for the menu item, choose "Hidden" under "Availability", check "Bind to", select "Singleton" in the combo box in front of it, and type aProperty under "Model Key Path".

The issue

Unfortunately, this doesn't work: changing the property has no effect on the menu item in question.

Investigating the cause

The issue appears to be that, despite declaring init() as private, Interface Builder is managing to create another instance of my singleton. To prove this, I added NSLog("singleton init") to the private init() method as well as the following code to applicationDidFinishLaunching() in my app delegate:

NSLog("sharedInstance = \(Singleton.sharedInstance) singleton = \(singleton)")

When I run the app, this is output in the logs:

singleton init
singleton init
sharedInstance = <MyModule.Singleton: 0x600000c616b0> singleton = Optional(<MyModule.Singleton: 0x600000c07330>)

Therefore, there are indeed two different instances. I also added this code somewhere else in my app delegate:

NSLog("aProperty: [\(singleton!.aProperty),\(String(describing:singleton!.value(forKey: "aProperty"))),\(Singleton.sharedInstance.singleton),\(String(describing:Singleton.sharedInstance.value(forKey: "aProperty")))] hidden: \(myMenuItem.isHidden)")

At one point, this produces the following output:

aProperty: [false,Optional(0),true,Optional(1)] hidden: false

Obviously, being a singleton, all values should match, yet singleton produces one output and Singleton.sharedInstance produces a different one. As can be seen, the calls to value(forKey:) match their respective objects, so KVC shouldn't be an issue.

The question

How do I declare a singleton class in Swift and wire it up with Interface Builder to avoid it being instantiated twice?

If that's not possible, how else would I go about solving the problem of binding a global property to a control in Interface Builder?

Is an MCVE necessary?

I hope the description was detailed enough, but if anyone feels an MCVE is necessary, leave a comment and I'll create one and upload to GitHub.

swineone
  • 2,296
  • 1
  • 18
  • 32
  • 1
    Even if you say that your class is a singleton, by adding a new `Object` to storyboard you are not referencing that singleton. You are *creating a new instance*. Actually, I am pretty sure you cannot do that. You cannot reference an object created in code in your storyboards. – Sulthan Jan 28 '19 at 11:15
  • @Sulthan I thought the way the class was declared would preclude that, due to `init()` being private. – swineone Jan 28 '19 at 11:17
  • 1
    Interface Builder creates objects using Objective-C and you cannot really enforce private init there. Even if you could, you would just get an error when loading the storyboard. – Sulthan Jan 28 '19 at 11:22
  • That's too bad, I had no idea. Can you suggest a different design in this case? Maybe if I made `aProperty` `static`? – swineone Jan 28 '19 at 11:23
  • It's been a few years, but I think [I hacked around IB duplicating singletons by using `allocWithZone` and `copyWithZone`](https://stackoverflow.com/questions/22871948/why-not-enforce-strict-singleton-application-delegate-object-to-use-in-nibs) – stevesliva Jan 28 '19 at 15:00

3 Answers3

4

I just want to start my answer by stating that singletons should not be used for sharing global state. While they might seem easier to use in the beginning, they tend to generate lots of headaches later on, since they can be changed virtually from any place, making your program unpredictable some times.

That being said, it's not impossible to achieve what you need, but with a little bit of ceremony:

@objc class Singleton: NSObject {
    // using this class behind the scenes, this is the actual singleton
    class SingletonStorage: NSObject {
        @objc dynamic var aProperty = false
    }
    private static var storage = SingletonStorage()

    // making sure all instances use the same storage, regardless how
    // they were created
    @objc dynamic var storage = Singleton.storage

    // we need to tell to KVO which changes in related properties affect
    // the ones we're interested into
    override class func keyPathsForValuesAffectingValue(forKey key: String) -> Set<String> {
        switch key {
        case "aProperty":
            return ["storage.aProperty"]
        default: return super.keyPathsForValuesAffectingValue(forKey: key)
        }

    }

    // and simply convert it to a computed property
    @objc dynamic var aProperty: Bool {
        get { return Singleton.storage.aProperty }
        set { Singleton.storage.aProperty = newValue }
    }
}
Cristik
  • 30,989
  • 25
  • 91
  • 127
  • The property in question tracks whether I was able to connect to a helper tool, and I think it's a good use of the singleton pattern. It's only updated by code inside the class, it can't be set by other code. With that said, it seems that rather than trying to coerce IB into accepting a singleton, [my other answer](https://stackoverflow.com/a/54410108/523079) ends up being the cleanest solution in this particular case, in my opinion. – swineone Jan 28 '19 at 21:10
  • This would be perfect if you delete everything after the first line! ;-) In any case, your `Singleton` class is really misnamed, since it's not a singleton at all. You could call it `SingletonProxy` or `FauxSingleton` or something like that. – Caleb Jan 28 '19 at 21:10
  • 1
    @Caleb I agree, I just used the same names as in the question. BTW, I could've add a `shared` property and make the `init` private, left them outside to not clutter the solution code. – Cristik Jan 28 '19 at 21:12
  • @swineone can't argue about cleanliness, after all the solution you found has only one line ;) – Cristik Jan 28 '19 at 21:13
1

Unfortunately you can’t return a different instance from init in Swift. Here are some possible workarounds:

  • Make an outlet for an instance of your class in Interface Builder and then only reference that instance throughout your code. (Not a singleton per se, but you could add some runtime checks to make sure it’s only instantiated from a nib file and not from code).
  • Create a helper class for use in Interface Builder and expose your singleton as its property. I.e. any instance of that helper class will always return a single instance of your singleton.
  • Make an Objective-C subclass of your Swift singleton class and make its init's always return a shared Swift singleton instance.
pointum
  • 2,987
  • 24
  • 31
  • To be honest, you can return a different instance from `init` in Swift with some ugly but permitted workaround but I would avoid it because it's not a good idea. And I would consider *that* unfortunate. It's always unfortunate to break type system, even a little. – Sulthan Jan 28 '19 at 21:00
  • Let me add that I used option 2 (create a helper class for use in Interface Builder) for a different class, with a lot of properties, where I didn't want to bind each manually as per [my answer](https://stackoverflow.com/a/54410108/523079). – swineone Jan 29 '19 at 11:45
1

There is a way around the problem in my particular case.

Recall from the question that I only wanted to hide and unhide a menu according to the state of aProperty in this singleton. While I was attempting to avoid write as much code as possible, by doing everything in Interface Builder, it seems in this case it's much less hassle to just write the binding programmatically:

menuItem.bind(NSBindingName.hidden, to: Singleton.sharedInstance, withKeyPath: "aProperty", options: nil)
swineone
  • 2,296
  • 1
  • 18
  • 32