Чи доступні спостереження за ключовими значеннями (KVO) у Свіфті?


174

Якщо так, чи є ключові відмінності, які б інакше не були при використанні спостереження за значенням ключа в Objective-C?


2
Приклад проекту, який демонструє використання KVO в інтерфейсі UIKit через Swift: github.com/jameswomack/kvo-in-swift
james_womack

@JanDvorak Дивіться Посібник з програмування KVO , який є хорошим вступом до теми.
Роб

1
Хоча це не відповідь на ваше запитання, ви також можете розпочати дії за допомогою функції didset ().
Вінсент

Зверніть увагу, що під час використання виникла помилка Swift4 .initial. Про рішення дивіться тут . Дуже рекомендую переглянути документи Apple . Він нещодавно оновлений і охоплює багато важливих приміток. Також дивіться іншу відповідь
Honey

Відповіді:


107

(Відредаговано, щоб додати нову інформацію): розгляньте, чи використання рамки Combine може допомогти вам досягти того, що ви хотіли, а не використовувати KVO

Так і ні. KVO працює над підкласами NSObject так само, як завжди. Це не працює для класів, які не підклас NSObject. Свіфт не має (наразі щонайменше) власної натурної системи спостереження.

(Див. Коментарі щодо того, як виставити інші властивості як ObjC, щоб KVO працював на них)

Дивіться документацію Apple , для повного прикладу.


74
Оскільки Xcode 6 beta 5, ви можете використовувати dynamicключове слово у будь-якому класі Swift, щоб увімкнути підтримку KVO.
fabb

7
Ура для @fabb! Для наочності dynamicключове слово входить до властивості, для якої потрібно зробити ключ-значення, яке можна спостерігати.
Джеррі

5
Пояснення цього dynamicключового слова можна знайти в розділі Використання Swift з какао та об'єктивом-C у бібліотеці розробників Apple .
Imanou Petit

6
Оскільки це мені було не зрозуміло з коментаря @ fabb: використовуйте dynamicключове слово для будь-яких властивостей класу, які ви хотіли б відповідати KVO (не dynamicключове слово для самого класу). Це працювало для мене!
Тім Камбер

1
Не зовсім; ви не можете зареєструвати новий didSet з "зовні", він повинен бути частиною цього типу під час компіляції.
Catfish_Man

155

Ви можете використовувати KVO в Swift, але лише для dynamicвластивостей NSObjectпідкласу. Подумайте, що ви хотіли спостерігати за barвластивістю Fooкласу. У Swift 4 вкажіть barяк dynamicвластивість у своєму NSObjectпідкласі:

class Foo: NSObject {
    @objc dynamic var bar = 0
}

Потім ви можете зареєструватися, щоб спостерігати за змінами у barвласності. У Swift 4 та Swift 3.2 це було значно спрощено, як було зазначено у застосуванні спостереження ключових значень у Swift :

class MyObject {
    private var token: NSKeyValueObservation

    var objectToObserve = Foo()

    init() {
        token = objectToObserve.observe(\.bar) { [weak self] object, change in  // the `[weak self]` is to avoid strong reference cycle; obviously, if you don't reference `self` in the closure, then `[weak self]` is not needed
            print("bar property is now \(object.bar)")
        }
    }
}

