Singletons в Swift и Интерфейсном Разработчике - PullRequest
0 голосов
/ 28 января 2019

Фон

У меня есть одноэлементный класс в моем приложении, объявленный в соответствии с однострочным синглтоном (с личным init()) в этом сообщении в блоге .В частности, это выглядит так:

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

    private init() {
    }
}

Я бы хотел связать состояние aProperty с тем, скрыт ли пункт меню.

Как я пытался решить проблему

Вот шаги, которые я выполнил для этого:

  1. Перейдите в Библиотеку объектов в Интерфейсном Разработчике и добавьте универсальный «Объект» в мою сцену Приложения.В инспекторе удостоверений настройте «Класс» на Singleton.

  2. Создайте ссылочную точку в моем делегате приложения путем перетаскивания Ctrl из одноэлементного объекта в Интерфейсном конструкторе в код моего делегата приложения,В итоге это выглядит так:

@IBOutlet weak var singleton: Singleton!
Перейдите в инспектор привязок для пункта меню, выберите «Скрытый» в разделе «Доступность», установите флажок «Привязать к», выберите «Синглтон» в поле со списком перед ним и введите 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 и wireэто с Интерфейсным Разработчиком, чтобы избежать его создания дважды?

Если это невозможно, как еще я бы решил проблему связывания глобального свойства с элементом управления в Интерфейсном Разработчике?

Нужен ли MCVE?

Надеюсь, описание было достаточно подробным, но если кто-то считает, что MCVE необходим, оставьте комментарий, и я его создам, и выложу на GitHub.

Ответы [ 3 ]

0 голосов
/ 28 января 2019

В моем конкретном случае есть способ обойти эту проблему.

Вспомните из вопроса, что я хотел только скрыть и показать меню в соответствии с состоянием aProperty в этом синглтоне.В то время как я пытался избежать написания как можно большего количества кода, делая все в Интерфейсном Разработчике, кажется, что в этом случае гораздо меньше хлопот просто написать связывание программно:

menuItem.bind(NSBindingName.hidden, to: Singleton.sharedInstance, withKeyPath: "aProperty", options: nil)
0 голосов
/ 29 января 2019

Я просто хочу начать свой ответ, заявив, что синглтоны не должны использоваться для обмена глобальным состоянием.Хотя в начале они могут показаться более простыми в использовании, в дальнейшем они, как правило, порождают множество головных болей, поскольку их можно изменить практически из любого места, что иногда делает вашу программу непредсказуемой.

При этом, это не так.невозможно достичь того, что вам нужно, но с небольшой церемонией:

@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 }
    }
}
0 голосов
/ 28 января 2019

К сожалению, вы не можете вернуть другой экземпляр из init в Swift.Вот некоторые возможные обходные пути:

  • Создайте выход для экземпляра вашего класса в Интерфейсном Разработчике и затем только ссылайтесь на этот экземпляр во всем вашем коде.(По сути, не одноэлементный, но вы можете добавить некоторые проверки во время выполнения, чтобы убедиться, что он создается только из файла nib, а не из кода).
  • Создайте вспомогательный класс для использования в Interface Builder и представьте свой синглтон какего собственность.Т.е. любой экземпляр этого вспомогательного класса всегда будет возвращать один экземпляр вашего синглтона.
  • Создайте подкласс Objective-C вашего одноэлементного класса Swift и сделайте так, чтобы его init всегда возвращал общий экземпляр одноэлементного Swift.
...