Segue를 통한 뷰 컨트롤러 전환과 데이터 교환 방법

꽤 오래전에 iOS에서 뷰를 전환하는 방법에 대해 글을 포스팅한 적이 있는데, 최근에도 비슷한 질문을 종종 받는다. 단순히 뷰를 표시하는 것보다는 어떻게 뷰 간에 데이터를 주고 받느냐는 것이다. 오늘은 이와 관련하여 스토리보드를 사용할 때의 방법 위주로 조금 자세히 살펴보도록 하겠다.

스토리보드 Segue에 의한 뷰 전환

스토리보드가 여전히 불편하다는 사람도 많이 있지만, 사실 스토리보드의 도입은 미리 정해진 콘티에 따른 콘텐츠 표시를 위한 목적의 앱을 코드 한 줄 없이 만들 수 있게 해준다는 점에서 높이 평가받을만 하다. 다만 문제는 그런식으로 만드는 앱은 큰 기능성을 가지지 못하기 때문에 많이 쓰이지는 않는다….

스토리보드의 구성 방식

스토리보드에서 뷰 전환이 어떻게 일어나는지에 대해 살펴보기위해서 먼저 스토리보드가 어떻게 만들어지는지부터 간략하게 살펴보자. 스토리보드 이전에도 Xcode에서는 인터페이스 빌더를 UI를 구성하는데 사용해왔다. 인터페이스 빌더를 사용하면 기본적으로 개별 뷰 단위로 화면을 정의하고, 화면 내 들어갈 UI 요소와 각 UI요소의 위치와 크기, 기본 속성값들을 정의할 수 있다. 뷰들은 부모자식의 상하 관계를 맺고 계층화되며, 각각의 고유한 속성값들을 가지고 있기 때문에 이러한 정보는 프로퍼티 리스트 형태로 기술되고, 파일에 저장될 때에는 XML 포맷을 사용해서 고정된다. XML 포맷으로 저장되는 Interface Builder 파일이라는 의미로 인터페이스 빌더의 데이터 저장파일은 .xib 라는 확장자를 사용한다.

Xcode는 프로젝트를 컴파일 할 때, 프로젝트 내의 각 소스코드를 컴파일 하는 과정에 더해 이러한 xib 파일들도 컴파일된다. 인터페이스 빌더 파일들은 앱 시작 시에 로딩되기 때문에 로딩시간을 단축시키기 위한 것으로 생각된다. 이렇게 컴파일된 인터페이스 빌더 파일은 .nib 확장자를 갖는다.1

스토리보드는 실제로 여러 개의 뷰에 대한 Nib 파일을 모아둔 번들이다. 번들은 macOS에서 특별한 속성을 갖는 디렉토리인데, 그냥 폴더라 생각해도 무방하겠다.  따라서 스토리보드 안에 있는 각각의 뷰들은 개별 nib 파일이며, 따라서 뷰와 뷰를 연결하는 방법은 존재하지만, 하나의 동일한 어떤 속성을 여러 뷰가 공유하는 것은 스토리보드 내에서 설정할 수 없다. 대신에 스토리보드는 개별 뷰들의 nib 파일외에 각 뷰들이 어떻게 연결될 것인가에 대한 정보를 별도로 추가한다. 각 뷰들이 어떤 관계를 갖고 연결되는가 하는 것을 나타내는 개념이 바로 Segue이다. (여기서 중요한 것은 두 화면 사이의 관계이다. Segue는 단순히 전환의 흐름이 아니다.)

Segue

Segue는 두 뷰사이의 관계를 말한다고 했다. 하지만 실질적으로 뷰는 해당 뷰를 제어하는 뷰 컨트롤러말고는 어떤 것과도 관계하지 않는다. (그렇게 해야 정상이다.) MVC는 코코아터치의 매우 중요한 기조 중 하나이고, 이걸 애플이 나서서 지키지 않을 이유는 없을 것이다. 따라서 두 화면 사이의 관계를 정의한다는 것은 Segue가 두 뷰 컨트롤러 인스턴스 사이의 관계를 규정한다는 것이다. 따라서 Segue는 다음과 같은 두 가지 속성만을 갖게 된다.

  • source : Segue의 관계는 방향성을 가진다. source는 전환시 시작점에 해당하는 뷰 컨트롤러를 가리킨다.
  • destination : 전환하고자하는 도착점에 대항하는 뷰 컨트롤러를 가리킨다.

그렇다면 도대체 어떻게 Segue는 앱의 화면 전환을 가능케하는 걸까?

Segue에 의한 화면 전환

간단한 스토리보드의 예를 살펴보자. 2개의 씬을 갖는 앱이 있고, 메인 뷰와 서브 뷰가 각각 존재한다. 스토리보드의 구성은 다음과 같을 것이다.

initial -> [main view(MainViewController)] -segue-> [subview(SubViewController)]

