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
的状态绑定到菜单项是否隐藏。
我是如何尝试解决问题的
以下是我执行此操作所遵循的步骤:
转到 Interface Builder 中的对象库并向我的应用程序场景添加一个通用 "Object"。在身份检查器中,将 "Class" 配置为 Singleton
.
通过按住 Ctrl 从 Interface Builder 中的单例对象拖动到我的 App Delegate 代码,在我的 App Delegate 中创建一个引用出口。它最终看起来像这样:
@IBOutlet weak var singleton: Singleton!
- 转到菜单项的绑定检查器,选择 "Availability" 下的 "Hidden",选中组合框中的 "Bind to"、select "Singleton"在它前面,然后在 "Model Key Path".
下键入 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 }
}
}
背景
我的应用程序中有一个单例 class,根据 this blog post 中的一行单例(带有私有 init()
)声明。具体来说,它看起来像这样:
@objc class Singleton {
static let Singleton sharedInstance = Singleton()
@objc dynamic var aProperty = false
private init() {
}
}
我想将 aProperty
的状态绑定到菜单项是否隐藏。
我是如何尝试解决问题的
以下是我执行此操作所遵循的步骤:
转到 Interface Builder 中的对象库并向我的应用程序场景添加一个通用 "Object"。在身份检查器中,将 "Class" 配置为
Singleton
.通过按住 Ctrl 从 Interface Builder 中的单例对象拖动到我的 App Delegate 代码,在我的 App Delegate 中创建一个引用出口。它最终看起来像这样:
@IBOutlet weak var singleton: Singleton!
- 转到菜单项的绑定检查器,选择 "Availability" 下的 "Hidden",选中组合框中的 "Bind to"、select "Singleton"在它前面,然后在 "Model Key Path". 下键入
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 }
}
}