iOS 앱의 구조

아이폰 앱 강좌를 검색하다보면 IB없이 만들기와 관련된 강좌가 상당히 많다. 즉 인터페이스 빌더 없이 모든 UI를 코드로 제어하는 형태로 앱을 개발하는 방법이다. 초보자에게는 추천하기 어렵고 또한 이렇게 코드로 UI를 구성하면 준비해야 하는 코드가 상당히 많아지는데, nib 파일로부터 객체가 생성되는 부분 특히 이렇게 어디선가 갑자기 툭 튀어나오는 객체들을 어떻게 처리해야 할지에 대해서는 좀 난감한 부분들이 있다보니 nib 파일을 사용하지 않는 방식으로 개발을 많이 하고 있는 것 같다.

그래서 오늘은 iOS앱의 구조에 대해 잠시 살펴보도록 하겠다. 분명 IB, 즉 nib 파일을 사용하는 방식은 빠른 시간안에 UI 레이아웃을 구성하고 시각적으로 조정하므로 별도의 컴파일없이 시각적으로 결과물을 확인할 수 있다는 점에서 장점을 가진다. nib 파일에 대한 이해가 어느 정도 충족된다면 인터페이스 빌더를 사용하여 앱을 개발하는 것이 그다지 나쁜 것만은 아님을 알 수 있을 것이다.

main

iOS앱은 Objective-C를 통해서 제작된다. Objective-C도 C언어의 확장이고 본질적으로는 C와 같다고도 볼 수 있다. 따라서 프로그램이 시작되면 main 함수가 호출된다. Xcode에서 ‘Empty Appliction’ 템플릿을 사용하여 프로젝트를 생성하면 Support Files 폴더 아래에 main.m 함수가 위치하는데, 대략 다음과 비슷한 코드가 들어있을 것이다.

#import <UIKit/UIKit.h>
#import "MyAppDelegate.h"

int main(int argc, const char *argv[]) 
{
    @autoreleasepool{
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([MyAppDelegate class]));    
    }
}

UIApplicationMain() 함수는 UIKit 프레임워크에서 제공하는 기본함수로, 앱의 시작점이 된다. 이 함수는 UIApplication객체(앱의 어플리케이션 객체)를 생성하고 앱의 구동에 필요한 사전준비를 한 다음 메인 런루프를 시작해 사용자로부터(혹은 제3의 소스로부터) 상호작용을 위한 입력을 받을 준비를 한다.

이 함수는 위에서 보듯 main 함수의 인자인 argc, arvg를 받으며 추가적으로 2개의 요소를 더 받는데 하나는 애플리케이션 객체의 클래스 이름이고 다른 하나는 앱 델리게이트 객체가 될 클래스의 이름이다.

즉 아이폰 앱이 구동을 시작하면 앱 객체가 생성되고, 앱 객체의 라이프 사이클의 특정 지점에서 필요한 동작을 대리하여 수행할 앱 델리게이트는 iOS앱에서 필수적으로 갖춰야 할 두 가지 객체가 된다.

앱메인 함수(UIApplicationMain())함수는 애플리케이션 객체를 생성하고 네 번째 인자로 받은 클래스의 이름을 통해 앱 델리게이트 객체를 만들어서 두 객체를 연결한다. 앱 델리게이트 객체가 애플리케이션 객체의 델리게이트가 되도록 연결을 구성해준다.

그리고 이 함수가 실행하는 또 하나의 작업이 있는데 바로 앱의 메인 nib 파일을 로딩하는 것이다. nib 파일을 로딩하는지, 그리고 어떤 객체가 그 속에 들어있는지에 따라서 main 함수나 앱 델리게이트 객체의 -application:didFinishLaunchingWithOptions: 메소드가 약간 달라질 수 있다.

UIWindow

아이폰 앱이 실행되면 아이폰의 화면은 앱이 가득 채우는 형태로 전환된다. 아이폰 앱의 화면은 기본적으로 UIWindow에 의해 구성된다. 즉 아이폰은 한 번에 하나의 창만 화면에 표시할 수 있고 창을 닫는 버튼 같은 게 없는 등 OSX의 창과는 그 모양이나 양상이 매우 다르지만, 코코아터치도 코코아에서 파생되었으므로 창을 기본으로 그 위에 뷰를 올리게 된다.

윈도 객체가 없으면 앱은 그저 까만 화면만 표시되게 된다. 그런데 실제로 Empty Application 템플릿으로 프로젝트를 만들고 바로 빌드해도 흰 바탕의 화면이 나온다. 즉 윈도가 생성되었다는 뜻이다. 그러면 Empty Application 템플릿의 앱 델리게이트 클래스의 소스에서 확인해보자.