먼저 앱이 시작되면 스토리보드 내에서 최초 씬에 해당하는 nib 파일이 로딩되고, 뷰 컨트롤러와 뷰가 만들어진다. 사용자가 어떤 액션을 취해서 서브 뷰로 넘어가려 한다면 Segue가 트리거된다. 이 때 Segue는 destination 속성으로 어떤 뷰와 연결될 것인지를 알고 있다. 그러면 스토리보드 런타임은 destination에 해당하는 nib 파일을 읽어서 서브 뷰와 뷰 컨트롤러를 생성하고 초기화한다. 목적지 씬이 만들어진다면 화면의 전환은 source에게 present(_:animated:completion:)을 호출하여 화면을 전환하게 한다. 이 과정을 조금 더 자세히 들여다보자.

  1. Segue가 트리거된다.
  2. nib 파일이 로딩되고 destination에 해당하는 뷰 컨트롤러의 인스턴스가 생성, 초기화된다.
  3. source에 해당하는 MainViewController에게 prepare(for:sender:)가 호출된다.
  4. Segue는 perform()을 호출받는다. 이 메소드는 직접 호출해서는 안되며, 런타임에 의해 자동으로 호출된다.
  5. 다시 source에 present(_:animated:completion:)이나 그외 화면 전환용 메소드가 호출되면서 화면이 전환된다.

데이터 교환 1 – 서브 뷰에게 데이터를 전달하기

위에서 살펴본 과정중에서 화면 전환이 일어나기 직전에 소스씬은 prepare(for:sender:)를 호출받는다. 이 시점에 destination은 생성되어 있고, segue를 통해서 참조할 수 있다. 바로 여기가 서브 뷰에게 데이터를 전달할 위치이다. segue.destination을 통해서 서브 뷰 컨트롤러의 인스턴스를 참조할 수 있으므로, 서브 뷰 컨트롤러는 외부로부터 전달받을 데이터에 대한 프로퍼티를 만들어두고, 메인 뷰 컨트롤러가 이 메소드 내에서 데이터를 할당해주면 된다. 이 때 중요한 것은, 서브 뷰 컨트롤러가 초기화된 시점에는 해당 프로퍼티 값을 가지고 있지 못하다는 것이다. 따라서 서브 뷰 컨트롤러에서는 외부에서 넣어줘야 하는 값에 대해서는 옵셔널로 프로퍼티를 선언해야 한다.

만약 메인 뷰에서 서브 뷰로 넘겨줘야 하는 데이터의 타입이 MyData라 가정하자.  그러면 서브 뷰 컨트롤러의 코드에서 프로퍼티는 다음과 같이 정의되어야 한다.

/// SubViewController
class SubViewController: UIViewController
{
  var data: MyData = nil
...
}

메인 뷰 컨트롤러는 데이터를 전달해주기 위해서 다음과 같이 prepare(for:sender:)를 오버라이딩한다.

