[iOS앱 만들기 006] 뷰 전환하기

가장 단순한 iOS의 앱을 하나 구성한다고 하면 이 앱은 앱 델리게이트과 기본 뷰(root view)의 뷰 컨트롤러, 즉 루트 뷰 컨트롤러로 구성이 될 수 있다. 물론 클래스를 디자인하기에 따라서는 앱 델리게이트와 뷰 컨트롤러가 하나의 객체일 수도 있다. (앱 델리게이트가 UIViewController의 서브클래스이면서 애플리케이션 프로토콜을 따르면서 이 두가지 일을 모두 수행하도록 만들면 되니까.)

하지만 많은 경우 iOS 앱은 하나 이상의 화면으로 구성된다. 간단한 메모 작성 앱을 떠올려보자. 앱을 실행하면 이미 작성하여 저장된 메모의 목록이 표시되는 뷰가 나올 것이고, 각각의 메모를 터치하면 해당 메모의 전체 내용을 표시하는 뷰로 전환된다. 이 처럼 화면을 전환하는 방법에는 몇 가지가 있을 수 있다.

  1. 뷰 컨트롤러 내에 View를 2개 준비하고, 뷰를 바꿔치기하거나,
  2. 혹은 A뷰 위에 B뷰를 얹어서 A를 가리기
  3. 뷰 컨트롤러에서 다른 뷰 컨트롤러의 뷰를 가져와 표시하기
  4. 네비게이션 컨트롤러 사용하기

사실, 현재 표시되는 뷰위에 다른 뷰를 ‘얹어서’ 기존 뷰를 가리도록 한다는 면에서 이 모든 방법들은 크게 다를 게 없다. 하지만 1번의 경우에는 뷰 컨트롤러 1개가 2개 이상의 뷰를 모두 제어하는 책임을 맡게 되는데, 이는 그리 좋은 디자인이 아닐 가능성이 크다.1 다른 방법으로는 제 2의 뷰+뷰 컨트롤러 조합을 가지고 있다가, 원래 뷰에서 다른 뷰를 modal하게 불러와서 표시하는 것이다. 이 방법은 UIViewController의 -presentViewController:animated:completion:이라는 메소드로 지원을 하고 있다.

마지막 방법은 단순히 2개의 뷰 전환이 아닌, 계층 구조를 가지는 뷰 트리에서 탐색을 용이하게 해주는 네비게이션 뷰 컨트롤러를 사용하는 방법이다. 네비게이션 뷰 컨트롤러는 뷰 컨트롤러의 일종이지만, 다른 뷰 컨트롤러들을 자식으로 가지고 이들은 계층 구조에 따라 표시할 수 있다.

여기서는 뷰 컨트롤러 1개가 뷰 1개를 관리한다는 원칙하에서 모달하게 뷰를 표시하는 방법 및 네비게이션 컨트롤러를 사용하는 방법을 알아보겠다.

뷰 컨트롤러에서 다른 뷰 컨트롤러의 뷰를 표시하기

한가지 잊지말아야 할 것은 화면에 보여지는 것은 뷰 컨트롤러가 아니라 뷰 그자체라는 점이다. 다만 코드에서는 뷰 컨트롤러가 뷰 컨트롤러를 호출하는 방식으로 구현해준다. 이러한 차이점은 염두에 두고 진행하자. 뷰 컨트롤러에서 다른 뷰 컨트롤러로 전환하는 방식은 다음과 같다.

편의상 루트 뷰 컨트롤러와 디테일 뷰 컨트롤러의 두 개의 뷰 컨트롤러가 있다고 가정하겠다. (Xcode에서 유틸리티 타입의 템플릿으로 시작하면 이런 구성을 되어 있을 것이다.)

  1. 루트 뷰 컨트롤러에서 표시하고자 하는 디테일 뷰 컨트롤러의 인스턴스를 생성한다.
  2. 디테일 뷰 컨트롤러의 modalTransitionStyle 프로퍼티 값을 적당히 설정한다.
  3. 디테일 뷰 컨트롤러의 델리게이트를 설정해준다. 보통은 루트 뷰 컨트롤러(해당 뷰 컨트롤러를 표시하려는 뷰 컨트롤러)가 된다. 디테일 뷰 컨트롤러가 화면에서 제거되기 직전에 델리게이트는 이를 알아차리게 된다. 하지만 꼭 델리게이트를 설정하지 않더라도 디테일 뷰 컨트롤러는 presentingViewController 속성을 통해서 자신을 호출한 뷰 컨트롤러를 알 수 있다.
  4. 루트 뷰 컨트롤러는 자기 자신에게 -presentViewController:animated:completion: 메시지를 보내면서 인자값으로 디테일 뷰 컨트롤러를 전달한다.

