One of the things I like most about Apple’s iOS SDK is the consistent and easy-to-use API they provide.  Across all their different frameworks there’s a pattern at work that makes using their classes easy to understand.  This is due in part to the simplicity for configuring those objects.  In most cases you don’t need to call cryptic methods to setup or teardown classes.  If you want to change a label’s font, you just set a property.  If you want to add a new set of tabs to a UITabBarController, you simply have to assign an array of view controllers to the “viewControllers” property and away you go.

This up front simplicity comes at a cost however: somebody had to write code to intercept the setting of those properties and update the view to reflect the changes made.  Fortunately for those developers at Apple, Cocoa and Cocoa Touch makes this simple through the use of Key-Value-Observing (KVO).  If you know how to use it, you can do the same thing in your applications as well.  Read on to see what I do to make implementing KVO in my projects easy and intuitive.

What is Key-Value-Observing?

Key-Value-Observing, or more commonly KVO, is a mechanism by which you can observe changes to keys and their values bound to an object.  It lets you make arbitrary objects aware of changes made to values in other objects that are important to you.  When those values are changed at all, theobserveValueForKeyPath:ofObject:change:context: method is invoked on the listener, or “observer”.  In essence, it lets you listen to when people change properties on your object.

You might say “Really? That’s all it does? I can get the same by creating a custom setter!” and you might be right, but there’s several reasons why creating custom setters to handle update logic might be undesirable:

  1. It’s easy to create one or two custom setters, but quite tedious if you have lots of properties you want to observe.
  2. Custom setters only work if you are monitoring properties under your control, but impossible if you want to monitor changes made to other objects you encapsulate.
  3. You have to implement your own retain/release/copy cycles manually, instead of simply using@synthesize.
  4. You have to manually keep track of what the old and new values of a property, if they’re important to you.
  5. Your code looks like dog food.

Using KVO in a class essentially buys you flexibility and easy-to-read code, and with a few general practices can be made easy to read and easy to extend.

Setting up KVO the hard way

Many developers getting started with KVO, myself included, typically start by assigning observers for one or two keyPath properties in either their init or viewDidLoad methods.  Then, within theirobserveValueForKeyPath:ofObject:change:context: method they will have code to respond to those setting changes.

As an example lets assume we’re creating a UIView subclass that has a UIColor property that controls the display of several subviews.

 
 
 
 
 
 

Objective-C

 
1
@property (nonatomic, copy) UIColor *color;

When that property changes lets make our view update all our sub-views to the appropriate color. We could do this with a custom setter, like so:

 
 
 
 
 
 

Objective-C

 
1
2
3
4
5
6
7
8
9
10
11
- (void)setColor:(UIColor *)color {
    if (color != color) {
        [color release];
        color = [color retain];
 
        mTitleLabel.textColor = color;
        mDescriptionLabel.textColor = color;
        [mButton setTitleColor:color
                      forState:UIControlStateNormal];
    }
}

For the purposes of this example though, lets utilize KVO for this.

 
 
 
 
 
 

Objective-C

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
- (id)initWithFrame:(CGRect)frame {

    self = [super initWithFrame:frame];
    if (self) {

        [self addObserver:self
               forKeyPath:@"color"
                  options:0
                  context:nil];
    }
}
 

- (void)observeValueForKeyPath:(NSString*)keyPath
                      ofObject:(id)object
                        change:(NSDictionary*)change
                       context:(void*)context
{
    if ([keyPath isEqualToString:@"color"]) {
        mTitleLabel.textColor = self.color;
        mDescriptionLabel.textColor = self.color;
        [mButton setTitleColor:self.color
                      forState:UIControlStateNormal];
    }
 
    else {
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                              context:context];
    }
}

In the initWithFrame: method we add ourselves as an observer of the keyPath “color”, passing in the default options.  We also define a method that Cocoa Touch uses to notify the observer, in this case “self”, when a keyPath has been changed.  This method is invoked for different properties that are changed, so you should always pass the message along to “super” if this method is called for a property you didn’t explicitly add as an observer.

While the above code does look longer than the custom setter equivalent, in the long-run this code is much easier to extend over the life of your class, especially as you add more complex behaviors.

Keeping Track of Your Key Paths

