iOS – Understanding KVO In Swift

Hello Readers, CoolMonkTechie heartily welcomes you in this article.

In this article, We will learn about KVO in Swift. KVO, which stands for Key-Value Observing, is one of the techniques for observing the program state changes available in Objective-C and Swift. This article demonstrates the KVO importance in Objective-C and Swift with practical example.

A famous quote about learning is :

” One learns from books and example only that certain things can be done. Actual learning requires that you do those things.”

So Let’s begin.


Overview

The KVO concept is simple:

” When we have an object with some instance variables, KVO allows other objects to establish surveillance on changes for any of those instance variables. “

KVO is a practical example of the Observer pattern. What makes Objective-C (and Obj-C bridged Swift) unique is that every instance variable that you add to the class becomes observable through KVO right away!

But in the majority of other programming languages, such a tool doesn’t come out of the box – we usually need to write additional code in the variable’s setter to notify the observers about the value changes.

Swift has inherited KVO from Objective-C, so for a full picture we need to understand how KVO works in Objective-C.


KVO in Objective-C

Consider we have a class named Person with properties name and age

@interface Person: NSObject
 
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) NSInteger age;
 
@end

The objects of this class now are able to communicate the changes of the properties through KVO, but with no additional code – this feature comes for free !

So the only thing we need to do is to start the observation in another class:

@implementation SomeOtherClass

- (void)observeChanges:(Person *)person {
    [person addObserver:self
             forKeyPath:@"age"
                options:NSKeyValueObservingOptionNew
                context:nil];
}

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSKeyValueChangeKey,id> *)change
                       context:(void *)context {
    if ([keyPath isEqualToString:@"age"]) {
        NSNumber *ageNumber = change[NSKeyValueChangeNewKey];
        NSInteger age = [ageNumber integerValue];
        NSLog(@"New age is: %@", age);
    }
}

@end

That’s it! Now every time the age property changes on the Person we’ll have New age is: ... printed to the log from the observer’s side.

As we can see, there are two methods involved in KVO communication.

The first is addObserver:forKeyPath:options:context:, which can be called on any NSObject, including Person. This method attaches the observer to an object.

The second is observeValueForKeyPath:ofObject:change:context: which is another standard method in NSObject that we have to override in our observer’s class. This method is used for handling the observation notifications.

There is a third method, removeObserver:forKeyPath:context:, which allows us to stop the observation. It’s important to unsubscribe from notifications if the observed object outlives the observer. So the subscription just has to be removed in the observer’s dealloc method.

Now, let’s talk about the parameters of the methods used in KVO.

- (void)addObserver:(NSObject *)observer 
         forKeyPath:(NSString *)keyPath
            options:(NSKeyValueObservingOptions)options
            context:(nullable void *)context;

Here, The method have the following parameters:

  • observer is the object that will be receiving the change notifications. Usually, we provide self in this parameter, as the addObserver: is called from inside own instance method.
  • keyPath is a string parameter that in simplest case is just the name of the property we want to observe. If the property references a complex object hierarchy it can be a set of property names for digging into that hierarchy: "person.father.age"
  • options is an enum that allows for customizing what information is delivered with notification and when it should be sent. Available options are NSKeyValueObservingOptionNew and NSKeyValueObservingOptionOld, which control whether to include the most recent and the previous values respectively. There is also NSKeyValueObservingOptionInitial for triggering the notification right after the subscription, and NSKeyValueObservingOptionPrior for diffing the changes in a collection, such as insertions of deletions in NSArray.
  • context is a reference to object of an arbitrary class, which can be helpful for identifying the subscription in certain complex use cases, such as when working with CoreData. In most other cases we simply provide nil here.

The method we used for handling the update notifications

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSKeyValueChangeKey,id> *)change
                       context:(void *)context

Here, The method have the following parameters :

  • keyPath is the same string value we provided when attaching the observer. We may ask why it is provided here as well. The reason is that we may be observing multiple properties at once, so this parameter can be used to distinguish the notifications for one property from another.
  • object is the observed object. Since we can observe changes on more than one object, this parameter allows us to identify who’s property has changed.
  • change is the dictionary with information about the changed value. Based on the NSKeyValueObservingOptions we provided upon subscription, this dictionary may contain the current value under key NSKeyValueChangeNewKey, previous value for NSKeyValueChangeOldKey, and the “diff” information when observing changes in a collection: NSKeyValueChangeIndexesKey and NSKeyValueChangeKindKey
  • context is the reference provided upon subscription. Again, used for proper observation identification and in most cases can be ignored.


When KVO does not work

