Доступно ли наблюдение значения ключа (KVO) в Swift?

174

Если да, есть ли какие-либо ключевые различия, которые иначе не присутствовали бы при использовании наблюдения значения ключа в Objective-C?

codeperson
источник
2
Пример проекта, который демонстрирует использование KVO в интерфейсе UIKit через Swift: github.com/jameswomack/kvo-in-swift
james_womack
@JanDvorak См. Руководство по программированию KVO , которое является хорошим введением в тему.
Роб
1
Хотя это и не ответ на ваш вопрос, вы также можете запускать действия с помощью функции didset ().
Винсент
Обратите внимание, есть ошибка Swift4 при использовании .initial. Для решения см. Здесь . Я настоятельно рекомендую посмотреть документы Apple . Он был недавно обновлен и содержит множество важных заметок. Также см. Другой ответ
Honey

Ответы:

107

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

Да и нет. KVO работает на подклассах NSObject так же, как всегда. Это не работает для классов, которые не подкласс NSObject. Swift не имеет (в настоящее время по крайней мере) собственной системы наблюдения.

(См. Комментарии о том, как выставить другие свойства как ObjC, чтобы KVO работал с ними)

См. Документацию Apple для полного примера.

Catfish_Man
источник
74
Начиная с Xcode 6 beta 5, вы можете использовать dynamicключевое слово в любом классе Swift для включения поддержки KVO.
Фабб
7
Ура для @fabb! Для ясности dynamicключевое слово относится к свойству, которое вы хотите сделать наблюдаемым по значению ключа.
Джерри
5
Объяснение этого dynamicключевого слова можно найти в разделе « Использование Swift с какао и Objective-C» в библиотеке разработчика Apple .
Imanou Petit
6
Так как из комментария @ fabb мне это было неясно: используйте dynamicключевое слово для любых свойств внутри класса, который вы хотите, чтобы он был KVO-совместимым (не dynamicключевое слово для самого класса). Это сработало для меня!
Тим Кэмбер
1
На самом деле, нет; вы не можете зарегистрировать новый didSet "извне", он должен быть частью этого типа во время компиляции.
Cat__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, Что нового в Foundation .

В Swift 3 наблюдать это немного сложнее, но очень похоже на то, что делают в Objective-C. А именно, вы бы реализовали, observeValue(forKeyPath keyPath:, of object:, change:, context:)что (а) гарантирует, что мы имеем дело с нашим контекстом (а не с тем, что наш 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подклассами описано в разделе « Наблюдение значения ключа » главы « Принятие конвенций проектирования какао» руководства « Использование Swift с какао и Objective-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-наблюдателя для одной и той же переменной, НаблюдениеValueForKeyPath будет вызываться несколько раз. Контекст может использоваться для различения собственных уведомлений в этой ситуации. Подробнее об этом: dribin.org/dave/blog/archives/2008/09/24/proper_kvo_usage
Змей,
1
Если вы оставите optionsпустым, это просто означает, что оно changeне будет включать старое или новое значение (например, вы можете просто получить новое значение, ссылаясь на сам объект). Если вы просто укажете .newи нет .old, это changeбудет означать, что в него будет входить только новое значение, но не старое значение (например, вам часто не важно, какое было старое значение, а только новое значение). Если вам нужно observeValueForKeyPathпередать вам как старое, так и новое значение, укажите [.new, .old]. Итог, optionsпросто указывает, что входит в changeсловарь.
Роб
92

И да, и нет:

  • Да , вы можете использовать те же старые API KVO в Swift для наблюдения за объектами Objective-C.
    Вы также можете наблюдать dynamicсвойства объектов Swift, наследуемых от NSObject.
    Но ... Нет, он не слишком типизирован, как можно было ожидать от системы наблюдения Swift.
    Использование Swift с какао и Objective-C | Наблюдение за ключевыми значениями

  • Нет , в настоящее время нет встроенной системы наблюдения значений для произвольных объектов Swift.

  • Да , есть встроенные Обозреватели Собственности , которые строго типизированы.
    Но ... Нет, они не являются KVO, так как они позволяют только наблюдать за собственными свойствами объектов, не поддерживают вложенные наблюдения ("ключевые пути"), и вы должны явно их реализовать.
    Язык программирования Swift | Наблюдатели за недвижимостью

  • Да , вы можете реализовать явное наблюдение за значениями, которое будет строго типизировано, и позволять добавлять несколько обработчиков из других объектов и даже поддерживать вложение / "ключевые пути".
    Но ... Нет, это не будет KVO, так как он будет работать только для свойств, которые вы реализуете как наблюдаемые.
    Вы можете найти библиотеку для реализации таких наблюдений за ценностями здесь:
    Observable-Swift - KVO для Swift - Наблюдение за ценностями и события

slazyk
источник
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
Если я не ошибаюсь, подход к вызову наблюдать за ValveForKeyPath предназначен для Swift2.
Толстяк
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 Type Совместимость

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

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

И свойство getter и setter вызывается при использовании. Вы можете проверить при работе с КВО. Это пример вычисляемого свойства

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
        }
    }
}
onmyway133
источник
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

См:

user1046037
источник
4

В настоящее время Swift не поддерживает какой-либо встроенный механизм для наблюдения за изменениями свойств объектов, кроме 'self', поэтому нет, он не поддерживает KVO.

Тем не менее, KVO является настолько фундаментальной частью Objective-C и Cocoa, что кажется весьма вероятным, что он будет добавлен в будущем. Текущая документация, кажется, подразумевает это:

Наблюдение значения ключа

Информация ожидается.

Использование Swift с какао и Objective-C

Coline
источник
2
Очевидно, что это руководство, на которое вы ссылаетесь, теперь описывает, как сделать KVO в Swift.
Роб
4
Да, теперь введено в эксплуатацию в сентябре 2014 года
Макс МакЛеод
4

Важно отметить, что после обновления Xcode до 7 beta вы можете получить следующее сообщение: «Метод не переопределяет ни один метод из своего суперкласса» . Это из-за необязательности аргументов. Убедитесь, что ваш обработчик наблюдения выглядит точно так:

override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [NSObject : AnyObject]?, context: UnsafeMutablePointer<Void>)
Артур Геворкян
источник
2
В бета-версии Xcode 6 требуется: переопределить func наблюдайте за ValveForKeyPath (keyPath: String ?, ofObject объект: AnyObject ?, изменить: [String: AnyObject] ?, контекст: 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)")
   }

}
DrPatience
источник