Is key-value observation (KVO) available in Swift?
Asked Answered
P

11

179

If so, are there any key differences that weren't otherwise present when using key-value observation in Objective-C?

Paeon answered 7/6, 2014 at 0:5 Comment(4)
An example project that demonstrates KVO being used in a UIKit interface via Swift: github.com/jameswomack/kvo-in-swiftRavel
@JanDvorak See the KVO Programming Guide, which is a nice introduction to the topic.Marja
Although not an answer to your question, you can also start actions using the didset() function.Peavey
Note there is a Swift4 bug when you use .initial. For a solution see here. I highly recommend to see Apple docs. It's been updated recently and covers lots of important notes. Also see Rob's other answerUnriddle
I
107

(Edited to add new info): consider whether using the Combine framework can help you accomplish what you wanted, rather than using KVO

Yes and no. KVO works on NSObject subclasses much as it always has. It does not work for classes that don't subclass NSObject. Swift does not (currently at least) have its own native observation system.

(See comments for how to expose other properties as ObjC so KVO works on them)

See the Apple Documentation for a full example.

Instar answered 7/6, 2014 at 0:15 Comment(8)
FYI, I filed a bug report with Apple about this. I encourage everyone else who would like to see a KVO or similar observation system in swift, to do the same.Straggle
Since Xcode 6 beta 5 you can use the dynamic keyword on any Swift class to enable KVO support.Sulphonamide
Hooray for @fabb! For clarity, the dynamic keyword goes on the property that you want to make key-value-observable.Balladry
The explanation for the dynamic keyword can be find in the Apple Developer Library's Using Swift with Cocoa and Objective-C section.Fallon
Since this wasn't clear to me from @fabb's comment: use the dynamic keyword for any properties inside of a class you'd like to be KVO compliant (not the dynamic keyword on the class itself). This worked for me!Balanced
Can we say that didSet as KVO—to some extent?Unriddle
Not really; you can't register a new didSet from the "outside", it has to be part of that type at compile time.Instar
In Swift 5.1 it appears that you can't get KVO support by simply adding dynamic to the property declaration; you must specify @objc dynamic. Omitting the @objc will result in a NSUnknownKeyException when you call value(forKey:).Pyrochemical
M
163

You can use KVO in Swift, but only for dynamic properties of NSObject subclass. Consider that you wanted to observe the bar property of a Foo class. In Swift 4, specify bar as dynamic property in your NSObject subclass:

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

You can then register to observe changes to the bar property. In Swift 4 and Swift 3.2, this has been greatly simplified, as outlined in Using Key-Value Observing in 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)")
        }
    }
}

Note, in Swift 4, we now have strong typing of keypaths using the backslash character (the \.bar is the keypath for the bar property of the object being observed). Also, because it's using the completion closure pattern, we don't have to manually remove observers (when the token falls out of scope, the observer is removed for us) nor do we have to worry about calling the super implementation if the key doesn't match. The closure is called only when this particular observer is invoked. For more information, see WWDC 2017 video, What's New in Foundation.

In Swift 3, to observe this, it's a bit more complicated, but very similar to what one does in Objective-C. Namely, you would implement observeValue(forKeyPath keyPath:, of object:, change:, context:) which (a) makes sure we're dealing with our context (and not something that our super instance had registered to observe); and then (b) either handle it or pass it on to the super implementation, as necessary. And make sure to remove yourself as an observer when appropriate. For example, you might remove the observer when it is deallocated:

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

}

Note, you can only observe properties that can be represented in Objective-C. Thus, you cannot observe generics, Swift struct types, Swift enum types, etc.

For a discussion of the Swift 2 implementation, see my original answer, below.


Using the dynamic keyword to achieve KVO with NSObject subclasses is described in the Key-Value Observing section of the Adopting Cocoa Design Conventions chapter of the Using Swift with Cocoa and Objective-C guide:

Key-value observing is a mechanism that allows objects to be notified of changes to specified properties of other objects. You can use key-value observing with a Swift class, as long as the class inherits from the NSObject class. You can use these three steps to implement key-value observing in Swift.

  1. Add the dynamic modifier to any property you want to observe. For more information on dynamic, see Requiring Dynamic Dispatch.

    class MyObjectToObserve: NSObject {
        dynamic var myDate = NSDate()
        func updateDate() {
            myDate = NSDate()
        }
    }
    
  2. Create a global context variable.

    private var myContext = 0
    
  3. Add an observer for the key-path, and override the observeValueForKeyPath:ofObject:change:context: method, and remove the observer in 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)
        }
    }
    