/// MainViewController
class MainViewController: UIViewController 
{
...
/// myData 값을 가지고 있다. 
var myData: MyData = ...
...
override func prepare(for segue:UIStoryboardSegue, sender:Any?) {
  if segue.identifier == "GotoSubView",
     let dest = segue.destination as? SubViewController
  {
    dest.myData = myData
  }
}
...

뷰 컨트롤러 하나에는 여러 개의 Segue가 연결될 수 있으므로, identifier 속성을 통해서 Segue를 구분해야 한다. (물론 하나 밖에 없다고 확정됐다면 굳이 체크할 필요는 없을 것이다.)

이렇게 데이터를 전달하고 나면, 앱이 알아서 화면을 전환할 것이다. 화면이 전환될 때 서브 뷰 컨트롤러의 viewWillAppear()가 호출될 것이므로, 해당 데이터를 화면에 표현하는 작업은 거기서 처리하면 될 것이다.

데이터 교환 2 – 서브 뷰에서 메인뷰로 데이터를 전달하기

iOS에서 뷰를 전환하는 것은 사실 전환이 아니라, 새로 표시할 뷰를 현재 뷰 위에 쌓아서 가리는 것이다. 따라서 Segue를 사용해서 서브 뷰를 표시했다면, 메인 뷰로 되돌아가는 것은 메인뷰를 다시 present 하는 것이 아니라, 자신을 호출한 뷰 컨트롤러에게 dismisss(animated:completion:)을 호출하여 자신을 버리도록 알려주게 된다. 서브 뷰 컨트롤러 입장에서는 dismiss가 되는 시점에 자신은 파괴될 것이므로 만약 여기서 데이터를 수정하거나 했다면, 이 데이터를 다시 메인뷰에게 돌려줄 방법이 있어야 한다. 이는 두 가지 방법이 있다.

나를 호출한 뷰 컨트롤러

첫째로 나를 호출한 뷰 컨트롤러를 이용하는 것이다. 모든 UIViewControllerpresentingViewController라는 프로퍼티를 가지고 있는데, 바로 자신을 호출하여 표시한 뷰 컨트롤러를 참조한다. 그렇다면 이걸 바로 사용하면 되는가? 만약 스토리보드를 사용하지 않고 화면을 전환했다면 그래도 된다. 하지만 잊지 말아야 할 것은 나를 실제로 호출한 뷰 컨트롤러가 MainViewController가 될 것이라는 보장이 없다는 것이다. 메인 뷰 컨트롤러에서 네비게이션 컨트롤러를 가지고 있었다면, 실제로는 네비게이션 컨트롤러가 나를 호출했을 수도 있다. 따라서 이 방법은 유동적인 요소가 있는 상황에서는 써먹을 수가 없으므로 그리 권장하지는 않겠다.

델리게이트

델리게이트는 훌륭한 대화수단이지.

죽음을 앞둔 서브 뷰 컨트롤러는 자신이 공들여서 가꾼 데이터를 그냥 날려버릴 위기에 처해있다. 무턱대고 데이터를 넘겨주자니 방법도 없고, 특히 네비게이션 컨트롤러는 당체 믿을 수가 없다. 이 때 가장 합리적인 선택은, 믿을만한 대리인에게 데이터를 위임해주는 것이다. 이 방식이 특히 좋은 이유는 확장성이다. 델리게이트와 상호작용을 위해서는 어떤 식으로 데이터를 전달할 것인가에 대한 약속이 미리 정해져있어야 한다. 델리게이트는 특정한 클래스로 고정될 필요없이, 어떤 프로토콜이기만 하면 된다. 따라서 서브 뷰 클래스는 자신의 데이터를 넘겨 받을 클래스가 누군지 알고 있을 필요가 없다.

델리게이트 패턴 적용하기

이 상황에서는 메인뷰 컨트롤러가 서브 뷰 컨트롤러의 델리게이트가 되어야 한다. 그러기 위해서 먼저 델리게이트 프로토콜을 하나 정의하도록 하자.

protocol MyDataDelegate {
  func providerSent(_ data: MyData) 
}

여기에는 기본적으로 달랑 하나의 메소드만 정의했다. 바로 데이터를 제공해주기로 한 고용주(?)가 나에게 데이터를 보냈다는 의미이다. 그러면 메인 뷰 컨트롤러는 다음과 같은 식으로 약간 디자인이 변경된다.

class MainViewController: UIViewController, MyDataDelegate {
...
var myData : MyData
...

// Adopt MyDataDelegate
func providerSent(_ data: MyData) {
  // 외부에서 보내준 데이터를 내가 받아서 갖는다.
  myData = data
}
...

그리고 서브 뷰 컨트롤러의 델리게이트가 내가 되어야 한다. 언제? 바로 prepare(for:sender:)에서!

@override func prepare(for segue:UIStoryboardSegue, sender:Any?) {
  if segue.identifier == "GotoSubView",
     let dest = segue.destination as? SubViewController
  {
    dest.myData = myData
    dest.delegate = self  // 내가 서브뷰의 델리게이트가 된다! 
  //^^^^^^^^^^^^^^^^^^^^
  }
}

이제 서브 뷰 컨트롤러에서는 다음과 같이 델리게이트를 선언한다. 물론 델리게이트도 최초 초기화시에는 존재하지 않으므로 옵셔널이어야 할 것이다. 또한 뷰를 델리게이트에게 되돌려주는 시점은 자신의 뷰가 사라지는 시점이면 된다.

class SubViewController: UIViewController {
...
var myData: MyData?
var delegate: MyDataDelegate?
...

// 뷰가 제거되기 전에 델리게이트에게 데이터를 위임하자.
override func viewWillDisappear(_ animated: Bool)) {
  super.viewWillDisappear(animated)
  delegate?.providorSent(myData)
}

정리

여기까지 스토리보드에서 뷰 전환시에 데이터를 양방향으로 주고 받는 방법에 대해 살펴보았다. 스토리보드를 사용하지 않는 경우에라도 방법은 사실 여기서 소개한 것과 똑같다. present하기 직전에 서브 뷰에 데이터와 델리게이트를 지정해주는 부분만 차이가 있고, 서브 뷰에서 데이터를 되돌려주는 방법은 여전히 동일하게 델리게이트 메소드를 호출하기만 하면 된다.

물론 앱 델리게이트라는 모든 뷰에서 전역적으로 사용할 수 있는 객체가 있기도 해서, 다른 뷰 컨트롤러를 참조할 수 없는 위치에서 앱 델리게이트를 전역 데이터 스토리지처럼 쓰던 시절도 존재했지만, 이건 마치 전역 변수를 쓰는 것처럼 몸에 해로운 습관이다. 여기서 보여진 패턴에 익숙해지면, 앱이 커지고 복잡해지더라도 여전히 동일한 패턴을 사용해서 뷰 컨트롤러간의 데이터 교환에 어려움을 겪지는 않을 것이다.


  1. Foundation 내에서도 인터페이스 빌더 번들은 Nib이라고 언급되며, 때문에 .xib 파일도 실제 읽을 때 nib 이라고 읽으며, 대체로 nib 파일로 통칭해서 사용된다.