KVO หรือ Key-Value Observing


KVO หรือ Key-Value Observing เป็นวิธีการหนึ่งในการเฝ้าดูการเปลี่ยนแปลงของ properties ของ object ใดๆ
เป็นกระบวนการที่มีประโยชน์อย่างยิ่งสำหรับการเขียน Code ที่มีการเชื่อมโยงกันระหว่าง Model กับ Controller โดยที่ Model ไม่ต้องมีส่วนเกี่ยวของใดๆ กับ Controller เลย

mvc
ภาพจากส่วนหนึ่งของ Slide จาก CS193p ของ stanford.edu CS193P

ตัวอย่างเช่นผมมี Model แบบนี้

@interface MagicCalculateModel : NSObject
/**
  * เมื่อมีการ set magicInput เข้ามาจะใช้เวลายาวนานในการคำนวณ
  * เมื่อคำนวณเสร็จก็จะ update magicOutput ให้
  * ให้ผู้ใช้ observe ดูที่ magicOutput เพื่อรอรับผลการคำนวณ
  */
@property (nonatomic,strong) NSString* magicInput;
@property (nonatomic, readonly) NSString* magicOutput;
@end

ถ้าเกิดมีการ set ค่าไปที่ magicInput โดยตรงแล้วผมต้องการ output ทันทีคงจะเป็นไปไม่ได้เพราะว่า output ของ model นี้ใช้เวลาในการคำนวณ, เราจะวน for ไปถาม magicOutput เพื่อขอดูว่าได้ผลลัพท์แล้วหรือยังก็คงไม่ไหว(UI กระตุกแน่นอนถ้าทำแบบนี้)

เพราะฉะนั้นการแก้ไขของเราก็คือการนำ KVO เข้ามาใช้โดยการให้ Controller ของเราไป Observe ที่ Model
ด้วย – (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath …;
โดยถ้า Controller จุดที่เหมาะสมที่จะไป addObserver ก็คือตอนที่สร้าง model ขึ้นมาจากตัวอย่างคือ add ที่ lazy instantiation เลย ,โดย KeyPath จะต้องตรงกับชื่อของ property ที่เราต้องการ observe

@interface ViewController ()
...
@property (nonatomic,strong) MagicCalculateModel* calculator;
...
@end

@implementation ViewController
@synthesize calculator = _calculator;
-(MagicCalculateModel *)calculator {
    if (!_calculator) _calculator = [[MagicCalculateModel alloc] init];
    // ให้ calculator รับเรา(ในที่นี้คือ Controller) เป็น observer
    // สำหรับ property ที่ชื่อว่า 'magicOutput'
    // ในกรณีที่มีการเปลี่ยนแปลงค่า'ใหม่' เกิดขึ้น
    // context คืออะไรผู้เขียนก็ยังไม่เคยใช้จ้า
    [_calculator addObserver:self
                  forKeyPath:@"magicOutput"
                     options:NSKeyValueObservingOptionNew
                     context:nil];

    return _calculator;
}

และเมื่อเราเป็นคนก่อเราก็ต้องเป็นคนเก็บ เมื่อ model นี้ไม่ได้ใช้เมื่อไหร่ก็ต้อง remove observer ออกด้วยเป็นเงาตามตัวเหมือนกับการ retain , release

-(void)setCalculator:(MagicCalculateModel *)calculator {
    if (_calculator != calculator) {
        // ยกเลิกการ observe เดิม
        [_calculator removeObserver:self
                         forKeyPath:@"magicOutput"];
        _calculator = calculator;
        //และถ้าค่าใหม่ไม่ใช่ nil ก็ขอ observe ต่อด้วย
        //ถ้า _calculator เป็น nil บรรทัดนี้ก็ไม่ error และก็จะไม่เกิดอะไรขึ้นด้วย
        [_calculator addObserver:self
                      forKeyPath:@"magicOutput"
                         options:NSKeyValueObservingOptionNew
                         context:nil];
    }
}

แล้วก็ไปรอรับผลจากการ observe ได้ที่ method observeValueForKeyPath:… ทำการ check ว่าให้ผลจากที่ model และ keyPath ที่เราต้องการหรือเปล่าเพื่อความถูกต้องถ้าเกิดมีการ observe ค่าไว้หลายตัว

-(void)observeValueForKeyPath:(NSString *)keyPath
                     ofObject:(id)object
                       change:(NSDictionary *)change
                      context:(void *)context {
    if (object == self.calculator && [keyPath isEqualToString:@"magicOutput"]) {
        self.output.text = self.calculator.magicInput;
    }
}

Code ตัวอย่าง
เอวัง

  1. ทำไมไม่ใช้ delegate
  2. เพราะว่า delegate รองรับ ‘ผู้รับ’ แค่ตัวเดียว(ส่วนใหญ่) ,KVO มี’ผู้รับ’กี่คนก็ได้ เช่นในกรณีที่ Model นี้ share กันใช้โดยหลาย Controller ,แล้ว KVO นั้น model ก็ไม่ต้อง implement อะไรเพิ่มเลยแต่ delegate จำเป็นต้อง implement เพิ่ม

  3. ทำไมไม่ใช้ NSNotificationCenter
  4. คนละจุดประสงค์การใช้กัน NSNotificationCenter เหมือนกระบอกกระจายเสียงที่ต้องการตะโกนให้ทั้ง Application ได้ยิน แล้วใครจะมารับก็ได้มีปัญหาการใช้ key ซ้ำซ้อนกันได้สูงกว่า, แต่ว่า KVO แค่จาก object หนึ่งไปอีก object หนึ่งเท่านั้น, อีกอย่าง NSNotificationCenter Model ก็ต้อง implement เพิ่มเหมือนกัน

Advertisements