[Note, this KVO discussion has subsequently been removed from the Using Swift with Cocoa and Objective-C guide, which has been adapted for Swift 3, but it still works as outlined at the top of this answer.]


It's worth noting that Swift has its own native property observer system, but that's for a class specifying its own code that will be performed upon observation of its own properties. KVO, on the other hand, is designed to register to observe changes to some dynamic property of some other class.

Marja answered 9/8, 2014 at 13:45 Comment(15)
What is the purpose of myContext and how do you observe multiple properties?Panchromatic
According to KVO Programming Guide: "When you register an object as an observer, you can also provide a context pointer. The context pointer is provided to the observer when observeValueForKeyPath:ofObject:change:context: is invoked. The context pointer can be a C pointer or an object reference. The context pointer can be used as a unique identifier to determine the change that is being observed, or to provide some other data to the observer."Marja
you need to remove observer in deinitBeekman
@Jacky Agreed. That's where I generally remove the observer. I had simply quoted the Apple documentation, which didn't cover this key point, but it looks like they've finally updated it to reflect this. So I've updated my quote from their documentation accordingly.Marja
@devth, as I understand, if subclass or superclass also registers KVO observer for the same variable, observeValueForKeyPath will be called multiple times. Context can be used to distinguish own notifications in this situation. More on this: dribin.org/dave/blog/archives/2008/09/24/proper_kvo_usageVerwoerd
need to change: [NSObject : AnyObject]? to [String : AnyObject]?Balmoral
@LeoDabus - Agreed. The API has changed. Answer updated. Thanks.Marja
If I mark it as private it can't be accessed in MyObserver classBalmoral
@LeoDabus - If this private global is defined within the same .swift file as the MyObserver class, it can be accessed fine. And it seems better to do that and keep it private, to avoid polluting the global namespace.Marja
Can we KVO the alpha of a view? https://mcmap.net/q/112810/-in-swift3-kvo-the-alpha-of-a-uiview/294884Meiny
I read here about the options, but still don't get what exactly do they mean in laymen's term. What would happen if you leave the options empty? What would happen if you remove .old?Unriddle
If you leave options empty, it just means that the change won't include the old or new value (e.g. you might just get the new value yourself by referencing the object itself). If you just specify .new and not .old, it means that change will include only the new value, but not the old value (e.g. you often don't care about what the old value was, but only care about the new value). If you need observeValueForKeyPath to pass you both the old and new value, then specify [.new, .old]. Bottom line, options just specifies what is included in the change dictionary.Marja
Thanks...and If I state prior then I will get 2 notifications? one for .old and another for .new? rather than getting a dictionary with 2 different keys? and if I state initial ummm not sure really what happens!?Unriddle
Re .prior, yep, that's it. And re .initial, generally when you set up notification, you don't receive a notification until the value changes. But you can use .initial if you also want to receive a notification when you first set it up (e.g. if the initialization routine is identical to your change routine that you're already doing in observeValue, you can use .initial to have it called immediately, too). It just depends upon what you're doing in your observeValue routine.Marja
added +1 for nice answerJuxtapose
I
107

(Edited to add new info): consider whether using the Combine framework can help you accomplish what you wanted, rather than using KVO

Yes and no. KVO works on NSObject subclasses much as it always has. It does not work for classes that don't subclass NSObject. Swift does not (currently at least) have its own native observation system.

(See comments for how to expose other properties as ObjC so KVO works on them)

See the Apple Documentation for a full example.

Instar answered 7/6, 2014 at 0:15 Comment(8)
FYI, I filed a bug report with Apple about this. I encourage everyone else who would like to see a KVO or similar observation system in swift, to do the same.Straggle
Since Xcode 6 beta 5 you can use the dynamic keyword on any Swift class to enable KVO support.Sulphonamide
Hooray for @fabb! For clarity, the dynamic keyword goes on the property that you want to make key-value-observable.Balladry
The explanation for the dynamic keyword can be find in the Apple Developer Library's Using Swift with Cocoa and Objective-C section.Fallon
Since this wasn't clear to me from @fabb's comment: use the dynamic keyword for any properties inside of a class you'd like to be KVO compliant (not the dynamic keyword on the class itself). This worked for me!Balanced
Can we say that didSet as KVO—to some extent?Unriddle
Not really; you can't register a new didSet from the "outside", it has to be part of that type at compile time.Instar
In Swift 5.1 it appears that you can't get KVO support by simply adding dynamic to the property declaration; you must specify @objc dynamic. Omitting the @objc will result in a NSUnknownKeyException when you call value(forKey:).Pyrochemical
O
92

Both yes and no:

  • Yes, you can use the same old KVO APIs in Swift to observe Objective-C objects.
    You can also observe dynamic properties of Swift objects inheriting from NSObject.
    But... No it's not strongly typed as you could expect Swift native observation system to be.
    Using Swift with Cocoa and Objective-C | Key Value Observing

  • No, currently there is no builtin value observation system for arbitrary Swift objects.

  • Yes, there are builtin Property Observers, which are strongly typed.
    But... No they are not KVO, since they allow only for observing of objects own properties, don't support nested observations ("key paths"), and you have to explicitly implement them.
    The Swift Programming Language | Property Observers

  • Yes, you can implement explicit value observing, which will be strongly typed, and allow for adding multiple handlers from other objects, and even support nesting / "key paths".
    But... No it will not be KVO since it will only work for properties which you implement as observable.
    You can find a library for implementing such value observing here:
    Observable-Swift - KVO for Swift - Value Observing and Events

Opportune answered 29/6, 2014 at 22:14 Comment(0)
F
10

An example might help a little here. If I have an instance model of class Model with attributes name and state I can observe those attributes with:

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)

