Key-Value Observing P1

Giới thiệu

Trong Objective-C và Cocoa framework, có một số cách để các object có thể liên lạc với nhau thông qua các events:

  • NSNotification & NSNotificationCenter
  • Callbacks
  • Delegates
  • Key-Value Observing

kvo-featured.png

Chúng ta sẽ đi tìm hiểu Key-Value Coding và Key-Value Observing để sử dụng chúng trong code của chúng ta một cách hiệu quả. Đầu tiên mình sẽ đi qua phần KVC trước.

Key-Value Coding

KVC cho phép bạn truy cập đến một property của object trực tiếp không cần thông qua setter hay dot annotation. Bạn xem qua ví dụ sau về class Children:

 @interface Children : NSObject

@property (nonatomic, strong) NSString *name;

@property (nonatomic) NSUInteger age;

@end

Mình khởi tạo và gán giá trị cho 1 instance của children sử dụng dot annotation

    Children *child = [Children new];
    child.name = @"Duong";
    child.age = 26;

Mình có thể sử dụng KVC để truy cập đến các property của child

    [child setValue:@"Huu Duong" forKey:@"name"];
    [child setValue:@30 forKey:@"age"];

Các class muốn sử dụng KVC thì phải conform informal protocol NSKeyValueCoding. Một điều tuyệt vời là NSObject đã có conform sẵn protocol này nên chúng ta kế thừa từ NSObject thì việc sử dụng KVC rất dễ dàng. Một điểm chú ý khi dùng KVC là key phải được viết chính xác để tránh crash app.

Observing for Property Changes

Bây giờ chúng ta sẽ tập trung vào phần chính là Key-value Observing. Các bước cần làm khi implement KVO:

  • Class để được observer phải thoả mãn KVC và có thể send notification tự động hoặc manually.
  • Class observer phải implement method observeValueForKeyPath:ofObject:change:context:

Chúng ta sẽ observe các thay đổi của children object. Ở viewWillAppear, mình sẽ thêm vào method observeValueForKeyPath:ofObject:change:context:

- (void)viewWillAppear:(BOOL)animated {
    [self.child addObserver:self
            forKeyPath:@"age"
               options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
               context:nil];
    [self.child addObserver:self
                  forKeyPath:@"name"
                     options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                     context:nil];
}

Các tham số của hàm trên có ý nghĩa:

  • addObserver: Đây là class sẽ observe, thường là self và mình sẽ implement method đi kèm.
  • forKeyPath: đây là string của key hoặc key path của property mà chúng ta muốn observe. Như ví dụ trên có thể dùng key là "name" hoặc keyPath là "child.name".
  • options: Các điều kiện observe, chúng ta có thể dùng một hoặc nhiều các NSKeyValueObservingOptions, có thể dùng các toán tử logic để kết hợp nhiều NSKeyValueObservingOptions với nhau.
  • context: Đây là con trỏ dùng như là một unique identifier để phân biệt. Thường thì chúng ta sẽ không dùng đến trường này (set nil cho nó).

Tiêp theo chúng ta cần implement method observeValueForKeyPath:ofObject:change:context: method. Việc này có một nhược điểm khá lớn khi số lượng property bạn muốn observe nhiều. Bạn phải viết code check cho từng trường hợp.

(void)observeValueForKeyPath:(NSString *)keyPath
                     ofObject:(id)object
                       change:(NSDictionary *)change
                      context:(void *)context{

    if ([keyPath isEqualToString:@"name"]) {
        NSLog(@"The name of the child was changed.");
        NSLog(@"%@", change);
    }

    if ([keyPath isEqualToString:@"age"]) {
        NSLog(@"The age of the child was changed.");
        NSLog(@"%@", change);
    }
}

Đoạn code ở trên rất đơn giản, chúng ta sẽ filter dựa trên keyPath, ở đây mình observe 2 key là "name" và "age" và chỉ log ra các sự thay đổi nếu có.

Sau khi thiết lập xong observe, giờ mình cần làm cho giá trị của property thay đổi. Ở viewDidApear mình gọi:

-(void)viewDidAppear:(BOOL)animated {
    [self.child setValue:@20 forKey:@"age"];
}

Chạy lại và chúng ta có log console như sau:

 2016-10-25 23:50:55.231 KVOTest[798:20825] The age of the child was changed.
2016-10-25 23:50:55.234 KVOTest[798:20825] {
    kind = 1;
    new = 20;
    old = 26;
}

Ở đây chúng ta có thể biết được oldValue và newValue của property. Vậy là ta đã observe được mỗi khi name hoặc age của object child bị thay đổi.

Observing multi objects