- (BOOL)application:(UIApplication*)application didFinishedLaunchingWithOptions:(NSDictionary*)launchOptions 
{
    self.window = [[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] autorelease];
    self.window.backgroundColor = [UIColor whiteColor];
    [self.window makeKeyAndVisible];
    return YES;
}

앱메인 함수에 의해 생성된 앱 델리게이트는 앱의 구동 절차가 마무리 되면 위에서 정의한 메시지를 받게 된다. 그리하여 UIWindow 객체를 생성하여 화면에 표시하고, 애플리케이션 객체가 이벤트 루프를 시작할 수 있도록 응답을 보내게 된다.

루트 뷰 컨트롤러

뷰는 화면에 UI를 표시하는 역할을 할 뿐, 실제로 뷰상에서 어떤 사용자 액션이 일어나는 경우 이는 주로 뷰 컨트롤러에 의해 처리되어야 한다.

만약 빈 템플릿으로부터 만든 프로젝트를 바로 빌드하고 실행하면 하얀 윈도는 표시가 되지만 윈도 객체에 ‘루트 뷰 컨트롤러’가 없다는 경고가 뜬다.

(물론 뷰 컨트롤러 없이, 앱 델리게이트가 일일이 다 처리해줘도 좋지만 뷰와 관련한 메모리 처리나 뷰의 라이프 사이클 관리까지는 힘들지도 모르겠다.) 따라서 비어있는 템플릿으로 앱을 작성하는 경우 아마도 UIViewController의 서브클래스를 만들고, 이를 통해 윈도의 루트 뷰 컨트롤러를 지정하고, 초기뷰를 창에 표시하는 코드가 이어서 들어가야 한다.

- (BOOL)application:(UIApplication*)application didFinishedLaunchingWithOptions:(NSDictionary*)launchOptions 
{
    self.window = [[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] autorelease];
    self.window.backgroundColor = [UIColor whiteColor];
    [self.window makeKeyAndVisible];

    self.window.rootViewController = [[[MYViewController alloc] init] autorelease];

    return YES;
}

네비게이션 컨트롤러

계층 구조의 네비게이션을 사용할 때는 네비게이션 컨트롤러를 사용한다. (UINavigationController) 네비게이션 컨트롤러도 UIViewController의 서브클래스이므로, 네비게이션 컨트롤러를 사용한다면 이를 바로 루프 뷰 컨트롤러로 설치할 수 있다.

- (BOOL)application:(UIApplication*)application didFinishedLaunchingWithOptions:(NSDictionary*)launchOptions 
{
    self.window = [[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] autorelease];
    self.window.backgroundColor = [UIColor whiteColor];
    myViewController = [[[myViewController alloc] init] autorelease];
    self.window.rootViewController = [[[UINavigationController alloc] initWithRootViewController:myViewController] autorelease];
    [self.window makeKeyAndVisible];
    return YES;
}

nib 파일이 있는 경우

단일 뷰 애플리케이션 템플릿으로 프로젝트를 시작하는 경우, 의외로 여러 부분에서 빈 템플릿과 차이가 난다. 이는 기본으로 제공되는 nib 파일에 필요한 객체들이 모두 정의되어 있기 때문이다.

  • UIApplication : UIApplicationMain()함수에 의해 자동으로 생성된다.
  • 그외의 객체들 : nib 파일로 옮겨 놓을 수 있다.

nib 파일에 다른 객체를 옮겨놓으려면 (그만큼 코드는 적게 작성해도 됨) 프레임워크가 제공하는 nib 파일 자동 로딩 기능을 이용하면 된다. 이는 프로젝트 내의 Info.plist 파일 내에 있는 NSMainNibFile에 파일 이름을 기록하면 된다. (Xcode의 타겟 설정 화면에도 nib 파일 이름을 넣는 부분이 있음)

Empty Application 템플릿으로부터 nib 파일을 사용하는 형태로 변경하기

