Swift 和 Interface Builder 中的单例

Singletons in Swift and Interface Builder

背景

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

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

    private init() {
    }
}

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

我是如何尝试解决问题的

以下是我执行此操作所遵循的步骤:

  1. 转到 Interface Builder 中的对象库并向我的应用程序场景添加一个通用 "Object"。在身份检查器中,将 "Class" 配置为 Singleton.

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

@IBOutlet weak var singleton: Singleton!
  1. 转到菜单项的绑定检查器,选择 "Availability" 下的 "Hidden",选中组合框中的 "Bind to"、select "Singleton"在它前面,然后在 "Model Key Path".
  2. 下键入 aProperty

问题

不幸的是,这不起作用:更改 属性 对相关菜单项没有影响。

调查原因

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

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

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

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

因此,确实存在两种不同的情况。我还在我的应用程序委托中的其他地方添加了这段代码:

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

在某一时刻,这会产生以下输出:

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

显然,作为单例,所有值都应该匹配,但 singleton 产生一个输出而 Singleton.sharedInstance 产生不同的输出。可以看出,对 value(forKey:) 的调用匹配它们各自的对象,因此 KVC 应该不是问题。

问题

如何在 Swift 中声明单例 class 并将其与 Interface Builder 连接起来以避免被实例化两次?

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

是否需要 MCVE?

我希望描述足够详细,但如果有人觉得 MCVE 是必要的,请发表评论,我会创建一个并上传到 GitHub。

不幸的是,您不能 return 与 Swift 中的 init 不同的实例。 以下是一些可能的解决方法:

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

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

回想一下我在这个单例中只想根据 aProperty 的状态隐藏和取消隐藏菜单。虽然我试图通过在 Interface Builder 中完成所有操作来尽量避免编写尽可能多的代码,但在这种情况下,以编程方式编写绑定似乎要简单得多:

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

我只是想通过说明单例不应该用于共享全局状态来开始我的回答。虽然它们在开始时看起来更容易使用,但它们往往会在以后产生很多令人头疼的问题,因为它们几乎可以从任何地方进行更改,有时会使您的程序变得不可预测。

话虽这么说,要达到你的要求也不是不可能,但要有一点仪式感:

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