Зауважте, у Swift 4 зараз сильно вводити клавіші за допомогою символу зворотної косої риски ( \.barце шлях для barвластивості об'єкта, який спостерігається). Крім того, оскільки він використовує шаблон закриття завершення, нам не доведеться видаляти спостерігачів вручну (коли tokenвипадає за межі, спостерігач видаляється для нас), і не потрібно турбуватися про виклик програми, superякщо ключ не матч. Закриття викликається лише тоді, коли викликається саме цей спостерігач. Для отримання додаткової інформації дивіться відео WWDC 2017, Що нового у Фонді .

У Swift 3 спостерігати це дещо складніше, але дуже схоже на те, що робиться в Objective-C. А саме, ви б реалізували, observeValue(forKeyPath keyPath:, of object:, change:, context:)що (a) гарантує, що ми маємо справу з нашим контекстом (а не з тим, що superзареєстрував наш екземпляр); а потім (b) або обробляти це, або передавати його для superреалізації, якщо це необхідно. І обов’язково знімайте себе в якості спостерігача, коли це доречно. Наприклад, ви можете видалити спостерігача, коли він розміщений:

У Swift 3:

class MyObject: NSObject {
    private var observerContext = 0

    var objectToObserve = Foo()

    override init() {
        super.init()

        objectToObserve.addObserver(self, forKeyPath: #keyPath(Foo.bar), options: [.new, .old], context: &observerContext)
    }

    deinit {
        objectToObserve.removeObserver(self, forKeyPath: #keyPath(Foo.bar), context: &observerContext)
    }

    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        guard context == &observerContext else {
            super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
            return
        }

        // do something upon notification of the observed object

        print("\(keyPath): \(change?[.newKey])")
    }

}

Зауважте, ви можете спостерігати лише за властивостями, які можуть бути представлені в Objective-C. Таким чином, ви не можете спостерігати дженерики, structтипи Swift, enumтипи Swift тощо.

Для обговорення реалізації Swift 2 дивіться мою оригінальну відповідь нижче.


Використання dynamicключового слова для досягнення KVO за допомогою NSObjectпідкласів описано в розділі « Спостереження за ключовими значеннями» в розділі « Прийняття конвенцій дизайну какао» в посібнику « Використання швидкого використання з какао» та «Ціль-C» :

Спостереження за значенням ключа - це механізм, який дозволяє сповіщати об'єкти про зміни зазначених властивостей інших об'єктів. Ви можете використовувати спостереження ключових значень із класом Swift, доки клас успадковує його від NSObjectкласу. Ви можете скористатися цими трьома кроками для впровадження спостереження ключових значень у Swift.

  1. Додайте dynamicмодифікатор до будь-якого властивості, яке ви хочете спостерігати. Для отримання додаткової інформації про те dynamic, див. Потрібна динамічна відправка .

    class MyObjectToObserve: NSObject {
        dynamic var myDate = NSDate()
        func updateDate() {
            myDate = NSDate()
        }
    }
  2. Створіть глобальну змінну контексту.

    private var myContext = 0
  3. Додайте спостерігача за ключовим шляхом та замініть observeValueForKeyPath:ofObject:change:context:метод та видаліть спостерігача в deinit.

    class MyObserver: NSObject {
        var objectToObserve = MyObjectToObserve()
        override init() {
            super.init()
            objectToObserve.addObserver(self, forKeyPath: "myDate", options: .New, context: &myContext)
        }
    
        override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
            if context == &myContext {
                if let newValue = change?[NSKeyValueChangeNewKey] {
                    print("Date changed: \(newValue)")
                }
            } else {
                super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
            }
        }
    
        deinit {
            objectToObserve.removeObserver(self, forKeyPath: "myDate", context: &myContext)
        }
    }

[Зауважте, ця дискусія KVO згодом була видалена з посібника " Використання Swift з какао" та "Objective-C" , який був адаптований для Swift 3, але він як і раніше працює так, як окреслено у верхній частині цієї відповіді.]


Варто зазначити, що Swift має власну систему спостереження за власною власністю , але це для класу із зазначенням власного коду, який буде виконуватися після спостереження за його власними властивостями. KVO, з іншого боку, призначений для реєстрації для спостереження за змінами деякої динамічної властивості якогось іншого класу.


Яке призначення myContextі як ви дотримуєтесь кількох властивостей?
дев’яток

1
Відповідно до Посібника з програмування KVO : "Коли ви реєструєте об'єкт як спостерігач, ви також можете надати contextвказівник. contextПокажчик надається спостерігачеві при observeValueForKeyPath:ofObject:change:context:виклику. contextПокажчик може бути вказівником C або посиланням на об'єкт. contextВказівник може бути використовується як унікальний ідентифікатор для визначення зміни, яка спостерігається, або для надання спостерігача деяких інших даних ".
Роб

вам потрібно видалити спостерігача в deinit
Джекі

3
@devth, наскільки я розумію, якщо підклас або надклас також реєструє спостерігача KVO для однієї і тієї ж змінної, obserValueForKeyPath буде викликатися кілька разів. Контекст можна використовувати для розрізнення власних сповіщень у цій ситуації. Більше про це: dribin.org/dave/blog/archives/2008/09/24/proper_kvo_usage
Zmey

1
Якщо ви залишите optionsпорожнім, це просто означає, що changeне буде включати старе або нове значення (наприклад, ви можете просто отримати нове значення самостійно, посилаючись на сам об’єкт). Якщо ви просто вказуєте, .newа не .old- це означає, що він changeбуде включати лише нове значення, але не старе значення (наприклад, вам часто не байдуже, що було старим значенням, а байдуже лише нове значення). Якщо вам потрібно observeValueForKeyPathпередати вам і старе, і нове значення, тоді вкажіть [.new, .old]. Підсумок, optionsпросто вказує, що включено до changeсловника.
Роб

92

І так, і ні:

  • Так , ви можете використовувати ті самі старі API KVO у Swift, щоб спостерігати об’єкти Objective-C.
    Ви також можете спостерігати dynamicвластивості об'єктів Swift, що успадковуються від NSObject.
    Але ... Ні, це не так сильно набрано, як можна було б очікувати, що це буде швидка система спостереження Свіфт.
    Використання Swift з какао та Objective-C | Ключові значення спостереження

  • Ні , наразі не існує вбудованої системи спостереження за довільними об'єктами Swift.

  • Так , є вбудовані спостерігачі власності , які сильно набрані.
    Але ... Ні, вони не є KVO, оскільки вони дозволяють лише спостерігати за властивостями об'єктів, не підтримують вкладені спостереження ("ключові шляхи"), і вам доведеться їх явно реалізувати.
    Швидка мова програмування | Спостерігачі власності

  • Так , ви можете реалізувати явне спостереження за значенням, яке буде сильно набрано, і дозволить додавати декілька обробників з інших об'єктів і навіть підтримувати вкладення / "ключові шляхи".
    Але ... Ні, це не буде KVO, оскільки він працюватиме лише для властивостей, які ви реалізуєте як видимі.
    Ви можете знайти бібліотеку для реалізації таких значень, спостерігаючи тут:
    Спостережуване-Швидке - KVO для Swift - Спостереження цінності та події


10

Приклад може трохи допомогти тут. Якщо у мене є екземпляр modelкласу Modelз атрибутами nameі stateя можу спостерігати за цими атрибутами за допомогою:

let options = NSKeyValueObservingOptions([.New, .Old, .Initial, .Prior])

model.addObserver(self, forKeyPath: "name", options: options, context: nil)
model.addObserver(self, forKeyPath: "state", options: options, context: nil)

Зміни цих властивостей викликають дзвінок на:

override func observeValueForKeyPath(keyPath: String!,
    ofObject object: AnyObject!,
    change: NSDictionary!,
    context: CMutableVoidPointer) {

        println("CHANGE OBSERVED: \(change)")
}

2
Якщо я не помиляюся, підхід для виклику obserValueForKeyPath призначений для Swift2.
Fattie

9

Так.

KVO вимагає динамічної диспетчеризації, тому вам просто потрібно додати dynamicмодифікатор до методу, властивості, підписника або ініціалізатора:

dynamic var foo = 0

У dynamicМодифікатор гарантує , що посилання на декларації будуть динамічно відправляються і доступні через objc_msgSend.


7

Окрім відповіді Роб. Цей клас повинен успадкувати NSObject, і у нас є 3 способи викликати зміну властивості

Використання setValue(value: AnyObject?, forKey key: String)відNSKeyValueCoding

class MyObjectToObserve: NSObject {
    var myDate = NSDate()
    func updateDate() {
        setValue(NSDate(), forKey: "myDate")
    }
}

Використання willChangeValueForKeyта didChangeValueForKeyвідNSKeyValueObserving

class MyObjectToObserve: NSObject {
    var myDate = NSDate()
    func updateDate() {
        willChangeValueForKey("myDate")
        myDate = NSDate()
        didChangeValueForKey("myDate")
    }
}

Використовуйте dynamic. Див. Розділ Сумісність типу Swift

Ви також можете використовувати динамічний модифікатор, щоб вимагати, щоб доступ до членів динамічно розсилався через час виконання Objective-C, якщо ви використовуєте такі API, як спостереження за значенням ключа, яке динамічно замінює реалізацію методу.

class MyObjectToObserve: NSObject {
    dynamic var myDate = NSDate()
    func updateDate() {
        myDate = NSDate()
    }
}

І користувач, який отримує майно, і сетер називаються при використанні. Ви можете перевірити, працюючи з KVO. Це приклад обчисленої властивості

class MyObjectToObserve: NSObject {
    var backing: NSDate = NSDate()
    dynamic var myDate: NSDate {
        set {
            print("setter is called")
            backing = newValue
        }
        get {
            print("getter is called")
            return backing
        }
    }
}

5

Огляд

Можна використовувати Combineбез використання NSObjectабоObjective-C

Наявність: iOS 13.0+ , macOS 10.15+, tvOS 13.0+, watchOS 6.0+, Mac Catalyst 13.0+,Xcode 11.0+

Примітка. Потрібно використовувати лише класи, які не мають типи значень.

Код:

Версія Swift: 5.1.2

import Combine //Combine Framework

//Needs to be a class doesn't work with struct and other value types
class Car {

    @Published var price : Int = 10
}

let car = Car()

//Option 1: Automatically Subscribes to the publisher

let cancellable1 = car.$price.sink {
    print("Option 1: value changed to \($0)")
}

//Option 2: Manually Subscribe to the publisher
//Using this option multiple subscribers can subscribe to the same publisher

let publisher = car.$price

let subscriber2 : Subscribers.Sink<Int, Never>

subscriber2 = Subscribers.Sink(receiveCompletion: { print("completion \($0)")}) {
    print("Option 2: value changed to \($0)")
}

publisher.subscribe(subscriber2)

//Assign a new value

car.price = 20

Вихід:

Option 1: value changed to 10
Option 2: value changed to 10
Option 1: value changed to 20
Option 2: value changed to 20

Посилання:


4

В даний час Swift не підтримує жодного вбудованого механізму спостереження за зміною властивостей об'єктів, окрім 'self', тому ні, він не підтримує KVO.

Однак KVO є настільки фундаментальною частиною Objective-C і какао, що здається цілком ймовірним, що він буде доданий у майбутньому. Поточна документація, мабуть, означає це:

Спостереження ключових значень

Інформація, що надходить.

Використання Swift з какао та Objective-C


2
Очевидно, що в цьому посібнику, на який ви посилаєтесь, описується, як зробити KVO в Swift.
Роб

4
Так, зараз впроваджено з вересня 2014 року
Макс Маклеод

4

Важливо згадати те, що після оновлення вашого Xcode до 7 бета-версії, ви можете отримати таке повідомлення: "Метод не замінює жоден метод зі свого суперкласу" . Це через необов'язковість аргументів. Переконайтесь, що обробник спостереження виглядає саме так:

override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [NSObject : AnyObject]?, context: UnsafeMutablePointer<Void>)

