Swift和Interface Builder中的单例

时间:2019-01-28 11:05:59

标签: swift macos cocoa interface-builder cocoa-bindings

背景

我的应用程序中有一个单例类,该类根据this blog post中的一行单例(带有私有init())进行声明。具体来说,它看起来像这样:

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

    private init() {
    }
}

我想将aProperty的状态绑定到菜单项是否隐藏。

我如何解决该问题

这是我执行此操作的步骤:

  1. 转到Interface Builder中的对象库,然后向我的Application场景添加一个通用的“ Object”。在身份检查器中,将“类”配置为Singleton

  2. 通过Ctrl-从Interface Builder中的singleton对象拖动到我的App Delegate代码中,在我的App Delegate中创建引用出口。最终看起来像这样:

@IBOutlet weak var singleton: Singleton!
  1. 转到“绑定”检查器的菜单项,在“可用性”下选择“隐藏”,选中“绑定到”,在其前面的组合框中选择“单身”,然后在“模型密钥路径”。

问题

不幸的是,这是行不通的:更改属性不会影响所涉及的菜单项。

调查原因

问题似乎是,尽管将aProperty声明为私有,但Interface Builder仍在设法创建我的单例的另一个实例。为了证明这一点,我将init()添加到私有NSLog("singleton init")方法中,并将以下代码添加到应用委托中的init()中:

applicationDidFinishLaunching()

当我运行该应用程序时,这将在日志中输出:

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

因此,确实存在两个不同的实例。我还将此代码添加到了我的应用程序委托中的其他位置:

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

在某一点上,将产生以下输出:

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

很显然,作为一个单例,所有值都应该匹配,但是NSLog("aProperty: [\(singleton!.aProperty),\(String(describing:singleton!.value(forKey: "aProperty"))),\(Singleton.sharedInstance.singleton),\(String(describing:Singleton.sharedInstance.value(forKey: "aProperty")))] hidden: \(myMenuItem.isHidden)") 产生一个输出,而singleton产生另一个输出。可以看出,对Singleton.sharedInstance的调用与它们各自的对象匹配,因此KVC不应成为问题。

问题

我如何在Swift中声明一个单例类,并将其与Interface Builder连接起来,以避免实例化两次?

如果这不可能,那么我将如何解决将全局属性绑定到Interface Builder中的控件的问题?

是否需要MCVE?

我希望描述足够详细,但是如果有人认为有必要使用MCVE,请发表评论,我将创建一个评论并上传到GitHub。

3 个答案:

答案 0 :(得分:4)

我只想通过声明单身人士不应用于共享全局状态来开始我的回答。尽管一开始它们似乎较容易使用,但它们以后往往会引起很多麻烦,因为可以在任何地方进行虚拟更改,这有时会使您的程序变幻莫测。

话虽如此,要实现您的需求并非不可能,但要有一点仪式:

@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 }
    }
}

答案 1 :(得分:1)

很遗憾,您无法在Swift中返回与init不同的实例。 以下是一些可能的解决方法:

  • 在Interface Builder中为类的实例创建出口,然后仅在整个代码中引用该实例。 (本身不是单例,但是您可以添加一些运行时检查,以确保仅从nib文件而不是从代码实例化它。)
  • 创建一个在Interface Builder中使用的帮助程序类,并将您的单例作为其属性公开。即该帮助程序类的任何实例将始终返回单例的单个实例。
  • 为您的Swift单例类创建一个Objective-C子类,并使它的init始终返回共享的Swift单例实例。

答案 2 :(得分:1)

在我的特殊情况下,有一种解决问题的方法。

回想一下我仅想根据此单例中aProperty的状态来隐藏和取消隐藏菜单的问题。当我尝试避免编写尽可能多的代码时,通过在Interface Builder中进行所有操作,似乎在这种情况下,以编程方式编写绑定的麻烦要小得多:

menuItem.bind(NSBindingName.hidden, to: Singleton.sharedInstance, withKeyPath: "aProperty", options: nil)