아마도 많은 개발자들이 Empty Application으로부터 프로젝트를 시작하는 것을 선호하는 가장 큰 이유는 템플릿 상에 미리 기재된 코드가 적기 때문이 아닌가 싶은데, 이로부터 시작해서 nib 파일을 사용하는 형태로 변경하는 방법을 소개한다.

  1. Empty Application 템플릿으로 프로젝트를 생성한다.
  2. main.m 파일에서 main 함수를 다음과 같이 수정한다.

     int main(int argc, const char *argv[]) 
     {
         @autoreleasepool{
             return UIApplicationMain(argc, argv, nil, nil);
         }
     }
    
  3. AppDelegate.h 파일에서 UIWindow와 UIViewContoller(자신이 사용하고자 하는 루트 뷰 컨트롤러의 클래스)객체에 대한 아웃렛을 선언한다.

     @property (strong, nonatomic) UIWindow * window;
     @property (strong, nonatomic) UIViewController * rootViewController;
    
  4. AppDelegate.m 의 application:didFinishLaunchingWithOptions: 메소드는 return YES;만 남기고 모든 코드를 제거한다. dealloc에서는 _window_rootViewController를 각각 해제해주도록 한다. (ARC라면 통과~)

  5. 새 nib 파일을 하나 만들고, Target 정보에서 Main NibFile 란에 그 이름을 입력해 준다.
  6. nib 파일을 열어 다음을 추가한다.

    • NSObject 객체를 끌어온 후 클래스를 앱 델리게이트의 클래스로 지정한다.
    • File’s Owner의 클래스는 UIApplication으로 변경한다.
    • UIWindow 객체를 끌어와서 추가한다.
    • UIViewController 객체를 끌어와서 추가한 다음, 자신이 사용할 클래스 명으로 클래스를 변경한다.
    • File’s Owner –> AppDelegate로 연결을 만들고 delegate라고 지정한다.
    • AppDelegate –> UIWindow로 연결을 만들고 ‘window`라고 지정한다.
    • AppDelegate –> UIViewController로 연결을 만들고 rootViewController라고 지정한다.
    • UIWindow –> UIViewController로 연결을 만들고 rootViewController라고 지정한다.

이상의 IB에서의 동작은 윈도 객체, 루트 뷰 컨트롤러를 생성하고 각각의 객체간의 연결을 만들어주는 코드를 IB상에서 수행한 것과 동일하다. UIApplicationMain함수는 Info.plist로부터 nib 파일을 인식하고 읽어들인 다음, 모든 객체(File’s Owner와 First Responder는 예외)를 생성하고, 각 객체간의 연결을 만든다.

보다 권장하는 방법

앱의 초기 구동속도를 빠르게 하는 방법 중 하나는 nib 파일로부터 읽어들이는 양을 최소화하는데 있다. 그리고 맨 처음에 사용할 게 아니라면 별도의 nib 파일로 UI요소를 분리하는 게 좋다. 그래서 보통은 다음과 같은 요소를 고려해도 좋다.

  1. 초기 로딩시 nib 파일을 열지 않도록 한다. window까지는 앱 델리게이트에서 생성하도록 한다.
  2. 뷰 컨트롤러는 해당 뷰의 내용을 미리 IB에서 작성해두는 것이 여러모로 편리하다. 뷰 컨트롤러 그 자체는 nib 파일로부터 초기화할 수 있으니 .h + .m + .nib 파일의 조합으로 뷰 컨트롤러 객체를 만든다. (nib 파일의 File’s Owner가 뷰 컨트롤러이면 되고, nib 파일 내에는 루트 뷰로 쓰일 뷰만 있으면 된다.)
  3. 이렇게 nib 파일을 통해 레이아웃을 구성해놓은 뷰 컨트롤러는 다음과 같은 방법으로 초기화하면 된다. (Xcode 4.5 버전에서의 템플릿은 이 방식으로 구성되는 듯 하다.)

     -(BOOL)application:(UIApplication*)application didFinishedLaunchingWithOptions:(NSDictionary*)launchOptions 
     {
         self.window = [[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] autorelease];
         self.window.backgroundColor = [UIColor whiteColor];
         myViewController = [[[myViewController alloc] initWithNibName:@"RootViewController" bundle:nil] autorelease];
         self.window.rootViewController = [[[UINavigationController alloc]     initWithRootViewController:myViewController] autorelease];
         [self.window makeKeyAndVisible];
         return YES;
     }
    

nib 파일이 로딩되면 nib 파일 내의 최상위 객체들은 모두 retain 수를 1 혹은 그 이상 가지게 된다. 이 nib 파일을 소유하는 객체는 이들 최상위 객체들에 대해 release할 책임을 진다. 특히 메인 nib 파일의 경우에는 애플리케이션이 소유하는데, 우리는 애플리케이션 객체를 서브클래싱할 일이 없으므로 이 때만 예외적으로 앱 델리게이트에서 해제해야 한다.

  • Noh

    정말 유용한 정보내요. 많은 도움이 됬습니다~