Chúng ta sẽ đi đến một vấn đề khác, trường hợp ở trên bạn chỉ có 1 đối tượng children bây giờ chúng ta thử observing 2 object children cùng 1 lúc, Code thay đổi như sau:

 (void)viewDidLoad {
    [super viewDidLoad];
    // init 2 children objects
    self.child1 = [Children new];
    self.child1.name = @"Duong";
    self.child1.age = 30;
    self.child2 = [Children new];
    self.child2.name = @"Child2";
    self.child2.age = 50;
}

 (void)viewWillAppear:(BOOL)animated {
    [self.child1 addObserver:self
            forKeyPath:@"age"
               options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
               context:nil];
                     context:nil];
    [self.child2 addObserver:self
                  forKeyPath:@"age"
                     options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                     context:nil];
}

(void)viewDidAppear:(BOOL)animated {
    [self.child1 setValue:@20 forKey:@"age"];
    [self.child2 setValue:@40 forKey:@"age"];
}

Chạy chương trình và xem log:

 2016-10-26 00:10:22.584 KVOTest[890:25403] The age of the child was changed.
2016-10-26 00:10:22.585 KVOTest[890:25403] {
    kind = 1;
    new = 20;
    old = 30;
}
2016-10-26 00:10:24.184 KVOTest[890:25403] The age of the child was changed.
2016-10-26 00:10:24.184 KVOTest[890:25403] {
    kind = 1;
    new = 40;
    old = 50;
}

Hehe. Có vấn đề ở đây một chút, giờ làm sao để phân biệt được change nào của object nào? Mình có 2 cách để giải quyết vấn đề trên:

  1. Sử dụng context: "the context is a pointer, and it actually points to itself". Đây là định nghĩa của context, nó là 1 biến con trỏ trỏ đến chính nó. Ta sẽ tạo biến context trong object Children, khai báo ở header file:
   @property void *someContext;

Tiếp đến trong hàm init của Children:

   - (instancetype)init {
    self = [super init];
    if (self) {
        self.name = @"";
        self.age = 0;
        self.someContext = &_someContext;
    }
    return self;
}

Thay đổi khai báo observe 1 chút

   - (void)viewWillAppear:(BOOL)animated {
    [self.child1 addObserver:self
            forKeyPath:@"age"
               options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
               context:self.child1.someContext];
    [self.child2 addObserver:self
                  forKeyPath:@"age"
                     options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                     context:self.child2.someContext];
}

    (void)observeValueForKeyPath:(NSString *)keyPath
                     ofObject:(id)object
                       change:(NSDictionary *)change
                      context:(void *)context{

    if (context == self.child1.someContext) {
        if ([keyPath isEqualToString:@"age"]) {
            NSLog(@"The age of the child 1 was changed.");
            NSLog(@"%@", change);
        }
    }
    else if (context == self.child2.someContext) {
        if ([keyPath isEqualToString:@"age"]) {
            NSLog(@"The age of the child 2 was changed.");
            NSLog(@"%@", change);
        }
    }
    else {
        NSLog(@"UNKNOW");
    }
}
  1. Kiểm tra object trong observeValueForKeyPath:ofObject:change:context:.

Với cách này ta chỉ cần kiểm tra như sau:

 -(void)observeValueForKeyPath:(NSString *)keyPath
                     ofObject:(id)object
                       change:(NSDictionary *)change
                      context:(void *)context{

    if (object == self.child1) {
        if ([keyPath isEqualToString:@"age"]) {
            NSLog(@"The age of the child 1 was changed.");
            NSLog(@"%@", change);
        }
    }
    else if (object == self.child2) {
        if ([keyPath isEqualToString:@"age"]) {
            NSLog(@"The age of the child 2 was changed.");
            NSLog(@"%@", change);
        }
    }
    else {
        NSLog(@"UNKNOW");
    }
}

Cả hai cách đều cho hiệu quả như nhau:

   2016-10-26 00:26:27.659 KVOTest[1105:32090] The age of the child 1 was changed.
2016-10-26 00:26:27.665 KVOTest[1105:32090] {
    kind = 1;
    new = 20;
    old = 30;
}
2016-10-26 00:26:27.666 KVOTest[1105:32090] The age of the child 2 was changed.
2016-10-26 00:26:27.667 KVOTest[1105:32090] {
    kind = 1;
    new = 40;
    old = 50;
}

Kết luận

Như vậy bạn đã có thể hiểu sơ qua Key-Value Observing là gì và làm cách nào để implement được nó một cách cơ bản. Mình sẽ viết tiếp phần 2 về cách kiểm soát được việc notification property change và làm thế nào để observer được một property là array của object.