Search code examples
swiftmacoscocoainterface-buildercocoa-bindings

Singletons in Swift and Interface Builder


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.


Solution

  • 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)