Wireframe

[Cocoa] Nib 파일로부터 객체 읽어오기

Nib 파일

nib파일은 인터페이스 빌더에서 생성한 객체들을 직렬화하여 저장하는 파일로, UI를 구성하는 객체들(편의상 인터페이스 객체라 부르겠음)을 저장하게 된다. 이 파일에는 인터페이스 빌더를 통해 추가한 인터페이스 객체들(창, 뷰, 버튼 컨트롤 등)과 이러한 객체들의 세부 설정(스타일, 색상, 폰트 등), 그리고 객체들 간의 연결(connection)정보가 모두 포함된다. 이 모든 인터페이스 객체의 초기화와 설정은 프로그래밍을 통해 코드 상에서 수행할 수 있지만, 인터페이스 빌더를 사용하면 시각적으로 결과물을 즉시 확인할 수 있고 작성해야 하는 코드의 양을 엄청나게 줄일 수 있다. (GUI를 코드로 초기화하는 것은 상당한 양의 코드를 작성해야 하는 큰 일이다.)

AppKit과 UIKit 프레임워크를 사용하여 앱을 작성할 때 nib 파일에 인터페이스 객체를 구성하면 앱이 시동될 때 nib-loading code라는 nib파일을 로드하는 프레임워크의 기능에 의해 자동으로 로딩되고, 내부에 만들어진 모든 객체들이 인스턴스화 되며, 모든 연결이 재구성된다. (앱이 시작할 때 자동으로 로드할 nib 파일은 Info.plist 파일에 기록되게 된다. 앱은 런칭시에 이 파일을 가장 먼저 읽어들이고 이 파일에 써 있는 정보를 바탕으로 초기화된다.)
nib 파일에는 인터페이스 객체외에도 눈에 보이지 않는 객체들, 이를테면 컨트롤러 객체와 같은 것들도 포함된다. 이 파일들 역시 nib-loading code에 의해 인스턴스화된다. 또한 nib 파일은 File's OwnerFirst Responder와 같은 특별한 객체들도 가지고 있는데, 이 객체들은 예외적으로 파일 로딩 과정에서 객체 인스턴스가 만들어지지 않는 것들이다.

File’s Owner

File's Owner 객체는 nib 파일에서 가장 중요한 역할을 담당하는 객체 중 하나인데, 앞서 언급했듯이 파일 로딩 과정에서 객체가 생성되는 것이 아니라, “임의의 객체가 그 자리에 들어갈 수 있게”하는 일종의 placeholder 객체이다. File’s Owner는 nib-loading code에 의해 생성되는 것이 아니라 프레임워크에 의해 (혹은 개발자가 작성한 코드에 의해) nib 파일을 로딩할 때 프레임워크(혹은 개발자의 코드)가 이 자리에 들어갈 객체를 제공해주게 된다. (따라서 기본적으로 1개의 nib 파일만 사용하는 경우, File's Owner 객체는 애플리케이션 객체가 되는 경우가 많다.)
인터페이스 빌더 상에서 File's Owner객체는 어떤 클래스의 객체가 될 것인지 결정할 수 없지만, 여전히 다른 인터페이스 객체나 컨트롤러 객체들과 커넥션을 만들 수 있다. nib-loading code는 넘겨받은 소유자 객체를 이 자리에 넣고 다른 객체들과의 연결을 생성한다.
이 객체가 중요한 이유는 코드와 nib 파일 내부의 객체들 간의 연결 고리가 되기 때문이다. 예를 들어 일반적인 앱에서 기본 nib 파일의 소유자는 애플리케이션이고 애플리케이션 객체는 내부에 앱 델리게이트 프로퍼티를 가지고 있는데, nib 파일 안에 있는 앱델리게이트 객체가 해당 프로퍼티에 할당된다. 즉 Xcode의 템플릿에 기본적으로 만들어져있는 앱델리게이트는 nib 파일로부터 인스턴스화되는 객체이다. (이를 확인해보고 싶다면 -init-initWithCoder:를 각각 오버라이드하여 어떤 메소드가 호출되는지 확인해보면 된다.)

First Responder

