在项目,可能会有需求需要监听 NSMutableArray 的变化,例如在可变数组中加入、删除或者替换了元素,我们需要根据这些变化来更新UI或者做其他操作。

那么如何来监听呢?

方法1,使用 mutableArrayValueForKey: 代理,这样,我们在获取定义的数组属性时不再使用其 getter 方法,而是通过代理方法获取数组属性后,再对数组进行增删改的操作。这是最简单高效的方法,使用示例如下:

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

#import "ViewController.h"
#import "Student.h"
#import "ViewModel.h"

static void *xxcontext = &xxcontext;

@interface ViewController ()

@property (strong, nonatomic) ViewModel *viewModel;

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

self.viewModel = [[ViewModel alloc] init];
[self.viewModel addObserver:self forKeyPath:@"students" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:xxcontext];
}

- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}

- (void)dealloc {
[self.viewModel removeObserver:self forKeyPath:@"students"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context == xxcontext) {
if ([keyPath isEqualToString:@"students"]) {
NSNumber *kind = change[NSKeyValueChangeKindKey];
NSArray *students = change[NSKeyValueChangeNewKey];
NSArray *oldStudent = change[NSKeyValueChangeOldKey];
NSIndexSet *changedIndexs = change[NSKeyValueChangeIndexesKey];

NSLog(@"kind: %@, students: %@, oldStudent: %@, changedIndexs: %@", kind, students, oldStudent, changedIndexs);
}
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}

- (IBAction)addStudent:(id)sender {

Student *st1 = [[Student alloc] initWithFirstName:@"carya" lastName:@"liu"];
Student *st2 = [[Student alloc] initWithFirstName:@"cici" lastName:@"liu"];
Student *st3 = [[Student alloc] initWithFirstName:@"ted" lastName:@"liu"];

NSArray *students = @[st1, st2, st3];
[self.viewModel addStudents:students];
[self.viewModel addStudent:st3];
}

@end

我们在 viewDidLoad 函数中注册了 viewModel 对象 students 属性的监听者,同时在 dealloc 中移除了监听者。

ViewModel 中定义了 students 属性,如下:

@property (copy, nonatomic) NSMutableArray *students;

主要看看 ViewModel 中添加Student对象的函数实现:

1
2
3
4
5
6
7
8
9
10
11
- (NSMutableArray *)studentsArray {
return [self mutableArrayValueForKey:NSStringFromSelector(@selector(students))];
}

- (void)addStudents:(NSArray *)students {
[[self studentsArray] addObjectsFromArray:students];
}

- (void)addStudent:(Student *)student {
[[self studentsArray] addObject:student];
}

上面的代码中,我们使用代理 mutableArrayValueForKey 替代 getter 来获取 students 属性,- (NSMutableArray *)studentsArray 类似于一个 getter 方法。

ViewController 中定义的 - (IBAction)addStudent:(id)sender 是一个按钮事件,点击该按钮,看看日志输出:

1
2
3
4
5
6
7
8
9
10
11
12
2015-08-25 08:17:29.216 KVO[4235:252903] kind: 2, students: (
"<Student: 0x7fe463c89960>"
), oldStudent: (null), changedIndexs: <NSIndexSet: 0x7fe463cac290>[number of indexes: 1 (in 1 ranges), indexes: (0)]
2015-08-25 08:17:29.217 KVO[4235:252903] kind: 2, students: (
"<Student: 0x7fe463c3c120>"
), oldStudent: (null), changedIndexs: <NSIndexSet: 0x7fe463d308c0>[number of indexes: 1 (in 1 ranges), indexes: (1)]
2015-08-25 08:17:29.218 KVO[4235:252903] kind: 2, students: (
"<Student: 0x7fe463c1fb60>"
), oldStudent: (null), changedIndexs: <NSIndexSet: 0x7fe463d308c0>[number of indexes: 1 (in 1 ranges), indexes: (2)]
2015-08-25 08:17:29.218 KVO[4235:252903] kind: 2, students: (
"<Student: 0x7fe463c1fb60>"
), oldStudent: (null), changedIndexs: <NSIndexSet: 0x7fe463d0c0b0>[number of indexes: 1 (in 1 ranges), indexes: (3)]

从上面的示例可以看出:

  • 使用 addObjectsFromArray 向数组中添加元素时,每往数组中添加一个元素都会触发一次 KVO 的执行
  • 从 KVO 的通知中可以获取触发这次通知的操作类型,这里是往数组中添加元素,kind 的数值是 2,即 NSKeyValueChangeInsertion
  • 从 KVO 的通知中还可获取到新添加的对象以及该对象在数组中的索引值

如果我们想一次性往数组中加入多个元素(如 addObjectsFromArray ),但是只想让其触发一次 KVO 的执行,怎么操作呢?

答案是使用 NSMutableArray 的这个接口 - (void)insertObjects:(NSArray *)objects atIndexes:(NSIndexSet *)indexes, 将 ViewModel 的 - (void)addStudents:(NSArray *)students 函数实现修改成如下:

1
2
3
4
5
6
- (void)addStudents:(NSArray *)students {
// [[self studentsArray] addObjectsFromArray:students];

NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange([self studentCount], [students count])];
[[self studentsArray] insertObjects:students atIndexes:indexSet];
}

