A observação de valor-chave (KVO) está disponível no Swift?


174

Em caso afirmativo, existem diferenças importantes que não estavam presentes ao usar a observação de valores-chave no Objective-C?


2
Um projeto de exemplo que demonstra KVO sendo usado em uma interface UIKit via Swift: github.com/jameswomack/kvo-in-swift
james_womack

@JanDvorak Consulte o Guia de programação do KVO , que é uma boa introdução ao tópico.
26414 Rob

1
Embora não seja uma resposta para sua pergunta, você também pode iniciar ações usando a função didset ().
30715 Vincent

Observe que há um bug do Swift4 quando você usa .initial. Para uma solução, veja aqui . Eu recomendo ver os documentos da Apple . Foi atualizado recentemente e abrange muitas notas importantes. Veja também a outra resposta de
Honey

Respostas:


107

(Editado para adicionar novas informações): considere se o uso da estrutura Combine pode ajudá-lo a realizar o que você queria, em vez de usar o KVO

Sim e não. O KVO trabalha nas subclasses NSObject da mesma forma que sempre. Ele não funciona para classes que não subclasses NSObject. Swift não possui (atualmente pelo menos) seu próprio sistema de observação nativo.

(Veja os comentários sobre como expor outras propriedades como ObjC para que o KVO funcione nelas)

Veja a documentação da Apple para um exemplo completo.


74
Desde o Xcode 6 beta 5, você pode usar a dynamicpalavra-chave em qualquer classe Swift para ativar o suporte ao KVO.
Fabb

7
Viva a @fabb! Para maior clareza, a dynamicpalavra - chave vai para a propriedade que você deseja tornar observável pelo valor-chave.
Jerry

5
A explicação para a dynamicpalavra - chave pode ser encontrada na seção Usando o Swift com cacau e Objective-C da Apple Developer Library .
Imanou Petit

6
Como isso não ficou claro no comentário do @ fabb: use a dynamicpalavra - chave para quaisquer propriedades dentro de uma classe que você gostaria de ser compatível com KVO (não a dynamicpalavra-chave na própria classe). Isso funcionou para mim!
precisa

1
Na verdade não; você não pode registrar um novo didSet de "fora", ele deve fazer parte desse tipo no momento da compilação.
precisa saber é o seguinte

155

Você pode usar o KVO no Swift, mas apenas para dynamicpropriedades da NSObjectsubclasse. Considere que você queria observar a barpropriedade de uma Fooclasse. No Swift 4, especifique barcomo dynamicpropriedade em sua NSObjectsubclasse:

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

Você pode se registrar para observar alterações na barpropriedade. No Swift 4 e no Swift 3.2, isso foi bastante simplificado, conforme descrito em Usando a observação de valores-chave no 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)")
        }
    }
}

Observe que, no Swift 4, agora temos uma digitação forte de caminhos-chave usando o caractere barra invertida (o \.baré o caminho-chave para a barpropriedade do objeto que está sendo observado). Além disso, como ele está usando o padrão de fechamento de conclusão, não precisamos remover manualmente os observadores (quando o tokenescopo está fora do escopo, o observador é removido para nós) nem precisamos nos preocupar em chamar a superimplementação se a chave não Combine. O fechamento é chamado apenas quando esse observador específico é chamado. Para mais informações, consulte o vídeo da WWDC 2017, What's New in Foundation .

No Swift 3, para observar isso, é um pouco mais complicado, mas muito semelhante ao que se faz no Objective-C. Ou seja, você implementaria observeValue(forKeyPath keyPath:, of object:, change:, context:)qual (a) garante que estamos lidando com o nosso contexto (e não com algo que nossa superinstância registrou para observar); e então (b) manuseie ou repasse para a superimplementação, conforme necessário. E certifique-se de se remover como observador, quando apropriado. Por exemplo, você pode remover o observador quando ele é desalocado:

No 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])")
    }

}

Observe que você só pode observar propriedades que podem ser representadas em Objective-C. Assim, você não pode observar genéricos, structtipos Swift enum, tipos Swift , etc.

