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

이 포스팅과 관련하여 스토리보드에서 Segue를 사용하여 화면을 전환하는 사이에 두 뷰 컨트롤러 간의 데이터를 교환하는 방법에 대한 예시를 최근에 추가로 포스팅하였으니, 참고해주세요.  – Segue를 통한 뷰 컨트롤러 전환과 데이터 교환 방법

가장 단순한 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개의 뷰 전환이 아닌, 계층 구조를 가지는 뷰 트리에서 탐색을 용이하게 해주는 네비게이션 뷰 컨트롤러를 사용하는 방법이다. 네비게이션 뷰 컨트롤러는 뷰 컨트롤러의 일종이지만, 다른 뷰 컨트롤러들을 자식으로 가지고 이들은 계층 구조에 따라 표시할 수 있다. (사실 이 방법 역시 2번 방법의 확장이다. 대신 뷰 컨트롤러의 계층 구조 관리를 네비게이션 컨트롤러에게 위임하는 것이다.)

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

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

한가지 잊지말아야 할 것이 있다. 초보자들이 흔히 하는 착각 중 하나가 이와 관련된 것인데, 화면에 보여지는 것은 뷰 컨트롤러가 아니라 뷰 그자체라는 점이다. 화면에 그려지면서 사용자 터치 이벤트를 받아들이는 창구는 뷰이다. 하지만 뷰가 이벤트에 반응하게 하는 것은 뷰 컨트롤러의 역할이다. 뷰 컨트롤러는 뷰를 소유하는 주인이며, 뷰는 터치 이벤트등의 이벤트 처리를 뷰 컨트롤러에게 위임한다.

실제로 본격적인 뷰 전환의 예를 보도록 하겠다. 편의상 루트 뷰 컨트롤러와 디테일 뷰 컨트롤러의 두 개의 뷰 컨트롤러가 있다고 가정하겠다. (Xcode에서 유틸리티 타입의 템플릿으로 시작하면 이런 구성을 되어 있을 것이다.) 대략의 과정은 다음과 같다.

  1. 루트 뷰 컨트롤러에서 표시하고자 하는 디테일 뷰 컨트롤러의 인스턴스를 생성한다. 기본적으로 UIViewController의 자식 클래스들은 인스턴스가 초기화되는 시점에 뷰를 생성하거나, 생성된 뷰를 인스톨해야 한다. 흔히 이 과정은 Nib파일로부터 생성하는 것으로 한다.
  2. 디테일 뷰 컨트롤러의 modalTransitionStyle 프로퍼티 값을 적당히 설정한다.(사실 이 부분은 이제 건너뛰어도 된다. iOS는 상황에 따라 가장 적절한 형태의 전환 방식을 자동으로 결정해 줄 수 있다.)
  3. 디테일 뷰 컨트롤러의 델리게이트를 설정해준다. 디테일 뷰 컨트롤러가 제거되는 시점에 데이터를 외부로 전달하려 할 때, 이 델리게이트가 필요하다. 하지만 꼭 델리게이트를 설정하지 않더라도 디테일 뷰 컨트롤러는 presentingViewController 속성을 통해서 자신을 호출한 뷰 컨트롤러를 알 수 있다.
  4. 루트 뷰 컨트롤러는 자기 자신에게 -presentViewController:animated:completion: 메시지를 보내면서 표시할 (즉 화면상으로는 전환될) 뷰의 뷰 컨트롤러를 그 인자로 선택한다.

뷰를 전환하는 것은 그 화면상의 효과이며, 메인 뷰가 디테일 뷰를 표시(presenting)한 시점에서 사실 뷰는 포토샵 레이어처럼 쌓여있는 형국이다. 따라서 표시된 디테일 뷰에서 원래 루트 뷰로 돌아가기 위해서는 루트 뷰를 다시 위로 얹는 방식이 아니라, 루트 뷰 자신이 제거되는 방식을 취하는 편이 맞다. 즉 iOS에서 뷰 컨트롤러들은 마치 스택과 같이 쌓여가고 이전 화면으로 돌아갈 때에는 pop하는 방식으로 관리된다.

  1. 루프 뷰 컨트롤러에게 -dismissViewControllerAnimated:completion: 메시지를 보낸다. 그러면 루트 뷰 컨트롤러가 디테일 뷰 컨트롤러를 화면에서 빼낸다.
  2. 디테일 뷰가 화면에서 빠지기 직전에 디테일 뷰 컨트롤러는 viewWillDisappear 메시지를 받는다. 그리고 화면이 다시 메인 뷰로 전환되기 직전 메인 뷰 컨트롤러는 viewWillAppear:animated: 메시지를 받는다. 이 과정에서 메모리 릴리즈와 뷰 업데이트등을 처리한다.

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