As my experience with KVO improved, so did my techniques for managing my observers, responding to keyPath value changes, and generally keeping my code clean.  One of the things I found was that it is useful to keep a list of the keyPaths you’re observing so that you can conveniently iterate over them programmatically.

To illustrate my point, consider an example where you expose multiple subviews as properties, and when some values change you would like to perform some sort of redraw operation.

 
 
 
 
 
 

Objective-C

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
- (id)initWithFrame:(CGRect)frame {

    if (ObservableKeys == nil) {
        ObservableKeys = [[NSSet alloc] initWithObjects:
                          @"titleLabel.font",
                          @"descriptionLabel.font",
                          // ...
                          nil];
    }
 
    self = [super initWithFrame:frame];
    if (self) {

        for (NSString *keyPath in ObservableKeys)
            [self addObserver:self
                   forKeyPath:keyPath
                      options:0
                      context:nil];
    }
}
 

- (void)dealloc {
    for (NSString *keyPath in ObservableKeys)
        [self removeObserver:self
                  forKeyPath:keyPath];
    [super dealloc];
}
 


- (void)observeValueForKeyPath:(NSString*)keyPath
                      ofObject:(id)object
                        change:(NSDictionary*)change
                       context:(void*)context
{
    if ([ObservableKeys containsObject:keyPath]) {
        [self redrawView];
    }
 
    else {
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                              context:context];
    }
}

As you can see not only would this be impossible to do with custom setters, but we managed to respond to several different property changes using a single block of code.

Additionally, keeping a static NSSet object around with a list of the keys you’re observing is convenient for several reasons:

  1. It lets you easily add your object as an observer of several keyPaths without having to copy/paste code.
  2. Your generated code is tighter because you don’t have to worry about inline strings being littered throughout your class.
  3. If you change the name of a keyPath, it happens in a much more limited area.

There are other advantages, but this is just the tip of the iceberg.

Implementing KVO in a UITableViewCell Subclass

In order to illustrate how to build a more complicated feature using KVO, lets create a UITableViewCell subclass that makes it easy to create a gloss icon in your table cells, just like the iTunes App Store app.  If we’re successful we should be able to create a table cell whose icon looks like the screenshot to the right.

Instead of requiring us to worry about the details of rendering the gloss icon overlay every time we want to display an app icon, lets create a property on our table cell that encapsulates this behavior.  Ideally we should be able to set the app icon’s original image once, and the UITableViewCell imageView.image property should be set to the appropriate content.  We could do this with a custom accessor, but I’d like to show you an easy way to do this with KVO.

AppStoreItemView.h

 
 
 
 
 
 

Objective-C

 
1
2
3
4
5
6
7
8
#import <UIKit/UIKit.h>
 
@interface AppStoreItemView : UITableViewCell
 
@property (nonatomic, retain) UIImage *icon;
@property (nonatomic) BOOL iconNeedsGlossEffect;
 
@end

You can see from our interface declaration that we’re adding two properties for the icon image itself, and a boolean indicating whether or not we want the gloss effect.  Now lets look at the implementation.

AppStoreItemView.m

 
 
 
 
 
 

Objective-C

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
#import "AppStoreItemView.h"
#import <QuartzCore/QuartzCore.h>
 
static NSSet * ObservableKeys = nil;
static NSString * const IconKeyPath = @"icon";
static NSString * const IconNeedsGlossEffectKeyPath
    = @"iconNeedsGlossEffect";
 
@interface AppStoreItemView ()
 
- (UIImage*)glossImageForImage:(UIImage*)image;
 
@end
 
@implementation AppStoreItemView
 
@synthesize icon;
@synthesize iconNeedsGlossEffect;
 
- (id)initWithStyle:(UITableViewCellStyle)style
    reuseIdentifier:(NSString *)reuseIdentifier
{
    // Setup our set of observable keys only once
    if (nil == ObservableKeys) {
        ObservableKeys = [[NSSet alloc] initWithObjects:
                          IconKeyPath,
                          IconNeedsGlossEffectKeyPath,
                          nil];
    }
 
    self = [super initWithStyle:UITableViewCellStyleSubtitle
                reuseIdentifier:reuseIdentifier];
    if (nil != self) {
        // Add observers for each of the keyPaths we care about
        for (NSString *keyPath in ObservableKeys)
            [self addObserver:self
                   forKeyPath:keyPath
                      options:(NSKeyValueObservingOptionOld |
                               NSKeyValueObservingOptionNew)
                      context:nil];
         
        self.imageView.layer.cornerRadius = 10.0;
        self.imageView.layer.masksToBounds = YES;
    }
     
    return self;
}
 