First Responder는 이벤트를 받아서 처리하는 객체를 의미하는데, File's Owner와 마찬가지로 placeholder로 정의된다. 다양한 이벤트가 발생할 때 이를 처리할 객체가 디자인 타임에는 정해지지 않기 때문인데, 예를 들어 메뉴의 ‘감추기’를 실행하면 모든 창이 감춰지는게 아니라 현재 활성화된 가장 상위의 창이 가려진다. 즉 메뉴항목은 First Responder에게 hide메시지를 보내지만, 이 메시지를 받는 객체는 그 시점에 결정된다. First Responder 역시 nib 파일을 로딩하는 시점에 객체화되는 것이 아니며, 프레임워크에 의해 그 때 그 때 결정된다.

최상위 객체(Top Level Objects)

최상위 객체는 부모 객체를 가지지 않는 객체들이다. 하나의 nib 파일 내에는 여러 개의 최상위 객체가 있을 수 있다. 메뉴바, 창, 컨트롤러 객체등이 여기에 속한다. 예외적으로 애플리케이션 객체나 File's Owner, First Responder는 부모 객체가 없지만 최상위 객체에 해당하지 않는다.
최상위 객체는 아웃렛을 지정하지 않더라도 코드 상에서 nib파일을 열어 최상위 객체를 로드하는 루틴을 통해 객체를 얻을 수 있다.

이미지와 사운드

nib 파일에는 파일 외부의 이미지나 사운드 파일에 대한 참조를 넣어둘 수 있다. nib 파일이 로딩되면 이 파일들은 이름이 붙은 캐시(named cached)가 되어 메모리에 로드된다. 이 파일의 내용은 UIImage 클래스의 imageNamed: 메소드를 통해서 이미지 객체로 만들거나 NSSoundsoundNamed:를 통해서 사운드 객체로 즉시 인스턴스화 할 수 있다.

nib 파일 디자인 가이드

nib 파일을 읽어들이는 시간을 최소화하면 앱의 로딩 및 실행 속도를 그만큼 빠르게 만들 수 있다. 간단한 앱을 만드는 경우라면 하나의 nib 파일 안에 모든 인터페이스 객체를 넣어둘 수 있겠지만, 앱의 규모가 조금이라도 커지는 경우에는 가능한 여러 개의 nib 파일로 쪼개어 인터페이스를 분리해주는 것이 좋다. 분리된 nib파일은 필요한 시점에 로딩하면 그만큼 초기 로딩 속도를 향상시킬 수 있고, 사용하지 않는 인터페이스 객체를 로드할 필요가 없어서 메모리 점유율도 낮출 수 있다.

nib 객체의 라이프 사이클

nib 파일이 로드되면 그 속에 있는 객체들이 인스턴스화된다. 기본적으로 객체들은 initWithCoder: 메시지를 받게된다. 커스텀 뷰의 경우에는 initWithFrame: 메시지를 받게 된다. (iOS에서는 적용되지 않음) 그리고 그 외 객체들은 init 메시지를 받아 초기화된다.
객체들의 초기화가 끝나면 커넥션(액션, 아웃렛, 바인딩)을 다시 연결한다. 이 모든 과정이 끝나면 적절한 객체에게 awakeFromNib: 메시지가 가게 된다. (iOS의 경우에는 인터페이스 객체만 이 메시지를 받는다.)
로딩이 끝나면 Visible at launch time 속성이 있는 창이 화면에 표시된다.

Nib 파일로부터 생성한 객체의 라이프 사이클 관리

NSBundle이나 NSNib 클래스로부터 nib 파일을 로드하면 프레임워크는 파일 속에 있는 객체의 사본을 만들어 반환한다. 이 객체를 유지하고 사용이 끝난 시점에 해제하는 것은 프로그래머의 책임이다. 통상적으로 최상위 객체에 대해서는 강한 참조를 유지하고, 그외의 객체에 대해서는 약한 참조를 유지하면 된다. (하위 객체들은 상위 객체가 소유하고 있다고 간주한다.)

OSX의 경우 모든 객체가 약한 참조를 지원하는 것은 아닌데, 이런 경우에는 assign 타입의 프로퍼티로 지정할 수 있다.

특히 아웃렛은 정의한 객체에 대해 private한 프로퍼티인 경우가 많은데, 이를 위해서는 카테고리 문법을 사용한 내부 인터페이스에 정의할 수 있다.

최상위 객체는 보다 특별하게 관리한다.

최상위 객체들은 참조수를 하나 더 가지고 생성된다. 앱킷 프레임워크는 nib 객체들이 적절히 해제될 수 있도록 몇 가지 장치를 제공한다.

iOS의 nib 객체들

최상위객체 : nib 파일의 객체들은 참조수 1의 오토릴리즈 객체로 인스턴스화된다. 객체 그래프를 재구성하면서 UIKit은 setValue:forKey:를 통해서 각각의 객체를 연결한다. 따라서 각 객체들의 프로퍼티는 항상 유효함을 보장한다. 하지만 아웃렛으로 지정하지 않은 객체는 유실될 수 있으므로 loadNibNamed:owner:options:를 통해서 얻어온 객체들은 리턴된 배열 자체를 리테인하거나, 개별 객체를 리테인하여 필요한 동안에는 외부에서 해제되지 않도록 유지해야 한다.
메모리경고 : 뷰 컨트롤러 객체가 메모리 경고를 받으면, 현재 사용하지 않고 나중에 다시 만들 수 있는 객체를 해제해야 한다. 종종 이러한 대표 객체는 뷰 컨트롤러의 뷰이다. 뷰가 상위뷰가 없다면(최상위 뷰라면) 폐기된다. 통상 nib 파일 내의 아웃렛들은 리테인되는 참조이므로, 뷰가 폐기된다고해도 아웃렛의 액션들까지 폐기되는 것은 아니다. 따라서 메모리 경고에 대해 적극적으로 대처하기 위해서는 아웃렛을 가지고 있는 커스텀 뷰는 viewDidUnload 메소드를 오버라이드하여 자신의 아웃렛들을 모두 nil로 대체해 주어야 한다.

OSX의 nib 객체들

OSX는 iOS와는 약간 달리 File’s Owner가 모든 최상위 객체들 및 리소스에 대한 책임을 진다. 객체 그래프의 루트 객체를 해제하면 여기에 의존하는 모든 객체가 연이어 해제된다. 그런데 메인 nib파일의 File’s Owner는 애플리케이션이다. 애플리케이션이 종료되면 NSApp 객체가 제거되어버리므로 자동적인 해제가 일어나지 안흔다. 따라서 메인 nib 파일에서도 모든 최상위 객체의 메모리 관리는 해주어야 한다.
(앞서 설명한 두 가지 장치들이 있기는 하다!) Nib파일 속의 객체들의 해제는 프로그래머가 해야하지만 File’s Owner가 NSWindowController인 경우에는 이 객체가 알아서 모든 최상위객체를 해제한다. 만약 그렇지 않은 다른 클래스의 객체를 File’s Owner로 하여 nib 파일을 로딩했다면, file’s owner는 모든 최상위객체에 대해 아웃렛을 가지고, 이들을 해제하는 코드를 포함해야 한다. 이런 형태로 모든 최상위 객체를 관리하지 않으려면 NSNib-instantiateNibWithOwner:topLevelObjects: 메소드를 사용해 최상위 객체로 이루어진 배열을 얻는 방법도 있다.
실제로 다양한 종류의 앱이 있을 수 있고 각각의 경우들을 따져보면 해야할 일이 조금 명확해 질 수 있다. 만약 싱글 윈도우 앱을 만든다고 하면 대부분의 nib 객체들은 앱의 라이프 사이클동안에 계속해서 유지된다고 볼 수 있고 앱이 종료될 때 해제되면 된다. 단, 이 때 dealloc을 호출한다고 해서 nib 객체들이 모두 자동적으로 해제되는 것은 아니다. (최상위 객체들은 모두 수동으로 해제해야 함) 복수의 창을 쓰는 경우는 주로 문서 기반 앱인 경우가 많은데, 이 때 NSWindowsController가 각각의 nib 파일을 소유하고 있다면 메모리 관리 코드를 작성해야 하는 부담은 크게 줄어든다. (거의 할 일이 없음)
어떤 앱들은 상황이 더욱 복잡한데, 별도의 윈도우나 패널등을 만들기 위해 nib 파일을 읽을 때 창의 소유자를 NSWindowController로 정하거나, release when closed 옵션을 체크했다면 대부분의 메모리 관리는 자동으로 이루어진다. 하지만 이런 방법을 사용하지 않는다면 창이 닫히는 시점에 최상위 객체는 모두 해제해야 한다. 예외적으로 인스펙터 창의 경우(레이지 로딩으로 불러들였을 때) 인스펙터 창은 한 번 로드되면 앱의 종료시까지 계속 유지되는 필요가 많으므로 특별히 해제하지 않는다.)

Nib 파일에 대한 프레임워크의 자동화 기능

AppKit과 UIKit은 nib 파일에 대해 프레임워크 수준에서 많은 자동화 기능을 제공하고 있다. 이러한 자동화 기능 중 대표적인 것은 1)앱 런칭시에 메인 nib 파일을 자동으로 읽어 앱의 초기 UI를 생성하고 2)뷰 컨트롤러는 개별적으로 nib 파일을 가져, nib 파일로부터 초기화가 가능하며, 3) 윈도컨트롤러나 도큐멘트 컨트롤러 역시 연동하는 nib 파일을 가질 수 있도록 한다.

메인 nib 파일의 자동 로딩

Info.plist 파일에는 앱의 메인 nib파일의 이름이 기록되어 있고(NSMainNibFile키), 앱은 실행을 시작할 때 이 파일로부터 메인 nib 파일을 읽어들여 초기 인터페이스 객체를 생성한다.

nib 파일을 소유하는 뷰 컨트롤러

UIViewControllerNSViewController 클래스는 연동된 nib 파일을 자동으로 읽어들이는 기능을 제공한다. 뷰 컨트롤러를 만들 때 nib 파일을 지정하면, 뷰 컨트롤러의 뷰에 액세스하려 할 때 자동으로 이 nib 파일을 읽어들인다. iOS에서는 메모리 경고시에 뷰 컨트롤러는 자동으로 nib 파일을 언로드하여 메모리 공간을 확보할 수 있다. (자세한 내용은 뷰 컨트롤러 관련 토픽에서 따로 다루기로 한다.)

윈도 컨트롤러 및 도큐멘트 컨트롤러

AppKit의 NSDocument는 디폴트 윈도 컨트롤러를 가지고 있고 이는 문서 창을 포함한 nib 파일을 읽어들일 수 있다. windowNibName 속성으로 사용할 nib 파일을 지정해 줄 수 있다. NSDocument 객체가 생성되면, 도큐멘트 객체는 디폴트 윈도 컨트롤러에게 이 nib 파일의 이름을 전달하고, 윈도 컨트롤러는 이 파일을 읽어 자동으로 창을 생성한다. Xcode의 기본 문서 기반 앱 템플릿에서 프로그래머는 단지 문서 윈도 내에 컨텐츠를 추가하기만 하면 된다.
NSWindowController 클래스도 자동으로 nib 파일을 로딩할 수 있다. 윈도 컨트롤러 객체를 초기화할 때 따로 만든 NSWindow 객체를 지정하거나 nib 파일을 로딩하여 윈도를 생성하는 두 가지 방법 중 하나를 사용한다. 윈도 컨트롤러를 사용하는 경우, 컨트롤러는 윈도를 계속 메모리에 유지하며 설령 nib 파일 내에서 release when closed가 선택되어 있더라고 이를 무시한다.

nib 파일을 통해서 NSWindowControllerNSDocument 객체를 초기화할 때 nib 파일의 설정-File’s Owner와 window의 관계-이 올바르지 않다면 로드된 후에도 윈도를 표시하지 못한다. (당연한 이야기지만)

코드를 통해 nib 파일을 로드하기

AppKit/UIKit은 NSBundle 클래스를 통해 번들 내에 포함된 nib 파일을 로드할 수 있다. 그외에도 AppKit에는 NSNib이라는 클래스가 제공되는데, 이 클래스는 NSBundle과 유사하게 nib 파일을 로딩하는 기능을 수행하면서도 몇 가지 장점을 제공한다.

NSBundle을 통해 nib 파일 로드하기

NSBundle은 AppKit과 UIKit에 공통적으로 포함된 클래스이며 동적으로 번들내의 nib 파일을 읽어들일 수 있게 한다. iOS는 메인 번들 내의 파일만 읽어올 수 있으나 OSX에서는 외부의 번들도 읽어들일 수 있다. 단, 이 방식은 OSX 10.8에서부터는 권장되지 않는다.(대신 NSNib을 사용해서 읽어들여야 한다.)

두 플랫폼간의 약간의 차이가 있는데, iOS는 nib파일을 열어 최상위 객체로 이뤄진 배열을 리턴하는 것이고, OSX는 File’s Owner로부터 nib 파일 내부에 접근해야 한다. nib 파일을 로드하는 모든 경우에 owner를 지정하는 것은 필수적이다.
다음은 OSX에서 nib 파일을 여는 간단한 예제

- (BOOL)loadMyNibFile
{
    if (![NSBundle loadNibNamed:@"myNib" owner:self]) {
        NSLog(@"Warning! Could not load myNib file.");
        return NO;
    }
    return YES;
}

그럼 OSX 10.8에서는 어떻게 nib 파일을 열지?

다음은 iOS의 예이다. OSX와는 달리 최상위 객체들을 한꺼번에 얻게 된다.

- (BOOL)loadMyNibFile
{
    NSArray * topLevelObjs = nil;
    topLevelObjs = [[NSBundle mainBundle] loadNibNamed:@"myNib" owner:self options:nil];
    if ( topLevelObjs == nil) {
        NSLog(@"Error!");
        return NO;
    }
    return YES;
}

물론 위의 예는 nib 파일을 로딩만 할 뿐이다. 리턴되는 배열 객체는 오토릴리즈되므로, 유지하고자하는 객체는 따로 retain 하여 보관해야 한다.

최상위 객체 읽어오기

최상위 객체를 얻는 가장 쉬운 방법은 File's Owner의 아웃렛을 통해서 이다. 이는 각 객체가 안전하게 리테인되도록 유지할 수 있다. 보통 nib 파일을 따로 읽어들여야 하는 경우는 분리된 윈도우를 초기화할 때인데, 다음 코드는 별도의 컨트롤러 객체가 nib 파일을 읽어들이는 과정을 보여준다. 눈여겨볼 점은 window 객체는 초기에 1의 참조수를 (최상위객체이므로) 가지게 되는데 강제로 release를 한 번 하게 된다. 왜냐하면 이 객체는 retained outlet으로 윈도 객체를 참조하기 때문에 로드된 즉시 참조수를 다시 올리기 때문이다.

@interface MyController : NSObject
{ NSWindow *_window;}
@property (retain) IBOutlet NSWindow *window;
- (void)loadMyWindow;
@end
@implementation MyController
@synthesize window;
- (void)loadMyWindow {
    [NSBundle loadNibNamed:@"myNib" owner:self];
    [window release];
}

하지만 이 방법은 10.8부터 권장하지는 않는다. 나중에 폐기된다면 다른 방법을 찾아야 하는데, NSNib(OSX)과 UINib(iOS)가 그런 역할을 대신해준다. 이 클래스들은 -instantiateWithOwner:topLevelObjects:를 통해 최상위 객체들을 얻어오도록 한다. (이 객체들은 모두 오토릴리즈된다.) 또한 이 방법을 통해서 읽어들인 파일은 모두 메모리에 캐쉬되어 재사용시 속도 향상을 꾀할 수 있다.
다음은 예제

- (NSArray *)loadMyNibFile {
    NSNib *aNib = [[NSNib alloc] initWithNibNamed:@"MyPanel" bundle:nil];
    NSArray *topLevelObjs = nil;
    if(![aNib instantiateNibWithOwner:self topLevelObjects:&topLevelObjs]) {
        NSLog(@"Error");
        return nil;
    }
    [aNib release];
    [topLevelObjs makeObjectsPerformSelector:@selector(release)];
    return topLevelObjs;
}

각각의 최상위 객체는 기본적으로 리테인수 1을 가지고 있는데, 배열에의해 소유되면서 리테인수가 2로 증가한다. 따라서 강제로 한 번식 release를 시킨다. (사실 이보다 더 높을 수도 있다)
nib 파일이 제대로 로드되고나면 그 컨텐츠는 즉시 사용 가능한 형태가 된다.

메인 nib에서 메인 윈도 분리하기

메인 nib 파일에서 윈도를 분리하는 애플의 가이드 문서가 있었던 것 같은데, 정확하게 뭘 어찌했는지 모르겠다. 메인 윈도를 메인 nib에서 분리하는 방법은 사실 매우 여러가지가 있을 수 있다. 윈도 컨트롤러를 별도로 사용할 것인지부터 시작해서…. 여기서는 별도의 컨트롤러는 없이 윈도 UI만 nib으로부터 분리하는 방법을 사용할 것이다. 이미지 없이 글로만 간략히 설명하도록 하겠다.

  1. 기본 코코아 앱 템플릿으로 프로젝트를 시작한다.
  2. MainMenu.xib 파일을 클릭해서 메인 nib 파일을 연다. 기본적으로 하나의 메인 윈도가 들어있는데 이를 과감히 삭제한다.
  3. Command+N을 눌러 새 파일을 만든다. 파일의 분류는 User Interface를 선택하고 여기서 Empty나 window를 선택한다. 파일 이름은 적당히 “MainWindow” 정도로 준다.
  4. 새로만든 nib 파일에서 라이브러리 팔레트로부터 window 객체를 꺼내서 추가한다. 이 윈도의 옵션 중 “visible at launch”에 체크해준다. 그외에 내가 만든 창임을 확인할 수 있도록 버튼이나 기타 등등 몇 개 UI를 창에 얹어본다.
  5. nib 파일의 File’s Owner를 앱 델리게이트 클래스로 정해준다. 그런 다음 File’s Owner에서 윈도 객체로 컨트롤-드래그하여 커넥션을 만든다. 윈도 객체가 델리게이트의 window 프로퍼티에 연결된다.
  6. AppDelegate.m 파일을 열어 코드를 추가하면 된다.

추가해야 하는 코드는 1)앱이 런칭되면 바로 MainWindow 파일을 읽어서 메인 윈도 UI를 호출해주기만 하면 된다. 이게 끝이다. nib 파일에서 File’s Owner와 NSWindow 사이에 연결을 만들었는데, nib파일을 열 때 소유자 객체는 앱 델리게이트 자신이되고, 로딩시에 연결이 자동으로 만들어진다. -application:didFinishLaunchingWithOption:내부에 다음 줄을 넣는다.

[NSBundle loadNibNamed:@"MainWindow" onwer:self];

윈도 컨트롤러를 사용해 추가 창 더 만들기

방금 만든 예제를 확장하여 별도의 nib 파일로부터 창을 추가하는 예제로 변경해보도록 하자. 다음과 같은 절차가 필요하다.

-(NSMutableArray *)subWindowControllers {
    if(!_subWindowControllers) {
        _subWindowControllers = [[NSMutableArray alloc] init];
    }
    return _subWindowControllers;
}
- (IBAction)addSubWindow:(id)sender {
    XSSubWindowController *subWindowController = [[XSSubWindowController alloc] initWithWindowNibName:@"XSSubWindowController"];
    [subWindowController loadWindow];
    [self.subWindowControllers addObject:subWindowController];
    [subWindowController release];
}

뷰 컨트롤러를 통해 nib 파일로부터 뷰를 가져오기

별도의 nib 파일에 뷰를 만들어두고 이를 가져와서 그대로 사용하려면 뷰 컨트롤러를 쓰는 게 가장 쉽다. 뷰 컨트롤러는 윈도 컨트롤러와 마찬가지로 nib 파일로부터 바로 초기화할 수 있기 때문이다.

- (void)loadMyView
{
    NSViewController *myViewController = [[NSViewController alloc] initWithNibName:@"MyView" bundle:nil];
    [myViewController loadView];
    [self.window.contentView addSubView:myViewController.view];
    [myViewController release];
}

뷰를 바로 nib 파일로부터 가져오기 (최상위객체일 때)

뷰가 nib 파일의 최상위 객체 중 하나라면 다음과 같은 방법으로도 view 객체를 생성할 수 있다.

- (NSView *)loadViewFromNib:(NSString *)nibFilename
{
    NSNib *aNib = [[NSNib alloc] initWithNibNamed:nibFilename bundle:nil];
    NSArray *objs = nil;
    NSView *result;
    [aNib instantiateWithOwner:self topLevelObjects:&objs];
    if(objs) {
        for(id item in objs) {
            if([item isKindOfClass:[NSView class]]) {
                result = item;
                break;
            }
        }
    } else {
        result = nil;
    }
    [aNib release];
    return result;
}
Exit mobile version