coding with objc & swift

停止重写setter方法,你应该用KVO

| Comments

本文译自:Stop overriding setters and just use KVO

KVO(Key-Value-Observing)是一个充满分歧的API,是的,至少可以这么说。尽管它有(文档记录的)缺陷,但在想知道某个属性值是否变化的时候,我个人更倾向于使用它。但是我和许多开发者交流过(包括我在Tumblr的iOS开发同事),大多数人都更倾向于采用重写setter的方法。这里有一种情况,我认为KVO会比重写setter的方法稍微好一点。

假设你有一个自定义的视图控制器的子类并带有一个属性。修改这个属性的值会导致视图控制器的视图或子视图发生一些变化。Tumblr的代码库中正好有一个这样的例子:

1
2
3
4
5
6
7
8
- (void)setContainerScrollable:(BOOL)containerScrollable {
    if (_containerScrollable != containerScrollable) {
        _containerScrollable = containerScrollable;

        self.container.scrollEnabled = containerScrollable;
        self.tableView.scrollEnabled = !containerScrollable;
    }
}

看起来很简单,对不对?现在,你可以简单地执行以下操作:

1
2
TMContainerViewController *controller = [[TMContainerViewController alloc] init];
controller.containerScrollable = YES;

这里有一个问题。由于我们在控制器的视图被加载前调用了自定义的setter,所以它根本没有达到预期的效果。最好的情况下,setter里面的子视图会是nil,调用不会有任何效果。最坏的情况,如果在setter里面引用了self.view,会导致view被过早地加载,可能发生不可预料的情况。

那么,如何才能解决这个问题?一种方法是,在view被加载后再调用一次setter方法,并且要防止自定义逻辑被提前执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)setContainerScrollable:(BOOL)containerScrollable {
    if (_containerScrollable != containerScrollable) {
        _containerScrollable = containerScrollable;

        if ([self isViewLoaded]) {
            self.container.scrollEnabled = containerScrollable;
            self.tableView.scrollEnabled = !containerScrollable;
        }
    }
}

- (void)viewDidLoad {
    // View set-up

    self.containerScrollable = self.isContainerScrollable;
}

这应该可以工作,但调用一个getter方法,将它的返回值传递给同名属性的setter方法,在我看来不是特别优雅。如果提炼出我们的自定义逻辑,把它到一个单独的私有实例方法中,又怎样呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (void)updateViewsForContainerScrollability {
    self.container.scrollEnabled = self.isContainerScrollable;
    self.tableView.scrollEnabled = !self.isContainerScrollable ;
}

- (void)setContainerScrollable:(BOOL)containerScrollable {
    if (_containerScrollable != containerScrollable) {
        _containerScrollable = containerScrollable;

        if ([self isViewLoaded]) {
            [self updateViewsForContainerScrollability];
        }
    }
}

- (void)viewDidLoad {
    // View set-up

    [self updateViewsForContainerScrollability];
}

这是一个很好地解决方案,并会按预期工作。话虽这么说,让我们来看看另一种方法,用KVO来解决同样的问题。

以下是我们处理KVO通知回调的代码:

1
2
3
4
5
6
7
8
9
10
11
12
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change
                       context:(void *)context {
    if (context == TMContainerViewControllerKVOContext) {
         if (object == self && [keyPath isEqualToString:@"containerScrollable"]) {
            self.container.scrollEnabled = self.isContainerScrollable;
            self.tableView.scrollEnabled = !self.isContainerScrollable;
        }
    }
    else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

现在,我们仍然面对同样的问题:需要确保代码在view被加载后以及属性值每次被更改后都能够被执行。幸运地是,NSKeyValueObservingOptionInitial提供了这样的特性。

1
2
3
4
5
6
7
- (void)viewDidLoad {
    // View set-up

  [self addObserver:self forKeyPath:@"containerScrollable"
            options:NSKeyValueObservingOptionInitial
            context:TMContainerViewControllerKVOContext];
}

由于我们不再重写setter方法,该属性值的更改可以完全独立于view的初始化。当view被配置好后,我们设置了一个观察者,对应的回调会立即被该属性的初始值触发,并且之后不论何时只要属性被修改了,都会触发相同的回调。

KVO代码可能会很凌乱,使用不当也会导致问题,也的确不是解决所有问题的最佳工具。但是如果你要问我,这就是一个相当不错的,它能够派上用场的例子。

Comments