이 때, 포인트는 디테일 뷰의 컨트롤러에 대한 참조를 루트 뷰 컨트롤러가 소유한다는 점이다. 따라서 디테일 뷰가 더 이상 필요없어지면 해당 리소스를 해제하는 것도 루브 뷰 컨트롤러야 해야 하는 일이다.

표시한 뷰를 제거하는 방법은 다음과 같다. (여기서부터는 디테일 뷰 컨트롤러에서 처리하는 부분이다.)

  1. 루프 뷰 컨트롤러에게 -dismissViewControllerAnimated:completion: 메시지를 보낸다. 그러면 루트 뷰 컨트롤러가 디테일 뷰 컨트롤러를 화면에서 빼낸다.
  2. 디테일 뷰 컨트롤러가 더 이상 필요없거나, 메모리 자원을 확보하기 위해 해당 객체를 해제한다.

다음은 코드로 살펴보는 예제이다. 먼저 디테일 뷰 컨트롤러를 표시하는 루트 뷰 컨트롤러의 코드

-(void)presentDetailViewController
{
    MYDetailViewController *dvc = [[MYDetailViewController alloc] initWithNibName:@"MYDetailViewController" bundle:[NSBundle mainBundle]];
    [dvc setDelegate:self];
    [dvc setModalTransitionStyle:UIModalTransitionStyleFlipHorizontal];
    [self presentViewController:dvc animated:YES completion:nil];
}

디테일 뷰 컨트롤러를 nib 파일로부터 읽어들여 인스턴스를 생성하고, 해당 뷰 컨트롤러의 뷰를 presenting한다.

뷰 전환 이후에는 화면을 디테일 뷰 컨트롤러가 점령(?)하고 있다. 이 화면을 해제하고 원래 루트 뷰로 돌아가려면 다음과 같은 메소드가 디테일뷰에 있으면 된다.

-(void)dismiss
{
    [self.presentingViewController dissmissViewControllerAnimated:YES completion:nil];
}

네비게이션 컨트롤러에서 새로운 뷰 추가하기

네비게이션 컨트롤러가 설치되어 있다고 가정하면, 뷰 컨트롤러는 자신이 속한 네비게이션 컨트롤러에게 새로운 뷰 컨트롤러를 삽입해 달라고 요청할 수 있다. 네비게이션 컨트롤러는 뷰 컨트롤러를 스택으로 관리하고 있다. 따라서 새로운 뷰 컨트롤러를 push 하면 새 뷰가 화면으로 들어오게 되고, pop 하면 맨 위의 뷰 컨트롤러부터 하나씩 차례로 사라져 루트 레벨까지 돌아갈 수 있다.

네비게이션 컨트롤러 내에 루트 뷰 컨트롤러가 설치돼 있다면 다음의 코드로 다음 레벨의 뷰 컨트롤러를 올릴 수 있다.

-(void)presentDetailViewController
{
    MYDetailViewController *dvc = [[MYDetailViewController alloc] initWithNibName:@"MYDetailViewController" bundle:[NSBundle mainBundle]];
    [self.navigationController pushViewController:dvc animated:YES];
}

그리고 디테일 뷰 컨트롤러는 비슷하게 네비게이션 컨트롤러를 통해서 자신을 빼낼 수 있다.

-(void)dismiss
{
    [self.navigationController popViewControllerAnimated:YES];
}

스토리보드를 사용하기

스토리보드에서는 훨씬 더 간단하게 이를 처리할 수 있다. 만약 스토리보드에서 연결만 잘 했다면 단 한줄의 코드도 작성할 필요가 없을 수도 있다! 네비게이션 컨트롤러를 쓰거나 쓰지 않거나 상관없이 특정 버튼이 다음 씬의 뷰 컨트롤러와 연결되면, 화면 전환을 위한 segue가 만들어진다. 그럼 별도의 추가적인 코드를 작성하지 않고도 해당 버튼이나 뷰를 탭했을 때 segue가 동작하면서 화면을 전환시켜준다. segue는 스토리보드 상의 정보로 목적지가 되는 뷰 컨트롤러의 클래스와 nib 파일의 내용을 모두 알고 있으므로, 자동으로 뷰 컨트롤러의 인스턴스를 만들어서 위에서 언급한 두 번째 혹은 세 번째 방법의 과정을 자동으로 모두 처리한 다음, 화면을 전환해 준다.