- (void)presentDetailViewController
{ // 디테일 뷰 컨트롤러로 화면 전환한다.
  MYDetailViewController* dvc = [[MyDetailViewController alloc]
                  initWithNibName:@"MYDetailViewController" bundle:nil]];
  //bundle에 nil을 주는 것은 [NSBundle mainBundle]과 동일하다.
  [dvc setDelegate:self];
  [dvc setModalTransitionStyle: UIModalTransitionStyleFlipHorizontal];
  [self presentViewController:dvc animated:YES completion:nil];
}

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

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

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

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

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

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

- (void)presentDetailViewController
{ // 네비게이션 컨트롤러를 사용하는 경우
  MYDetailViewController *dvc = [[MyDetailViewController alloc] 
      initWithNibName:@"MYDetailViewController" bundle:[NSBundel mainBundle]];
  // 네비게이션 컨트롤러가 인스톨된 경우.
  if (self.navigationController) {
    [self.navigationController pushViewController:dvc animated:YES];
  } else {
    [self presentViewController:dvc animated:YES completion:nil];
  }
}

그리고 디테일 뷰 컨트롤러는 비슷하게 네비게이션 컨트롤러를 통해서 자신을 빼낼 수 있다. 역시 네비게이션 컨트롤러가 있는지 여부를 확인하고, 있는 경우에 뷰 스택에서 자신을 제거한다.

- (void)dismiss
{
  if (self.navigationController) {
    [self.navigationController popViewControllerAnimated:YES];
  } else {
    [self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
  }
}

스토리보드를 사용하기

스토리보드에서는 훨씬 더 간단하게 화면의 전환을 처리할 수 있다. 만약 스토리보드에서 연결만 잘 했다면 단 한줄의 코드도 작성할 필요가 없을 수도 있다! 기본적인 윈도의 루트 뷰 컨트롤러를 네비게이션 컨트롤러로 설정했다면 스토리보드에서는 Segue만 설정해주면 화면의 전환은 “공짜로” 이루어진다.

Segue를 통해 뷰 컨트롤러가 연결된 경우에 탭과 같은 터치 이벤트에 의해 화면이 전환되기도 하지만, 코드상에서 performSegueWithIdentifier:sender:를 사용해 강제로 segue를 작동시킬 수도 있다.

트리거링이 어떤 경로에 의해  이루어졌는지에 상관없이, 화면 전환 직전에 뷰 컨트롤러는  -prepareForSegue:sender: 메시지를 받게 되므로 화면 전환시점에 필요한 정보를 전달하기 위해 이 메소드를 오버라이드 할 수 있다.

nib파일로부터 뷰 컨트롤러를 생성하는 것처럼, 스토리보드에서도 뷰 컨트롤러를 가져와 인스턴스를 만들 필요도 있을 수 있다. 이 경우에는 필요한 뷰 컨트롤러에 대해서 스토리보드 상에서 식별자 문자열(identifier)값을 설정해주고, 이를 통해서 nib파일에서처럼 로딩할 수 있다. 참고로 스토리보드로부터 만들어진 뷰 컨트롤러들은 storyboard라는 프로퍼티를 가지고 있다.

다음 예제는 Segue로 연결되지 않은 뷰로 화면전환하는 방법이다.

 

- (void)presentSpecialViewController: (id)sender
{
  UIStoryboard *sb = [self storyboard];
  MYSpeicalViewController *svc = [storyboard instantiateViewControllerWithIdentifier:@"SpecialViewController"];
  if (self.navigationController) {
    [[self.navigationController] pushViewController:svc  animated:YES];
  }  else {
   [self presentViewController:svc animated:YES completion:nil];
  }
}

 

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

 

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


  1. 하나의 객체는 가능하면 한 가지 책임만 지도록 하는 것이 확장이나 유지보수에 도움이 되므로