태그 보관물: Cocoa binding

코코아바인딩 :: 컨트롤러의 컨텐츠 제공방법

컨트롤러 컨텐츠를 제공하기

컨트롤러들은 (당연히도) 관리할 컨텐츠가 필요하며, 컨텐츠를 지정하는 방법에는 여러가지가 있다. 바인딩을 생성하는 코드를 통해서도 가능하며, IB를 통해서도 설정할 수 있다.

컨트롤러 컨텐츠 설정하기

NSObjectController와 그 서브클래스들은 -initWithContent:를 통해서 초기화되며, 만약 컨텐츠 바인딩을 의도한다면 nil을 넘겨도 된다. 혹은 명시적으로 setContent:를 통해서 지정해줄 수도 있다. 보통은 컨트롤러의 컨텐츠 바인딩을 통해서 연결하는 것이 일반적이다. Continue reading “코코아바인딩 :: 컨트롤러의 컨텐츠 제공방법” »

코코아 바인딩이 동작하는 방식

바인딩이 동작하는 방식

  • 키밸류 바인딩으로 바인딩이 만들어지는 방식
  • 언바인딩
  • NSEditor, NSEditorRegistration 프로토콜
  • KVC / KVO
  • 여러 기술들이 조화되는 방식

코코아 바인딩을 지원하는 기술들

코코아 바인딩은 KVC와 KVO를 기반으로 동작하며, 동시에 NSEditor, NSEditorRegistration 프로토콜을 이용한다. 이런 것들이 어떻게 돌아가는지를 이해하기 위해서 그래픽 소프트웨어를 하나 상상해보도록 하자. 이 앱은 사용자가 사각형이나 원 같은 그래픽 오브젝트를 그릴 수 있는데, 각각의 그래픽 오브젝트는 그림자와 같은 프로퍼티를 갖는다. 그림저는 각도나 오프셋같은 속성값을 또 가지고 있다.

1

인스펙터에서는 선택된 객체의 그림자 위치나 각도의 정보가 표시된다. 이 인스펙터의 바인딩 구현은 아래와 같다. 조이스틱뷰와 각각의 텍스트 필드는 NSArrayControllerselection을 통해서 메인 창에서 선택된 그래픽 오브젝트와 묶이게 된다. 배열 컨트롤러는 메인 창에서 생성된 모든 그래픽 객체들의 배열을 가지고 있으며, Graphic 인스턴스는 그림자의 각도와 거리를 저장하는 인스턴스변수를 가지고 있다.

각도를 나타내는 텍스트 필드는 “도”를 기준으로 각도 값을 표현하지만, 실제 각도값은 라디안 단위로 저장되고, 이용된다. 따라서 shadowAngle에 대한 바인딩은 내부적으로 각-라디안 단위를 변환하는 변환기를 이용한다.

2

  1. 사용자는 인스펙터의 각도 필드에 새 값을 입력한다. 텍스트 필드는 NSEditorRegistration 프로토콜을 이용하여 편집이 시작되고 끝났을 때 이를 알려준다. 텍스트 필드의 바인딩은 값 변환기를 써서 각도값을 라디안 값으로 바꿔준다.
  2. KVC를 이용하여, 뷰는 컨트롤러를 통해 모델 객체의 shadowAngle을 변경한다. ("selection.showdowAngle")
  3. KVO를 이용하여 모델 객체는 컨트롤러에게 shadowAngle값이 변경되었음을 알린다.
  4. KVO에 의해 컨트롤러는 조이스틱뷰와 텍스트 필드에게 shadowAngle 값이 변경되었음을 알린다. (뷰가 업데이트된다.)

조금 더

1

어떤 UI객체의 바인딩을 IB에서 셋업하는 경우, 객체를 선택하고 바인딩 인스펙터를 보면, 해당 객체의 바인드 가능한 속성들이 표시된다.

이때 바인드에 체크하고, 바인드할 객체를 선택한다. 해당 객체가 컨트롤인 경우에는 두 가지 키가 필요한데, 하나는 컨트롤러 키, 다른 하나는 모델 키이다. 컨트롤러 키의 경우에는 selection, values, arrangedObjects 등의 키가 올 수 있다. 이는 어떤 모델 객체를 선택할 것인가에 대한 부분이고, 모델 객체 자체의 키에 대해서는 모델 키 패스를 쓴다.

위 그림과 같은 바인딩은 코드로는 다음과 같이 표현한다.