Even though KVO looks like magic, there is nothing extraordinary behind it. In fact, we can have direct access to its internals, which are hidden by default.

The trick is how Objective-C generates setter for properties. When we declare a property like

@property (nonatomic, assign) NSInteger age;

The factual setter generated by Objective-C is equivalent to the following:

- (void)setAge:(NSInteger)age {
    [self willChangeValueForKey:@"age"];
    _age = age;
    [self didChangeValueForKey:@"age"];
}

And if we explicitly define the setter without calling these willChangeValueForKey and didChangeValueForKey.

- (void)setAge:(NSInteger)age {
    _age = age;
}

… the KVO will stop working for this property.

So basically, these two methods willChangeValueForKey and didChangeValueForKey allow KVO to deliver the updates to the subscribers, and the developer can opt-out by omitting those calls from the setter.

It is important to understand that every @property synthesised by Objective-C adds a hidden instance variable with _ prefix.

For example, @property NSInteger age; generates an instance variable with the name _age that can be accessed just like the property:

self.age = 25;
self._age = 25;

The difference is that self.age = 25; triggers setter setAge:, while self._age = 25; changes the stored variable directly.

This means that even if the KVO is enabled for the age property, the KVO communication will work correctly for self.age = 25; and won’t deliver an update for self._age = 25;

Another way to break free from KVO is to not use @property in the first place, but instead store the instance variable in the anonymous category of the class:

@interface Person () {
    NSInteger _privateVariable;
}
@end

For such variables, Objective-C does not generate setter and getter, thus not enabling KVO.


KVO in Swift

Swift has inherited the support for the KVO from Objective-C, but unlike the latter, KVO is disabled in Swift classes by default.

Objective-C classes used in Swift keep KVO enabled, but for a Swift class we need to set the base class to NSObject plus add @objc dynamic attributes to the variables:

class Person: NSObject {
    @objc dynamic var age: Int
    @objc dynamic var name: String
}

There are two APIs available in Swift for Key-Value Observing: the old one, which came from Objective-C, and the new one, which is more flexible, safe and Swift-friendly.

Let’s start with the new one:

class PersonObserver {

    var kvoToken: NSKeyValueObservation?
    
    func observe(person: Person) {
        kvoToken = person.observe(\.age, options: .new) { (person, change) in
            guard let age = change.new else { return }
            print("New age is: \(age)")
        }
    }
    
    deinit {
        kvoToken?.invalidate()
    }
}

As we can see, the new API is using a closure callback for delivering the change notification right in the place where the subscription started.

This is more convenient and safe because we no longer need to check the keyPathobject or context, – no other notifications are delivered in that closure, just the one we’ve subscribed on.

There is a new way for managing the observation lifetime – the act of subscribing returns a token of type NSKeyValueObservation which has to be stored somewhere, for example, in an instance variable of the observer class.

Later on, we can call invalidate() on that token to stop the observation, like in the deinit method above.

The final change is related to the keyPath. String was error-prone because when we rename a variable the compiler won’t be able to tell us that the keyPath now leads to nowhere. Instead, this new API is using Swift’s special type for keyPath, which allows the compiler to verify the path is valid.

The options parameter has just the same set of options as in Objective-C. If we need to provide more than one option, we just bundle them in an array: options: [.new, .old]

The old API is also available, although it maintained all its disadvantages, so we encourage you to use the new API instead.

Here is the old one:

class PersonObserver: NSObject {
    
    func observe(person: Person) {
        person.addObserver(self, forKeyPath: "age",
                           options: .new, context: nil)
    }
    
    override func observeValue(forKeyPath keyPath: String?,
                               of object: Any?,
                               change: [NSKeyValueChangeKey : Any]?,
                               context: UnsafeMutableRawPointer?) {
        if keyPath == "age",
           let age = change?[.newKey] {
             print("New age is: \(age)")
        }
    }
}

The old API requires the observer to be an NSObject descendant as well. We also need to verify the keyPathobject, and context, since other notifications are also delivered in this method, just like in Objective-C.

That’s all about in this article.


Conclusion

In this article, We understood about KVO in Swift. This article described about the KVO importance in Objective-C and Swift with practical example.

Thanks for reading ! I hope you enjoyed and learned about the KVO Concepts in iOS. Reading is one thing, but the only way to master it is to do it yourself.

Please follow and subscribe us on this blog and and support us in any way possible. Also like and share the article with others for spread valuable knowledge.

If you have any comments, questions, or think I missed something, feel free to leave them below in the comment box.

Thanks again Reading. HAPPY READING!!???

Loading

Leave a Comment