1. 程式人生 > >iOS如何為NSMutableArray新增KVO

iOS如何為NSMutableArray新增KVO

在專案,可能會有需求需要監聽 NSMutableArray 的變化,例如在可變陣列中加入、刪除或者替換了元素,我們需要根據這些變化來更新UI或者做其他操作。

那麼如何來監聽呢?

方法1,使用 mutableArrayValueForKey: 代理,這樣,我們在獲取定義的陣列屬性時不再使用其 getter 方法,而是通過代理方法獲取陣列屬性後,再對陣列進行增刪改的操作。這是最簡單高效的方法,使用示例如下:

#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物件的函式實現:

- (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 是一個按鈕事件,點選該按鈕,看看日誌輸出:

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 函式實現修改成如下:

- (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 通知的傳送,需像如下程式碼實現上述方法:

- (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 中新增新元素的按鈕事件實現更改成如下:

- (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];
}

編譯重新執行示例,控制檯日誌輸出如下:

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 也有其代理方法。

參考: