Nib 파일로부터 UI 관련 객체를 로딩하기

nib 파일에서 뷰/뷰컨트롤러를 가져오기

UIView를 상속받은 커스텀 뷰를 작성할 때, 뷰의 서브 뷰들을 일일이 동적으로 구성하는 것보다 인터페이스 빌더를 통해서 구성하는 것이 더 편한 경우가 있다. 흔히 테이블 뷰의 셀에 쓰이는 뷰가 이런 식으로 구성하기 좋다.  코드 상으로 모든 뷰의 구성 요소들을 일일이 구성하고 초기화하는 것은 너무 번거로우니, 메인 스토리보드와는 별개의 nib 파일에 뷰를 세팅하고 이를 로드하는 방식으로 좀 더 간결하고 예쁘게 처리할 수 있는 방법이 있을지 고민해보자.

nib 파일의 구성

nib 파일은 인터페이스 빌더에서 구성해놓은 여러 시각적 요소 및 비 시각적 요소(뷰 컨트롤러 등)에 대해서 개별적인 세팅값이나 뷰의 위치나 색상, 폰트 등의 설정을 시각적으로 구성해놓고, 이들을 object graph로 만든 정보를 직렬화한 파일이다. 보다 정확하게 말하면 인터페이스 빌더에서 구성한 모든 정보는 XML 형태의 .xib 이라는 확장자의 파일로 저장된다. 그리고 프로젝트를 컴파일하게 되면 이 .xib 파일 역시 바이너리 파일로 컴파일되고 그 컴파일된 결과가 .nib 파일이 된다.

nib로딩 – Bundle 클래스를 이용하는 방법

앱 번들을 관리해주는 Bundle 클래스는 nib파일을 로딩해주는 loadNibNamed(_:owner:options:) 메소드를 가지고 있다. 이 메소드는 번들 내의 주어진 nib 파일을 이름으로 찾아서 이를 로드하여, 해당 nib 파일 내의 탑레벨 객체들을 [Any]? 타입으로 리턴해준다.

이때 로딩되어 배열이 들어가는 객체들의 순서는 nib 파일 내에 등록된 순서이다. 이렇게 로딩한 배열에서 원하는 타입의 객체를 얻으려면 is 연산자를 통한 타입 캐스팅을 사용해서 찾아쓰면 되겠다.

func loadPageView() -> PageView? {
  guard let objectsInNib = Bundle.main.loadNibNamed("views", owner:self, options:nil)
  else { return nil }
  for obj in objectsInNib.lazy {
    if obj is PageView {
      return obj
    }
  }
  return nil
}

혹은 다음과 같이 lazy를 이용해서 간단히 처리할 수도 있겠다.

func loadPageView() -> PageView? {
  guard let objectsInNib = Bundle.main.loadNibNamed("views", owner:self, options:nil)
  else { return nil }
  return objectsInNib.lazy.filter{ $0 is PageView }.first
}

UINib 클래스를 이용하는 방법

UINib 클래스는 인터페이스 빌더가 생성한 nib 파일의 콘텐츠를 래핑하는 클래스로, 번들 내의 nib 파일을 통해서 인스턴스를 생성하면 nib 파일의 내용을 캐시해둔다. instantiate(withOwner:options:)를 이용하면 [Any] 타입의 배열을 얻을 수 있는데, 여기에는 nib 파일에서 생성한 순서대로의 최상위 객체들이 포함된다.

코드 자체는 UINib 인스턴스를 만들어서 탑 레벨 객체를 생성, 그 중에 필요한 클래스의 객체를 얻으면 되는 것이라, 위의 예제와 크게 다를 바 없다.

func getPageView() -> PageView? {
  let viewNib = UINib(named: "views", bundle:nil)
  let objectsInNib = viewNib.instantiate(withOwner: self, options:nil)
  return objectsInNib.lazy.filter{ $0 is PageView }.first
}

참고로 UINib 클래스를 이용하는 방법은 하나의 nib 파일의 콘텐츠에 대해서 복수의 사본을 여럿 만들어야 할 때, 더 나은 성능을 발휘한다. 일반적으로 nib 파일을 로딩하는 프로세스는 파일 자체를 읽어오는 것을 시작으로 하지만, UINib 인스턴스는 이미 읽어온 파일의 내용을 캐시하고 있기 때문에 디스크로부터 파일을 읽어들이는 오버헤드를 줄일 수 있다.

참고할 내용들

소유주

https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/LoadingResources/CocoaNibs/CocoaNibs.html#//apple_ref/doc/uid/10000051i-CH4-SW15

모든 nib 파일에는 File's owner라는 객체가 정의되어 있는데, 이는 일종의 프록시 객체1로 nib 파일이 로드될 때 생성되는 것이 아니라 nib-loading 프로세스를 시작할 때 인자로 받게 되는 객체이다. 이 객체는 nib 파일의 내용과 프로그램의 코드 사이를 이어주는 역할을 하기 때문에 중요한데, 예를 들어 콘텐츠를 nib 파일 내에 정의했다면, 파일의 소유주는 보통 뷰 컨트롤러 객체가 된다. (그래서 위의 예제들에서도 self는 뷰 컨트롤러임을 상정한다.)

Xcode 상에서 파일 소유주 객체는 일단 객체가 있는 것으로 간주하고, 이 객체와 nib 파일 내의 뷰 및 다른 객체들과 커넥션을 만들 수 있다. 이러한 커넥션은 nib 파일의 내용을 로딩하면서 생성되는 객체 그래프 내에서 자동으로 연결된다.

initWithCoder:

스토리보드내에 정의된 뷰 컨트롤러들은 보통 코드상에서 직접 인스턴스화하지 않는다. 대신에 해당 씬이 필요한 경우에 스토리보드에 의해서 자동으로, 혹은 instantiateViewController(withIdentifier:)를 스토리보드에게 보내서 뷰 컨트롤러의 인스턴스를 생성하게 된다.

스토리보드는 이 과정에서 뷰 컨트롤러를 init(coder:)를 통해서 초기화한다.2

initWithNibname:bundle:

UIViewController들은 init(nibName:bundle:)이라는 초기화 메소드를 지원한다. 실질적으로 이 과정은 뷰 컨트롤러 (혹은 AppKit에서의 윈도 컨트롤러)인스턴스를 초기화하고, 여기에 nibName 속성을 부여한다. 그리고 nib 파일은 실질적으로는 게으르게 평가되면서 뷰를 실제로 액세스하기 위해서 로드하는 시점에 읽어들이게 된다.

nib 파일을 읽어들이는 과정은 파일로부터 직렬화된 데이터를 읽고 이를 객체 그래프로 복원하는 과정이다. 이 과정에서 얼려져있던 객체들이 온전한 인스턴스로 복원되며, 이를 위해서 모든 객체들은 init(coder:) 메시지를 받게 된다.

awakeFromNib

awakeFromNib() 메시지는 nib 파일의 내용을 읽어서 객체 그래프를 복원할 때, 모든 복원이 완료된 후에 nib 파일 내의 모든 객체가 받게되는 메시지이다. 이 메시지가 호출되는 것은 각 객체가 인스턴스화는 물론3 nib 파일 내에 정의되었던 모든 속성값과 아웃렛등의 커넥션이 복원 완료되었다는 것을 의미한다.

이 메소드를 오버라이드하여 생성 및 초기화가 완료된 객체들이 수행해야 할 작업들 (아마도 인터페이스 빌더 상에서는 할 수 없었던 설정이나, 상황에 따라서 설정을 변경하는 것 등)을 처리할 수 있다. (그리고 반드시 이 과정에서는 super.awakeFromNib()을 호출해야 한다.)

참고로 이 메시지는 AppKit에서만 파일 소유주에게 전달되며, UIKit에서는 파일 소유주에게 전달되지 않는다.

정리

  1. init(nibName:bundle:) 은 nib 파일로부터 뷰 컨트롤러를 생성할 때 사용하며, 이 때 nib 파일은 아직 로딩되지 않는다.
  2. init(coder:)는 nib 파일을 읽어들여서 그 데이터로부터 뷰 컨트롤러 (및 내부 뷰 들)를 생성할 때 사용되는 이니셜라이저이다.
  3. awakeFromNib()은 모든 nib 로딩 과정이 끝난 후에 nib 파일로부터 깨어난 모든 객체들이 받게 된다.

참고: https://www.quora.com/Cocoa-API-What-is-the-difference-between-initWithCoder-initWithNibName-and-awakeFromNib-1


  1. `NSProxy`가 아니다. 일종의 placeholder라 생각하면된다. 
  2. 스토리보드는 1개 이상의 nib 파일을 담고 있는 번들이며, 스토리보드는 이렇게 초기화한 뷰 컨트롤러의 nibName 프로퍼티를 실제 nib 파일 이름으로 세팅한다. 
  3. nib 파일을 로딩한 후에 내부 객체들을 복원하는 과정에서 앞서 말한 init(coder:)가 사용된다.