- (void)dealloc {
    // Tidy up and remove all the observers when the view is destroyed
    for (NSString *keyPath in ObservableKeys)
        [self removeObserver:self
                  forKeyPath:keyPath
                     context:nil];
     
    [super dealloc];
}
 
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    // If the keyPath being changed isn't one we care about,
    // pass this up to super and return immediately.
    if (![ObservableKeys containsObject:keyPath]) {
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                              context:context];
        return;
    }
     
    // Fetch the old and new objects from the change dictionary
    id oldObject = [change objectForKey:NSKeyValueChangeOldKey];
    id newObject = [change objectForKey:NSKeyValueChangeNewKey];
     
    // Detect null values, since the changed object references
    // are object references.
    if ([NSNull null] == (NSNull*)oldObject)
        oldObject = nil;
    if ([NSNull null] == (NSNull*)newObject)
        newObject = nil;
     
    // Update imageView when the icon is changed
    if ([IconKeyPath isEqualToString:keyPath]) {
        self.imageView.image = [self glossImageForImage:newObject];
    }
     
    // If the gloss effect is changed, refresh the gloss image
    else if ([IconNeedsGlossEffectKeyPath isEqualToString:keyPath]) {
        self.imageView.image = [self glossImageForImage:self.icon];
    }
}
 
- (UIImage*)glossImageForImage:(UIImage*)image {
    // Code goes here to create the gloss image
    // and return the resulting image.  See the
    // sample code for the full example.
    return mergedImage;
}
 
@end

This code may seem complicated, but in the end it is very straight-forward. I’m also introducing a few additional concepts here that I’d like to call out specifically.

Observing Options

KVO allows you to specify different options for when you’re observing a set of keyPaths. It’s a bit-mask, so you can enable multiple options simultaneously.  In this example I’m indicating that I want to be notified of both the old and new values of these properties.  When the icon is changed, the change dictionary supplied to our callback tells us both the old and new values of the property.

Using static strings for keyPaths

This is a pattern I use which helps for simplicity of code, as well as a defensive coding mechanism. By declaring static NSString variables for each keyPath you’re observing, it ensures you won’t accidentally type the wrong keyPath somewhere in your code. It also gives you a single place for defining, or changing, the keyPath you’re interested in observing.

Returning early from the observer callback

In this example I’m returning early in the block, when the keyPath is not in my set of “ObservableKeys”. This makes the callback code cleaner since you eliminate one extra nested “if” block, and helps to prevent mistakes.

Detecting null values in the observer callback

I find it useful to extract values from the change dictionary as early as possible, and then cast those values as needed.

Using private methods for observer callback behaviour

If you’re not careful your observer callback can get quite large.  It’s powerful to be able to have a single point where complex behaviours and patterns can be established between different properties, but you should make sure it doesn’t become overgrown with logic. It’s better to start out by pushing complex logic into private methods, and simply invoke that from within your callback.

Where to go for help

Apple’s own documentation, as always, is a good source of information but it can tend to be a lot to take in.  At this point you should have a basic understanding of how KVO works, and how you can use it in your application.  You can also download the sample application created above at Github.

Once you get started, finding answers to your questions becomes simpler when you’ve gotten the hang of KVO.  Good luck, and happy coding!