Para uma discussão sobre a implementação do Swift 2, veja minha resposta original, abaixo.


O uso da dynamicpalavra-chave para atingir o KVO com NSObjectsubclasses é descrito na seção Observação de valor-chave do capítulo Adotando convenções de design de cacau do guia Usando Swift com cacau e Objective-C :

A observação do valor-chave é um mecanismo que permite que os objetos sejam notificados sobre alterações nas propriedades especificadas de outros objetos. Você pode usar a observação de valores-chave com uma classe Swift, desde que a classe seja herdada da NSObjectclasse. Você pode usar estas três etapas para implementar a observação de valores-chave no Swift.

  1. Adicione o dynamicmodificador a qualquer propriedade que você deseja observar. Para mais informações dynamic, consulte Exigir expedição dinâmica .

    class MyObjectToObserve: NSObject {
        dynamic var myDate = NSDate()
        func updateDate() {
            myDate = NSDate()
        }
    }
    
  2. Crie uma variável de contexto global.

    private var myContext = 0
  3. Adicione um observador para o caminho da chave, substitua o observeValueForKeyPath:ofObject:change:context:método e remova o observador 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)
        }
    }
    

[Observe que essa discussão sobre o KVO foi removida posteriormente do guia Usando Swift com cacau e Objective-C , que foi adaptado para o Swift 3, mas ainda funciona conforme descrito na parte superior desta resposta.]


Vale a pena notar que o Swift possui seu próprio sistema de observação de propriedades nativas , mas isso é para uma classe que especifica seu próprio código que será executado mediante a observação de suas próprias propriedades. O KVO, por outro lado, foi projetado para registrar-se para observar alterações em algumas propriedades dinâmicas de alguma outra classe.


Qual é o objetivo myContexte como você observa várias propriedades?
devth 26/08/14

1
De acordo com o KVO Programming Guide : "Quando você registra um objeto como observador, também pode fornecer um contextponteiro. O contextponteiro é fornecido ao observador quando observeValueForKeyPath:ofObject:change:context:é chamado. O contextponteiro pode ser um ponteiro C ou uma referência a objeto. O contextponteiro pode ser usado como um identificador exclusivo para determinar a alteração que está sendo observada ou para fornecer outros dados ao observador ".
Rob

você precisa remover observador em deinit
Jacky

3
@devth, pelo que entendi, se a subclasse ou superclasse também registrar o observador KVO para a mesma variável, observeValueForKeyPath será chamado várias vezes. O contexto pode ser usado para distinguir as próprias notificações nessa situação. Mais sobre isso: dribin.org/dave/blog/archives/2008/09/24/proper_kvo_usage
Zmey

1
Se você deixar em optionsbranco, significa apenas que o valor changenão incluirá o valor antigo ou o novo (por exemplo, você pode obter o novo valor referenciando o próprio objeto). Se você apenas especificar .newe não .old, significa que changeincluirá apenas o novo valor, mas não o valor antigo (por exemplo, muitas vezes você não se importa com o que o valor antigo era, mas apenas com o novo valor). Se você precisar observeValueForKeyPathpassar o valor antigo e o novo, especifique [.new, .old]. Bottom line, optionsapenas especifica o que está incluído no changedicionário.
Rob

92

Sim e não:

  • Sim , você pode usar as mesmas APIs KVO antigas no Swift para observar objetos Objective-C.
    Você também pode observar as dynamicpropriedades dos objetos Swift herdados de NSObject.
    Mas ... Não , não é fortemente tipado como você poderia esperar do sistema de observação nativo Swift.
    Usando Swift com cacau e Objective-C | Observação do valor-chave

  • Não , atualmente não existe um sistema de observação de valores embutido para objetos Swift arbitrários.

  • Sim , existem Observadores de Propriedade embutidos , que são fortemente tipados.
    Mas ... Não, eles não são o KVO, pois permitem apenas observar as propriedades dos objetos, não suportam observações aninhadas ("caminhos-chave") e você precisa implementá-las explicitamente.
    A linguagem de programação rápida | Observadores de propriedades

  • Sim , você pode implementar a observação explícita de valores, que será fortemente digitada, além de permitir a adição de vários manipuladores de outros objetos, e até dar suporte a aninhamento / "caminhos de chave".
    Mas ... Não , não será o KVO, pois funcionará apenas para propriedades que você implementa como observável.
    Você pode encontrar uma biblioteca para implementar esse valor observando aqui:
    Observable-Swift - KVO for Swift - Value Observing and Events