2
У Xcode beta 6 він вимагає: переопределити функцію спостерігатиValueForKeyPath (keyPath: String ?, ofObject object: AnyObject?, Change: [String: AnyObject] ?, context: UnsafeMutablePointer <Void>)
hcanfly

4

Це може виявитися корисним для небагатьох людей -

// MARK: - KVO

var observedPaths: [String] = []

func observeKVO(keyPath: String) {
    observedPaths.append(keyPath)
    addObserver(self, forKeyPath: keyPath, options: [.old, .new], context: nil)
}

func unObserveKVO(keyPath: String) {
    if let index = observedPaths.index(of: keyPath) {
        observedPaths.remove(at: index)
    }
    removeObserver(self, forKeyPath: keyPath)
}

func unObserveAllKVO() {
    for keyPath in observedPaths {
        removeObserver(self, forKeyPath: keyPath)
    }
}

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if let keyPath = keyPath {
        switch keyPath {
        case #keyPath(camera.iso):
            slider.value = camera.iso
        default:
            break
        }
    }
}

Я використовував KVO таким чином у Swift 3. Ви можете використовувати цей код з кількома змінами.


1

Ще один приклад для тих, хто стикається з проблемою таких типів, як Int? і CGFloat ?. Ви просто встановите клас як підклас NSObject і оголосите свої змінні таким чином, наприклад:

class Theme : NSObject{

   dynamic var min_images : Int = 0
   dynamic var moreTextSize : CGFloat = 0.0

   func myMethod(){
       self.setValue(value, forKey: "\(min_images)")
   }

}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.