Back to Basics: Using KVO的更多相关文章

  1. iOS---观察者模式之--->KVO

    文章结构如下: Why? (为什么要用KVO) What? (KVO是什么) How? ( KVO怎么用) More (更多细节) 原理 自己实现KVO 在我的上一篇文章浅谈 iOS Notifica ...

  2. Objective-C之KVC、KVO

    1,KVC(键值编码)  Key Value Coding 1.1在C#中,可以通过字符串反射来获取对象,从而对对象的属性进行读写,Object-C中有同样的实现,通过字符串(属性名词)对对象的属性进 ...

  3. OS 如何选择delegate、notification、KVO?

    原文链接:http://blog.csdn.net/dqjyong/article/details/7685933 前面分别讲了delegate.notification和KVO的实现原理,以及实际使 ...

  4. KVC 和 KVO

    KVC 键值编码    全称是Key-value coding,翻译成键值编码.它提供了一种使用字符串而不是访问器方法去访问一个对象实例变量的机制.        1.通过key(成员变量的名称)设置 ...

  5. 11. KVC And KVO

    1. KVC And KVO  的认识 KVC/KVO是观察者模式的一种实现  KVC全称是Key-value coding,翻译成键值编码.顾名思义,在某种程度上跟map的关系匪浅.它提供了一种使用 ...

  6. KVO __ 浅谈

    KVO :Key-Value Observing 它提供一种机制,当指定的对象的属性被修改后,则对象就会接受到通知.简单的说就是每次指定的被观察的对象的属性被修改后,KVO就会自动通知相应的观察者了. ...

  7. iOS开发系列--Objective-C之KVC、KVO

    概述 由于ObjC主要基于Smalltalk进行设计,因此它有很多类似于Ruby.Python的动态特性,例如动态类型.动态加载.动态绑定等.今天我们着重介绍ObjC中的键值编码(KVC).键值监听( ...

  8. delegate、notification、KVO场景差别

    delegate: 编译器会给出没有实现代理方法的警告 一对一 使用weak而不是assign,或者vc消失时置为nil 可以传递参数,还可以接收返回值 notification: 编译期无法排错 一 ...

  9. IOS学习之初识KVO

    什么是KVO? KVO(Key-Value Observing)键值观察,是一种通过对对象的某一个属性添加观察者,一旦这个属性值发生变化,就会通知当前观察者的一种机制. 该如何使用? 1.注册,指定被 ...

随机推荐

  1. c#将Excel数据导入到数据库的实现代码(OleDb)

    sing System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web ...

  2. LINUX 内核代码 errno 错误代码提示 /include/asm/errno.h

    首先在自己的程序中#include<errno.h> 添加打印errno的语句 printf("errno is: %d\n",errno); 根据errno的值查错. ...

  3. Tempdb对SQL Server性能的影响

    转载文章,原文地址:http://www.cnblogs.com/laodao1/archive/2010/04/15/1712395.html1.SQL Server系统数据库介绍 SQL Serv ...

  4. 重拾C++ 基础知识总结(一)

    1.使用gcc编译c++文件报错 proc1.cc:(.text+0x14): undefined reference to `std::cout' C++程序使用gcc命令只能编译,不能链接库文件 ...

  5. linux c下几种定时器实现

    1.alarm n秒后触发一次,不是循环的2.setitimer 可以发出3种信号给自己,3.timerfd 这个接口基于文件描述符,通过文件描述符类似epoll那种的可读事件进行超时通知,能够被用于 ...

  6. [javascript]event属性

    1.clientX和clientY clientX和clientY是事件发生时,鼠标离浏览器可视文档区域左上角的位置 2.offsetX和offsetY offsetX和offsetY是事件发生时,鼠 ...

  7. delphi 创建数据库配置文件(TIniFile)

    一.有必要了解INI文件的结构: ;注释 [小节名] 关键字=值 ... ---- INI文件允许有多个小节,每个小节又允许有多个关键字, “=”后面是该关键字的值. ---- 值的类型有三种:字符串 ...

  8. iPhone 被同步到 Mac上后 如果不希望更新到Mac上在哪里删除?

    前往文件夹   /Users/用户名/Library/Application Support/MobileSync  直接删除  就行了(同时要倾倒废纸篓). 目前iPhone链接Mac 后  不让 ...

  9. mschedule 简单linux进程管理(树莓派)

    树莓派是神奇的机器,CPU和内存都少的可怜,但体积小功耗低,在上面搞些动搞些西其实也挺有意思,挺好玩的.装的是pidara,基本服务没有精简多少,先cat一下CPU和RAM. [able@raspi ...

  10. CSS中定位position

    毋庸置疑的是,pisition是css中是最重要的属性之一. 一共有四种定位方式,static.relative.absolute.fixed. 默认的定位方式static 页面中所有的元素默认都是s ...