再次运行,就会发现 KVO 只触发了一次。

方法2,遵从属性的 KVC 规则,实现对应操作的方法。先看看对于 NSMutableArray 类型 KVC 方面的文档:

In order to be key-value coding compliant for a mutable ordered to-many relationship you must implement the following methods:

-insertObject:in<Key>AtIndex: or -insert<Key>:atIndexes:. At least one of these methods must be implemented. These are analogous to the NSMutableArray methods insertObject:atIndex: and insertObjects:atIndexes:.

-removeObjectFrom<Key>AtIndex: or -remove<Key>AtIndexes:. At least one of these methods must be implemented. These methods correspond to the NSMutableArray methods removeObjectAtIndex: and removeObjectsAtIndexes: respectively.

-replaceObjectIn<Key>AtIndex:withObject: or -replace<Key>AtIndexes:with<Key>:. Optional. Implement if benchmarking indicates that performance is an issue.

The -insertObject:in<Key>AtIndex: method is passed the object to insert, and an NSUInteger that specifies the index where it should be inserted. The -insert<Key>:atIndexes: method inserts an array of objects into the collection at the indices specified by the passed NSIndexSet. You are only required to implement one of these two methods.

对于上述文档,个人简单理解为,要实现 NSMutableArray 的增删改操作遵从 KVC 的规则,需要实现其对应方法:

  • 增操作 -insertObject:in<Key>AtIndex: 或者 -insert<Key>:atIndexes:
  • 删操作 -removeObjectFrom<Key>AtIndex: 或者 -remove<Key>AtIndexes:
  • 改操作 -replaceObjectIn<Key>AtIndex:withObject: 或者 -replace<Key>AtIndexes:with<Key>:

并将这些接口暴露给调用者,在对数组进行操作时需使用上述实现的接口。

对于方法1中提到的实例,如果想让往 ViewModel 的 students 数组添加元素时触发 KVO 通知的发送,需像如下代码实现上述方法:

1
2
3
4
5
6
7
8
9
10
11
- (NSUInteger)studentCount {
return [self.students count];
}

- (void)insertStudents:(NSArray *)array atIndexes:(NSIndexSet *)indexes {
[self.students insertObjects:array atIndexes:indexes];
}

- (void)insertObject:(Student *)object inStudentsAtIndex:(NSUInteger)index {
[self.students insertObject:object atIndex:index];
}

ViewController 中添加新元素的按钮事件实现更改成如下:

1
2
3
4
5
6
7
8
9
10
11
- (IBAction)addStudent:(id)sender {
Student *st1 = [[Student alloc] initWithFirstName:@"carya" lastName:@"liu"];
Student *st2 = [[Student alloc] initWithFirstName:@"cici" lastName:@"liu"];
Student *st3 = [[Student alloc] initWithFirstName:@"ted" lastName:@"liu"];
NSArray *students = @[st1, st2, st3];
// [self.viewModel addStudents:students];
// [self.viewModel addStudent:st3];

NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange([self.viewModel studentCount], [students count])];
[self.viewModel insertStudents:students atIndexes:indexSet];
}

编译重新运行示例,控制台日志输出如下:

1
2
3
4
5
2015-08-25 20:50:35.324 KVO[6483:294703] kind: 2, students: (
"<Student: 0x7f8508684de0>",
"<Student: 0x7f8508684e00>",
"<Student: 0x7f850863d690>"
), oldStudent: (null), changedIndexs: <NSIndexSet: 0x7f850863d6b0>[number of indexes: 3 (in 1 ranges), indexes: (0-2)]

从日志中可以看出,KVO 的通知只触发了一次,从 KVO 的通知中获取到了新添加的3个元素以及新元素在数组中索引。

监听 NSMutableArray 内元素变化的 KVO 总结:

  1. 使用 mutableArrayValueForKey: 代理来获取 NSMutableArray 属性
  2. 实现 NSMutableArray 属性遵从 KVC 规则的方法,并将这些方法暴露给调用者

另外,从 NSKeyValueCoding Protocol Reference 中可以看出,对于 NSMutableSet 和 NSMutableOrderedSet 也有其代理方法。

参考:


如果觉得本文对你有帮助,就请用微信打赏我吧^_^