Changes to these properties will trigger a call to:

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

        println("CHANGE OBSERVED: \(change)")
}
Freewheel answered 21/6, 2014 at 18:40 Comment(1)
If I'm not mistaken, the observeValueForKeyPath call approach is for Swift2.Meiny
D
10

Yes.

KVO requires dynamic dispatch, so you simply need to add the dynamic modifier to a method, property, subscript, or initializer:

dynamic var foo = 0

The dynamic modifier ensures that references to the declaration will be dynamically dispatched and accessed through objc_msgSend.

Dittman answered 9/8, 2014 at 1:2 Comment(0)
R
8

In addition to Rob's answer. That class must inherit from NSObject, and we have 3 ways to trigger property change

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

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

Use willChangeValueForKey and didChangeValueForKey from NSKeyValueObserving

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

Use dynamic. See Swift Type Compatibility

You can also use the dynamic modifier to require that access to members be dynamically dispatched through the Objective-C runtime if you’re using APIs like key–value observing that dynamically replace the implementation of a method.

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

And property getter and setter is called when used. You can verify when working with KVO. This is an example of computed property

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
        }
    }
}
Rollway answered 19/1, 2016 at 10:11 Comment(0)
U
4

Currently Swift does not support any built in mechanism for observing property changes of objects other than 'self', so no, it does not support KVO.

However, KVO is such a fundamental part of Objective-C and Cocoa that it seems quite likely that it will be added in the future. The current documentation seems to imply this:

Key-Value Observing

Information forthcoming.

Using Swift with Cocoa and Objective-C

Unnecessarily answered 11/6, 2014 at 5:42 Comment(2)
Obviously, that guide you reference now describes how to do KVO in Swift.Marja
Yep, now implemented as of September 2014Pilgrimage
M
4

One important thing to mention is that after updating your Xcode to 7 beta you might be getting the following message: "Method does not override any method from its superclass". That's because of the arguments' optionality. Make sure that your observation handler looks exactly as follows:

override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [NSObject : AnyObject]?, context: UnsafeMutablePointer<Void>)
Mcdaniel answered 8/9, 2015 at 12:42 Comment(1)
In Xcode beta 6 it requires: override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>)Incipient
J
4

This may be prove helpful to few people -

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

I had used KVO in this way in Swift 3. You can use this code with few changes.

Judo answered 13/7, 2017 at 9:14 Comment(0)
A
4

Overview

It is possible using Combine without using NSObject or Objective-C

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

Note: Needs to be used only with classes not with value types.

Code:

Swift Version: 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

Output:

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

Refer:

Ayurveda answered 17/11, 2019 at 9:43 Comment(1)
iOS 13 +? Other people were just ignored?Normand
A
1

Another example for anyone who runs into a problem with types such as Int? and CGFloat?. You simply set you class as a subclass of NSObject and declare your variables as follows e.g:

class Theme : NSObject{

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

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

}
Atrophied answered 28/1, 2016 at 9:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.