본문 바로가기

프로그래밍/iOS

KVC(Key-Value Coding) & KVO(Key-Value Observing)

  1. KVC(Key-Value Coding)
    - NSObject에 NSKeyValueCoding.h 카테고리로 포함되어있어 NSObject만 상속받더라도 사용 가능

    - NSDictionary처럼 문자열로 된 key를 사용하여 객체의 속성값에 간접 접근하는 방법.

    - 코드가 간결해지고 유지보수가 쉬움

    - 클래스간 의존성이 낮아짐

    - 객체 형태로만 주고받음
       1) ex: NSInteger → NSNumber형으로 return하므로  intValue 사용하여 형변환 필요.

    - 직접 접근하는 방법에 비해 느림(많은 과정을 거침)

    - 객체 접근 과정
       1) key로 들어온 문자열과 같은 이름의 메소드 확인.
       2) 비슷한 getter 메소드 확인(없을 경우 이 이름의 property가 없음)
       3) 배열과 관련된 index, count 등의 메소드 확인(있을 경우 배열이나 셋과 관련된 객체)
       4) 인스턴스 변수에서 탐색
       5) 없을 경우 valueForUndefinedKey 메소드 호출. 따로 오버라이드 하지 않으면 예외 발생.
  2. KVO(Key-Value Observing)
    - NSObject에 NSKeyValueObserving.h 카테고리로 포함되어있어 NSObject만 상속받더라도 사용 가능

    - 어떤 객체의 key나 keyPath를 옵저버에 등록하면 등록한 객체의 변경에 대한 알림을 받을수 있음
       1) key: 객체에 접근(ex: @"object")
       2) keyPath : 객체에 포함된 객체에 접근(ex: @"object.child")

    - 주로 model과 controller 사이의 통신에 유용하게 사용됨.

    - 대상 객체는 KVC 규칙을 준수해야 함

    - KVC에서 사용하는 방식으로 Key의 값이 변경되어야 알림을 받을 수 있음
       1) setValue:forKey: 메소드 사용
       2) 접근자 메소드 사용(self 등)

    - 사용 순서
       1) 옵저버 등록

    //현재 뷰 컨트롤러에 옵저버 등록
    [self addObserver:self forKeyPath:@"myKey" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
    
    //다른 뷰 컨트롤러에 옵저버 등록
    ViewController2 *vc2 = [[ViewController2 alloc] init];
    [vc2 addObserver:self forKeyPath:@"myKey" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
    
    //옵저버 등록 옵션(NSKeyValueObservingOptions)
    typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
        NSKeyValueObservingOptionNew, //변경 된
        NSKeyValueObservingOptionOld, //변경 전
        NSKeyValueObservingOptionInitial, //즉시 감시 시작(옵저버 등록 메소드가 리턴되기도 전부터)
        NSKeyValueObservingOptionPrior //변경 전, 후로 2번 통보
    };

       2) 알림 받기

    //옵저버로부터 알림 받기
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {   
        if([keyPath isEqualToString:@"myKey"]) {
            NSString *oldStr = [[change valueForKey:NSKeyValueChangeOldKey] stringValue];
            NSString *newStr = [[change valueForKey:NSKeyValueChangeNewKey] stringValue];        
        }   
    }
    
    //change에 들어오는 NSDictionary의 키(NSKeyValueChangeKey)
    typedef NSString * NSKeyValueChangeKey NS_STRING_ENUM;
     NSKeyValueChangeKindKey; //변경 종류
     NSKeyValueChangeNewKey; //변경 된 값
     NSKeyValueChangeOldKey; //변경 전 값
     NSKeyValueChangeIndexesKey; //변경된 값의 index
     NSKeyValueChangeNotificationIsPriorKey; //NSKeyValueObservingOptionPrior 여부
    
    //어떻게 변경되었는지에 대한 값들(NSKeyValueChangeKindKey)
    typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
        NSKeyValueChangeSetting, //값 셋팅
        NSKeyValueChangeInsertion, //값 삽입
        NSKeyValueChangeRemoval, //값 삭제
        NSKeyValueChangeReplacement, //값 대체
    };

       3) 옵저버 제거

    //나를 관찰하는 나를 제거
    [self removeObserver:self forKeyPath:@"myKey"];
    
    //_vc2를 관찰하는 나를 제거
    [_vc2 removeObserver:self forKeyPath:@"myKey"];
    


    - Observing방식 변경(Manual / Auto)

    //메소드를 오버라이딩하여 사용 + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key { BOOL autoMatic = NO; if([key isEqualToString:@"manualKey"]) { autoMatic = NO; } else { autoMatic = [super automaticallyNotifiesObserversForKey:key]; } return autoMatic; }


    - Manual로 값의 변경을 통보하는 방법
    - (void)setChangeValue:(NSString *)changeStrValue {
        [self willChangeValueForKey:@"changeValue"]; //변경 될 값에 대한 통보
        strValue = changeStrValue; //값 변경
        [self didChangeValueForKey:@"changeValue"]; //값이 변경되었다고 통보
    
        //이런 방식으로 변경되므로 option이 NSKeyValueChangeNotificationIsPriorKey일 경우
        //변경 전, 후로 통보를 받을 수 있음
    }
    


    - 의존 키 추적
       1) fullName = firstName + lastName
       2) firstName이나 lastName 하나만 바뀌어도 fullName이 변경되나 인식되지 않음.
       3) fullName에 영향을 주는 키 패스를 설정, firstName/lastName 중 하나만 변경되어도 인식

    + (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
        NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
        
        if([key isEqualToString:@"fullName"]) {
            NSSet *affectingKeys = [NSSet setWithObjects:@"lastName", @"firstName", nil];
            keyPaths = [keyPaths setByAddingObjectsFromSet:affectingKeys];
        }
        
        return keyPaths;
        
        return [NSSet setWithObjects:@"lastName", @"firstName", nil];
    }


  3. 관련 실습
  4. - observer의 key값으로 구조체가 들어간 경우
       1) 스칼라 변수(ex:NSInteger)가 들어가는 것 처럼 객체로 변경되어 저장(CGPoint → NSPoint)
       2) 값을 받아 올 경우 형 변환 필요

    //ex: CGPoint
    CGPoint point = [[change valueForKey:NSKeyValueChangeNewKey] CGPointValue];
    

       3) Enum: 정해놓은 변호가 출력.
       4) Custom 구조체: 16진수 값으로 변경됨(NSData 비슷하게 로그에 찍힘)

    - 객체를 observing하는데 관찰하는 객체를 없애면 observer가 dealloc 되는가?
       1) removeObserver를 호출하기 전에는 dealloc되지 않음

    - 관찰하는 객체가 변경될 경우 변경을 알리는 메소드는 어디서 호출하는가?(observingCenter같은게 있나?)
       1) 따로 관리해주는 center같은 객체는 없고 변경 즉시 변경된 객체에서 직접 호출.

    - Collection Operator(@sum, @min, @max 등)

    //array같은 단일 객체에 대해서는 self를 붙여줘야 동작
    [_array valueForKeyPath:@"@max.self"];
    
    //객체(모델) 내부 프로퍼티에 접근할 경우 명시해줘야 함
    [_animal valueForKeyPath:@"@sum.numOfLegs"];
    


    observeValueForKeyPath의 change를 통해 가져온 변경 값의 클래스 타입
       1) NSString
          1] 
    빈 값 : __NSCFConstantString

    2] 숫자, 영문 : NSTaggedPointerString
         3] 한글 : __NSCFString
    2) 구조체 : NSConcreteValue
    3) NSNumber 관련 : __NSCFNumber
    4) 컴파일 시에 자료형을 확정할 수 없기 때문에 런타임 헤더 형식을 가지는 것 같음