뷰 컨트롤러 객체는 코드상에서 performSegueWithIdentifier:sender:를 사용해 강제로 segue를 작동시킬 수도 있다.

이때에도 화면 전환 이전에 특별한 준비 작업 (새롭게 나타날 뷰에서 화면에 표시할 정보를 제공하는 등)이 필요하다면 -prepareForSegue:sender:를 구현해서 segue의 identifier로 구분하여 사전 작업을 해 줄 수 있다.

경우에 따라서는 스토리보드 내에서 segue로 연결되지 않은 뷰 컨트롤러를 인스턴스화해서 사용해야 할 경우가 있는데, 이 때는 스토리보드에서 해당 뷰 컨트롤러에 ID를 설정한 다음, 이를 통해 해당 객체를 가져올 수 있다. 스토리보드내에 포함된 뷰 컨트롤러들은 .storyboard 프로퍼티를 가지는데, 이 참조를 이용해서 스토리보드로 하여금 특정 ID를 가지고 있는 뷰 컨트롤러 객체를 코드상으로 생성하는 방법은 다음과 같다.

-(void)presentingSpecialViewController:(id)sender
{
    UIStoryboard *storyboard = self.storyboard;
    SpecialViewController *svc = [storyboard instantiateViewControllerWithIdentifier:@"SpecialViewController"];
    [self presentViewController:svc animated:YES completion:nil];
}

instantiateViewControllerWithIdentifier: 메소드를 이용해서 지정한 ID를 가진 뷰 컨트롤러의 인스턴스를 얻을 수 있다.2

한 편 별도의 스토리보드 파일을 따로 만들어 두고, 해당 스토리보드에서 뷰 컨트롤러를 꺼내오는 방법이 필요할 때도 있다. 스토리보드 파일의 크기를 작게 만들면, 한 번에 메모리로 읽어들이는 데이터의 양을 줄일 수 있다. (스토리보드 내의 객체 그래프가 lazy하게 로딩된다는 언급을 애플 개발자 문서에서 찾을 수가 없는데, 혹시 알고 계시는 분은 댓글로 추가 바랍니다.).

이 코드는 예외적으로 새로운 윈도우를 만드는데, iOS 장치에 외부 디스플레이를 연결했을 경우에, 해당 디스플레이에 표시할 별도의 창을 만드는 내용이다. 별도의 화면 규격에 대한 내용이므로 별도의 스토리보드 파일에 UI 설계도가 들어있을 것을 가정한다.

-(UIWindow*)windowFromStoryBoard:(NSString*)storyboardName onScreen:(UIScreen*)screen
{
    UIWindow *window = [[UIWindow alloc] initWithFrame:[screen bounds]];
    window.screen = screen;

    UIStoryboard* storyboard = [UIStoryboard storyboadWithName:storyboardName bundle:nil];
    MainViewController *mainViewController = [storyboard instantiateInitialViewController];
    window.rootViewController = mainViewController;
    return window;
}

리턴된 window 객체에 대해 makeKeyAndVisible을 호출해주면 화면에 해당 윈도우가 표시될 것이다.

이상으로, 뷰 컨트롤러에 대한 기본적인 내용을 살펴보았으니, 다음 시간에는 앱을 구성하는 필수적인 객체들은 무엇인지, 그래서 어떤 방식으로 앱을 구성할 수 있는지에 대한 내용을 살펴보도록 하겠다.


  1. 하나의 객체는 가능하면 한 가지 책임만 지도록 하는 것이 확장이나 유지보수에 도움이 되므로 
  2. 이 뷰 컨트롤러는 스토리보드로부터 로딩되므로 UI 구성은 스토리보드에 설정한 그것과 동일하며 initWithCoder: 메소드를 통해서 초기화된다. 
  • 김윤래

    안녕하세요.

    위키북스 출판사입니다.
    http://www.wikibook.co.kr

    메일을 드리려고 하는데 메일 주소를 찾을 수가 없네요..ㅠㅠ
    yoon(엣)wikibook.co.kr 로 메일 주소 부탁드립니다…^^;;

    고맙습니다.

  • 최지우

    궁금했던것들인데 오늘 실습하면서 덕분에 완벽히 이해했습니다. 감사합니다!