10

Um exemplo pode ajudar um pouco aqui. Se eu tiver uma instância modelda classe Modelcom atributos namee statepuder observá-los com:

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)

Alterações nessas propriedades acionarão uma chamada para:

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

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

2
Se não me engano, a abordagem de chamada observeValueForKeyPath é para Swift2.
Fattie

9

Sim.

O KVO requer despacho dinâmico, portanto, basta adicionar o dynamicmodificador a um método, propriedade, subscrito ou inicializador:

dynamic var foo = 0

O dynamicmodificador garante que as referências à declaração sejam despachadas e acessadas dinamicamente objc_msgSend.


7

Além da resposta de Rob. Essa classe deve herdar de NSObject, e temos 3 maneiras de acionar alterações de propriedade

Use setValue(value: AnyObject?, forKey key: String)deNSKeyValueCoding

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

Use willChangeValueForKeye didChangeValueForKeydeNSKeyValueObserving

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

Use dynamic. Consulte Compatibilidade do tipo Swift

Você também pode usar o modificador dinâmico para exigir que o acesso aos membros seja enviado dinamicamente por meio do tempo de execução do Objective-C se você estiver usando APIs como observação de valor-chave que substitui dinamicamente a implementação de um método.

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

E getter e setter de propriedade são chamados quando usados. Você pode verificar ao trabalhar com o KVO. Este é um exemplo de propriedade computada

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

Visão geral

É possível usar Combinesem usar NSObjectouObjective-C

Disponibilidade: iOS 13.0+ , macOS 10.15+, tvOS 13.0+, watchOS 6.0+, Mac Catalyst 13.0+,Xcode 11.0+

Nota: Precisa ser usado apenas com classes e não com tipos de valor.

Código:

Versão 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

Resultado:

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

Referir:


4

Atualmente, o Swift não suporta nenhum mecanismo interno para observar alterações de propriedades de objetos que não sejam 'self'; portanto, não, ele não suporta o KVO.

No entanto, o KVO é uma parte tão fundamental do Objective-C e do Cocoa que parece bastante provável que seja adicionado no futuro. A documentação atual parece implicar isso:

Observação do valor-chave

Informações futuras.

Usando Swift com cacau e Objective-C


2
Obviamente, esse guia que você faz referência agora descreve como executar o KVO no Swift.
26414 Rob

4
Sim, agora implementado em setembro de 2014
Max MacLeod

4

Uma coisa importante a mencionar é que, após atualizar o seu Xcode para 7 beta, você pode receber a seguinte mensagem: "O método não substitui nenhum método da superclasse" . Isso é devido à opcionalidade dos argumentos. Certifique-se de que seu manipulador de observação tenha exatamente a seguinte aparência:

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

2
No Xcode beta 6 requer: substituição func observeValueForKeyPath (keyPath: Cadeia ?, ofObject objecto: AnyObject ?, mudança: [Cadeia: AnyObject] ?, contexto: UnsafeMutablePointer <Vazio>)
hcanfly

4

Isso pode ser útil para poucas pessoas -

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

Eu usei o KVO dessa maneira no Swift 3. Você pode usar esse código com poucas alterações.


1

Outro exemplo para quem encontra um problema com tipos como Int? e CGFloat ?. Você simplesmente define sua classe como uma subclasse de NSObject e declara suas variáveis ​​da seguinte maneira, por exemplo:

class Theme : NSObject{

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

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

}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.