[joystick bind:@"angle" toObject:GraphicController withKeyPath:@"selection.shadowAngle" options:options];
  • @"angle" : 메시지 수신자의 프로퍼티 중에서 바인드할 프로퍼티의 키패스
  • GraphicController: 프로퍼티를 바운드할 객체
  • @"selection.shadowAngle" : 프로퍼티가 바운드될 키패스
  • options: 플레이스 홀더나 값 변환기등의 정보를 담은 사전

조이스틱의 인터페이스 중 다음과 같은 부분이 있다고 하자.

@interface Joystick : NSView
@property (assign, nonatomic) float angle;
@property (weak, nonatomic) id observedObjectForAngle;
@property (strong, nonatomic) NSString* observedKeyPathForAngle;
@property (strong, nonatomic) NSValueTransformer *angleToRadianTransformer;
...
@end

-bind:toObject:withKeyPath:options:는 대략 다음과 같이 구현된다.

@implement Joystick
@synthesize angle, observedObjectForAngle, observedKeyPathForAngle, angleToRadianTransformer;

static void *AngleBindingContext = (void*)@"JoysticAngle";

- (void)bind:(NSString*)binding 
    toObject:(id)observableObject 
    withKeyPath:(NSString*)keyPath 
    option:(NSDictionary*)options
{
    if([binding isEqualToString:@"angle"])
    {
        [observableObject addObserver:self forKeyPath:keyPath
            options:nil context:AngleBindingContext];

        self.observableObjectForAngle = observableObject;
        self.observedKeyPathForAngle = keyPath;
        self.angleToRadianTransformer = nil;
        NSString *vtName = [options objectForKey:@"NSValueTransformerName"];
        if(vtName != nil){
            self.angleValueTransformaer = [NSValueTransformer valueTransformerForName:vtName];
        }
    }
}
...
@end

이 구현코드 조각은 완전한 것은 아니다. 단지 바인딩이 구성될 때 어떤 일들이 일어나는지 정도만을 보여주는 것이다. 컨텍스트를 만들어놓고 이 정보를 전달하고 있음에 주목하라. 이 값은 -observedValueForKeyPath:ofObject:change:context:에 사용되어서 어떤 바인딩으로부터 변경이 감지되었는지를 판단하는 기준이 된다.

변경에 응답하기

뷰 상에서 변경이 발생

위에서 언급한대로, 조이스틱 객체는 컨트롤러에 다음과 같이 바인딩되어있다고 가정한다.

[joystick bind:@"angle" toObject:GraphicaController withKeyPath:@"selection.shadowAngle" options:options]l;

UI 요소에서 업데이트가 일어나면, 해당 정보는 KVC를 통해서 컨트롤러로 전달된다. 대부분의 디테일은 생략하고 다음과 같은 일이 일어난다.

- (void)updateForMouseEvent:(NSEvent *)event
{
    NSNumber* newControllerAngle = nil;
    float newAngleDegrees;
    // <- 새 angle 값을 계산한다.

    self.angle = newAngleDegrees;

    if(self.observedObjectForAngle != nil)
    {
        newControllerAngle = [self.angleToRadianTransformer
                            reverseTranformedValue:@(newAngleDegrees)];
    } else {
        newControllerAngle = @(newAngleDegrees);
    }
    [self.observedObjectForAngle setValue:newControllerAngle forKeyPath:self.observedKeyPathForAngle];
}
....

조이스틱 객체가 컨트롤러와 바인드되면, 조이스틱 자체도 컨트롤러의 해당 키패스에 대한 옵저버로 등록이 된다. 모델에서 변경이 일어나는 경우에는 모델은 KVO를 통해서 컨트롤러에게 변경 통지를 보내고, 다시 컨트롤러는 뷰에게 변경통지를 보낸다. 따라서 J조이스틱은 다음과 같은 메소드를 구현하여, 뷰를 업데이트 할 수 있다.

- (void)observedValueForKeyPath:(NSString *)keyPath
        ofObject:(id)object
        change:(NSDictionary *)change
        context:(void*)context
{
    if(context == AngleObservationContext) // static 변수로 지정해놓은 놈을 쓴다.
    {
        id newAngle = [self.observedObjectForAngle valueForKeyPath:self.observedKeyPathForAngle];
        if((newAngle = NSNoSelectionMarker) || (newAngle == NSNotApplicableMarker) || (newAngle == NSMultipleValuesMarker))
        {
            badSelectionForAngle = YES;
        }
        else {
            badSelectionForAngle = NO;
            if (self.angleToRadianTransformer){
                newAngle = [angleToRadianTransformer transformedValue:newAngle];
            }
        }
        ...
    }
    ...
    [self setNeedsDisplay:YES];
}