[Cocoa] 코코아 바인딩을 활용한 주소록 앱 만들기

바인딩으로 주소록 만들기

코코아 바인딩을 활용하면 엄청나게 많은 양의 코드를 줄일 수 있다는 것은 많이 알려져 있다. 바인딩을 활용하는 가장 흔한 예는 슬라이더와 텍스트필드를 창에 올려두고 모델 객체의 값과 연결하여 숫자값이나 슬라이더 조작이 어떻게 맞물려 돌아가는지를 살펴보는 예인데, 오늘은 조금더 발전한 형태의 예를 만들어 보도록 하겠다. 예를 들면 주소록 같은 거 말이다.

주소록에 실컷 정보를 입력하고 저장할 수 없으면 문제가 되는데, 저장하는 기능은 다음 기회에 붙여 보도록 하겠다. (사실 NSCoding 프로토콜을 따르도록만 모델을 만들면 쉽게 구현할 수 있다.) 오늘은 단지 각각의 정보를 연결하는 데 초점을 맞추므로, 여러가지 제약 사항 등은 고려하지 않는다.

주소록 앱의 모양

주소록 앱은 대략 다음과 같은 모양을 가진다고 가정한다.

1.앱의 화면 상단에는 테이블 뷰가 표시된다. 테이블 뷰에는 사람의 사진, 이름 (성과 이름을 붙여서 표시), 전화번호, 생일을 표시한다. 우리는 이를 마스터 UI라고 하겠다.

  1. 테이블 뷰 아래에는 선택된 사람의 보다 상세한 정보를 표시할 수 있게끔한다. 사진 / 성 / 이름 / 생일 / 주소 / 전화번호 /이메일 주소 등을 표시하도록 한다. (물론 이는 나중에 원하는대로 손쉽게 수정할 수 있다.) 이걸 디테일 UI라고 하자.

  2. 또한 새로운 사람을 추가하는 버튼과, 현재 선택된 사람을 삭제하는 버튼이 있다.

데이터 모델

수록된 사람은 Person 이라는 클래스로 표현하기로 한다. 앞서 언급한 내용들은 모두 키밸류 코딩/키밸류 옵저빙을 따라야 하므로, 모두 프로퍼티로 정의하자. 프로젝트를 시작하여, 새 코코아 앱을 만들기 시작한다. 맨 먼저, Person 클래스를 만든다.

#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (copy, nonatomic) NSString *firstName, *lastName, *phone, *email, *address;
@property (readonly, nonatomic) NSString *fullName;
@property (strong, nonatomic) NSDate *birthday;
@property (strong, nonatomic) NSImage *photo;
@end

Person의 구현부에서는 선언한 프로퍼티의 접근자를 생성하고, 초기화메소드를 정의해준다. 만약 아카이빙으로 저장하고 싶다면, NSCoding에 필요한 -initWithCoder:-encodeWithCoder: 메소드를 구현해주어야 한다.

또한 fullName 은 별도의 인스턴스 변수에 저장되는 문자열객체가 아니라, firstName과 lastName을 조합하여 만들어지는 프로퍼티이므로, 이 키 값의 의존성에 대해서도 명시하여야 한다. 의존성을 명시하면 firstName, lastName 둘 중 하나를 수정하면 자동을 fullName이 수정되어 UI에 반영된다.

#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (copy, nonatomic) NSString *firstName, *lastName, *phone, *email, *address;
@property (readonly, nonatomic) NSString *fullName;
@property (strong, nonatomic) NSDate *birthday;
@property (strong, nonatomic) NSImage *photo;
@end

#import "Person.h"
@implementation Person
#pragma mark - accessors
@synthesize firstName = _firstName, lastName = _lastName, phone = _phone, email = _email, address = _address, birthday = _birthday, photo = _photo;

-(NSString *)fullName
{
    return [NSString stringWithFormat:@"%@ %@",self.firstName, self.lastName];
}

#pragma mark - define key dependancy
+(NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
    if ([key isEqualToString:@"fullName"]) {
        return [NSSet setWithObjects:@"firstName", @"lastName", nil];
}
return [super keyPathsForValuesAffectingValueForKey:key];
}

-(id)init {
    self = [super init];
    if (self) {
        .... 각 속성들을 초기화;
    }
}
@end

앱 델리게이트

앱 델리게이트 (혹은 메인 윈도를 관리하는 컨트롤러)에서는 각각의 사람을 관리하는 배열 객체를 하나 준비한다. 이 역시 바인딩에서 참조해야하므로 프로퍼티로 선언해 둔다.

// AppDelegate.h
//...
@property (strong, nonatomic) NSMutableArray *persons;
//...

// AppDelegate.m
//...
@synthesize persons = _persons;
//...
-(NSMutableArray*)persons
{
    if(!_persons) _persons = [NSMutableArray array];
    return _persons;
}

//...

인터페이스 빌더

인터페이스 빌더에서는 다음과 같은 모양대로 (이미지 필요) 만든다. 마스터 부분은 테이블 뷰로 구현하고, 디테일 뷰는 박스를 만들고 그 속에 필드들을 만들어 넣는다.

[이미지는 다음 기회에]

테이블뷰는 4개의 칼럼을 가지고 이는 각각 이름/전화번호/생일/사진을 표시하도록 한다. (테이블에 이미지를 표시하고자 한다면, imageCell을 테이블 셀에 끌어 놓으면 된다.)

생일은 날짜로 표시되므로 NSDateFormatter를 칼럼안에 추가해 준다. (그러면 텍스트 필드 셀 안에 데이터 포매터가 들어가게 된다.) 날짜 포매터는 attriibute 인스펙터에서 date style을 medium style로 지정해주자.

NSArrayController 추가하기

주소록에 등재된 사람의 목록은 AppController.persons 배열로 나타난다. 이를 바인딩에 활용하기 위해 NSArrayController를 인터페이스 빌더로 끌어다 놓는다. 이 배열 컨트롤러의 이름은 identity inspector의 identity 섹션 중 Label 에서 변경할 수 있다. PersonsController라고 배열 컨트롤러의 이름을 붙인다. 이 이름은 프로그래밍 상에서는 별다른 영향이 없다. 대신에 binding inspector에서 이름을 찾기가 수월해지도록 쓴다. 특히, 이런 종류의 앱에서는 배열 컨트롤러를 여러 개 사용하는 경우가 종종 있으므로 혼동을 피하기 위해 이름으로 구분짓는다고 생각하면 된다.

배열 컨트롤러를 모델에 바인딩하기

다음은 Attribute Inspector를 열고 content Object 섹션을 보자. 여기서는 배열에 각 원소가 되는 모델 객체를 정의한다. Entity는 코어데이터에서 사용하는 엔티티와 연결될 때 쓰는 것이고 우리는 Class로 지정한다. 디폴트로 아마 NSMutableArray가 표시되는데, Person을 입력해주자.

다시 binding inspector를 보자. (일곱번째 탭) 이 중 Controller Content 섹션에서 Content Array 를 선택한다. 이 컨트롤러는 인원 전체를 담는 배열의 내용을 관리하므로, bind to에 체크한 다음, 대상을 AppDelegate로 지정한다.

그 아래에는 Controller Key, Model Key Path, Value Transformer 항목이 있다. 이 중에서 Model Key Path에 persons 를 입력한다. (즉 AppDelegate의 persons 배열이 배열 컨트롤러의 컨텐츠가 된다.)

테이블 뷰를 컨트롤러에 바인딩하기

다음은 마스터 영역의 테이블뷰의 각 칼럼을 컨트롤러를 통해 모델에 바인딩해보자. 테이블뷰를 연결하는 것이 아니라, 테이블 뷰 내의 칼럼을 바인딩 해야 한다. 따라서 인터페이스 빌더에서 테이블 뷰를 한 번 클릭한다. 이 때는 테이블 뷰가 아닌 테이블 뷰를 감싸는 스크롤뷰가 선택된다. 한 번 더 클릭한다. 이번에는 테이블 뷰가 선택된다. 한 번 더 선택하고자 하는 칼럼을 다시 클릭한다. 이제 테이블 뷰 칼럼이 선택된다.

정확한 객체를 선택해서 바인딩해야 하므로, 인터페이스 빌더 왼쪽에 있는 객체 목록 창에서 선택하는 것이 보다 도움이 될 수 있다. “사진”열을 선택했다면 이제 바인드 할 차례.  바인딩 인스펙터에서 Value 섹션을 열어 PersonsController에 바인드한다. Controller key 에 arrangedObjects를 입력하고  Model Key Path에 photo를 입력한다.

즉, 배열의 각 원소 중에서 photo 키가 사진 칼럼에 대응된다는 이야기다. 같은 방식으로 테이블 뷰의 나머지 칼럼에 대해서도 알맞는 키에 바인딩을 시켜준다.

  • 이름 –> arrangedObject > fullNam
  • 전화번호 –> arrangedObject > phon
  • 생일 –> arrangedObject > birthday

디테일 UI를 컨트롤러에 바인드하기

디테일 UI는 마스터 (테이블 뷰)에서 선택된 개별 Person 모델 객체의 컨텐츠와 연결하여 기존 데이터를 편집하므로, PersonsController의 selection에 연결되어야 한다. 같은 방식으로 디테일 영역의 UI 요소들은 각각 PersonsController에 바인드 될 때, Controller Key = selection, Model Key Path = 대응될 person 객체의 키로 바인드한다.

추가 / 삭제를 위한 버튼

각각의 버튼은 control-drag로 PersonsController에 연결하여 add: / remove:와 연결해주면 된다.

모든 작업이 끝났다. (실제로 이를 만드는데는 10분이 채 걸리지 않았다.) 앱을 빌드하고 실행하면 창이 열린다. Add 버튼을 클릭하면 테이블 뷰에 빈 선택열이 생긴다. (만약 Person 클래스의 초기화 과정에서 초기 값을 넣었다면 그 값이 표시될 것이다.) 디테일 영역에서 이를 편집하면, 바로 그 내용이 테이블 뷰에 반영된다.

몇 가지 조정할 것들

  1.  테이블 뷰 중 이름 칼럼은 속성 인스펙터에서 editable 속성을 해제해준다. (Full Name은 read-only 이므로 편집할 수 없으니까.)
  2. 생일 입력은 NSDatePicker를 쓰는게 속편하다.
  3. 이미지 웰의 editable 속성을 체크해주면 파인더 등에서 그림 파일을 끌어다 추가할 수 있게 된다.

추가 사항

  1. 파일을 저장하는 것은 간단히 앱 번들 내에 적당한 파일이름을 하나 주고 NSKeyedArchivier를 사용해서 persons 객체를 아카이빙하여 저장하면 된다. 그 전에 Person 클래스가 NSCoding 프로토콜을 따르도록 메소드를 추가 구현해야 한다.
  2. 파일 저장 기능을 넣을 때는 앱이 시작될 때, 파일로부터 역시 NSKeyedUnArchiver를 사용하여 persons  객체를 복원하면 된다.
  3. 예제 파일에서는 창을 닫으면 앱이 종료되고, 종료되기 직전에 파일을 저장하는 코드를 추가했다.