Navigation Controllers

Navigation controller(이하 NavC)는 계층 구조의 컨텐츠를 drill-down 형식으로 제공하기 위해 view controller들의 스택을 관리해준다. 이 계층 구조는 self-contained한데, 그 뜻은 필요한 기능들은 다 갖추고 있다는 의미로 해석하면 될 것 같다. 또한, 이 구조는 NavC가 관리하는, 아니면 우리가 만든 view controller(이하 VC)가 관리하는 view들의 집합체라고도 할 수 있다. 여기서 VC는 따로 view 계층 구조를 가지고 있고, NavC가 그 계층 구조 사이사이 넘나드는 것으로 보면 된다.

만약 커스텀으로 navigation interface를 구현했다고 하더라도 NavC를 직접 사용해야하는 부분이 반드시 있을 것이다. 예를 들면, navigation bar나 tool bar를 구성할 때 말이다.

Anatomy of a Navigation Interface

NavC의 가장 주된 일은 VC와 view를 띄워주는 것이다. 특히 NavC는 navigation bar를 띄워줘서 흔히 봤던 뒤로가기 버튼을 포함할 수 있다. Toolbar 역시 가능하다.

다음 그림은 navigation interface를 보여준다. 여기서 navigation view는 NavC의 view 프로퍼티에 저장되어 있는 view이다. 이 interface에 있는 다른 모든 view들은 불투명하며 NavC에 의해 관리되는 계층 구조의 일부라고 보면 된다.

앞서 말했듯이 navigation bar와 tool bar는 커스터마이징이 가능하지만, 이 계층 구조에서 view를 직접적으로 수정하면 절대 안된다! 따라서 유일한 방법은 UINavigationControllerUIViewController가 제공하는 함수를 사용하는 것이다. 최근에 한번 toolbar를 직접 설정해주려고 했는데 안되었던 이유가 여기서 나오는 것 같다.

The Objects of a Navigation Interface

NavC는 navigation interface를 구현하기 위해 여러 object들을 사용한다. 우리는 일부 object를 직접 제공할 필요가 있다. 이 문서에서는 한가지 예시를 들어주는데, 특히 보여줄 컨텐츠를 가진 VC를 제공하는 역할을 직접 해주면 된다고 한다. 만약 NavC로부터 오는 notification에 응답하고 싶다면 delegate object를 사용하면 된다. 또한 NavC는 navigation bar, tool bar 같은 view들을 만들고 관리해준다. 다음 그림은 NavC와 핵심 object 간의 관계를 보여준다.

참고로 navigation bar와 toolbar는 일부 외형적, 행동 양식만 커스터마이징 할 수 있다. NavC는 오직 이 bar들을 구성하고 보여줘야하는 책임만 가지고 있다. 추가적으로 UINavigationBar의 delegate와 자동으로 할당시켜 놓기 때문에 직접 해줄 필요가 없다.

위 그림에서 보이는 navigation 스택에 있는 VC와, delegate는 수정이 가능하다. 스택은 당연히 LIFO 구조로 동작한다. 이 스택의 첫번째 요소는 root VC인데 절대 스택에서 빼낼 수 없다. 참고로 스택에 넣고 빼고 하는 작업은 UINavigationController에 있는 메소드로 할 수 있다.

다음 그림을 봐보자. 이 그림은 NavC와 위에서 언급한 스택 간의 관계를 보여준다. 참고로 그림에서 보이는 topViewControllervisibleViewController는 반드시 같을 필요는 없다. Modal로 VC가 떴을 때가 이에 해당한다. 왜냐하면 Modal VC는 topViewController에 할당되지 않기 때문이다.

NavC의 또 다른 주 책임은 사용자의 액션에 의해 스택에서 추가되고 삭제되는 것에 반응하는 것이다. 각각 추가된 VC는 앱의 데이터를 보여줘야하는 책임을 가지게 된다. 일반적으로 사용자가 어떤 아이템을 누르면, 우리는 그 아이템과 관련된 데이터를 가진 VC를 만들고 스택에 넣어줘야한다.

생각보다 단순한 과정이다. 다만, 특정 클래스에 의해 스택에 넣어지도록 하는 것은 좋지 않다. 제대로 이해한 것인지 모르겠지만, 공식문서에 따르면 데이터를 VC간에 전달받는 과정에서 이런 실수가 나오는 것 같다. 따라서 이런 경우에는 delegate 패턴을 활용하여 해결하도록 하자!

대부분 코드로 스택에서 꺼내는 경우는 없다고 보면 된다. 그냥 뒤로가기 버튼만 누르면 알아서 해당 처리를 해준다.

Creating a Navigation Interface

Navigation interface를 만들고 싶다면, 어떤 의도를 가지고 만들 것인지 결정해야한다. 또한 이는 데이터를 조직하는데 있어 중요한 과정이기 때문에 다음 경우에만 만드는 것이 좋다.

  • Window의 root VC로 사용
  • Tab bar의 tab VC로 사용
  • Split view interface에서 2개의 root VC 중 하나로 사용(아이패드만 해당)
  • 또다른 VC로부터 modal로 띄우기
  • Popover 형태로 보여주기(아이패드만 해당)

위에서 3가지 시나리오에서 NavC는 가장 기본적인 부분을 제공하고 앱이 종료될때 까지 그대로 남아있게된다. 나머지 2개에서는 임시 용도로 사용하는 느낌이고, VC의 역할과 동일하다고 보면 된다. 차이는 오직 single VC에서는 못하는 navigation을 제공한다는 점에 있다.

Defining the Content View Controllers for a Navigation Interface

모든 navigation interface는 root 레벨을 대표하는 데이터의 레벨을 가지고 있다. 이 레벨이 바로 interface의 시작점이다. 사실 해석만으로는 무슨 이야기인지 확 와닿지는 않는다. 그래도 다행히 공식문에서 다음 그림을 제공한다. 각 VC마다 데이터를 표현하는 방식이 다르다. 이게 바로 데이터의 레벨을 말한 것 같다.

Navigation interface를 만들기 위해 먼저 어떤 데이터를 각 레벨마다 어떤 구조로 표현할 것인지를 반드시 정해야한다. 데이터를 각 레벨마다 맞는 방법으로 관리하고 보여주는 VC를 만들어줘야 한다. 만약 표현방법이 레벨마다 같다면, 같은 VC의 인스턴스를 만들고 사용해야한다.

각 VC는 가장 마지막단에 있는 경우만 제외하고 다음 계층으로 가기 위한 방법을 반드시 제공해야한다. 위 그림의 사진 앱을 예로 들자면, Album list controller에서 한 cell을 누르면 다음 VC인 Photo album controller의 인스턴스가 만들어지고 보여지는 과정이 수반된다.

Creating a Navigation Interface Using a Storyboard

스토리보드로 만들어보자. 다음과 같은 과정을 따라가보자.

  1. 라이브러리에서 NavC를 드래그 & 드롭
  2. Interface builder가 NavC와 VC를 만들고 서로 관계를 만들어준다. VC가 NavC의 root VC라는 것을 알려주는 것이다.
  3. 만들어진 것은 첫번째로 띄우도록 설정하자!

Creating a Navigation Interface Programmatically

이곳은 옛날 코드이기 때문에 넘어가도록 하겠다.

Adopting a Full-Screen Layout for Navigation Views

일반적으로 navigation interface는 컨텐츠를 navigation bar와 toolbar 사이 공간에 보여준다. 하지만 원한다면 풀스크린으로 보여줄 수 도 있다. 이 풀스크린 layout에서는 컨텐츠 view가 navigation bar와 status bar, toolbar 밑으로 내려가진다.

이런 것들을 결정할 때 NavC는 다음과 같은 여러가지 요소를 고려한다.

  • 아래 깔려있는 window 혹은 parent view가 전체 스크린 bounds 사이즈에 맞춰져있는지?
  • Navigation bar가 투명하게 설정되어있는지?
  • Toolbar를 사용중이라면 투명하게 설정되어있는지?
  • 아래 깔려있는 VC의 wantsFullScreenLayout(지금은 deprecated)이 true인지?

이 요소들은 view의 최종 사이즈를 정하는데 사용된다. 수록한 순서 역시 고려하는 순서와 일치한다. 참고로 window 사이즈가 한계 요소이다. 예를 들어 앱의 main window(modal로 띄웠다면 parent view)가 스크린을 다 담지 못한다면, view 역시 마찬가지이다. 그렇다면 navigation bar나 toolbar가 현재 보이는 상태라고 생각해보자. 이런 경우에는 view가 어떤 layout을 사용하는지 신경쓸 필요가 없다. 왜냐하면 NavC는 절대 navigation bar 아래로 컨텐츠를 보여주지 않기 때문이다. 위에서 언급한 bar들을 사용하면 그에 맞게 영역이 조정된다는 의미인 것 같다.

만약에 navigation interface와 스크린 전체를 사용하는 컨텐츠를 같이 사용하고 싶다면 다음 스텝을 따라보자.

  1. 컨텐츠를 담을 view의 frame을 스크린에 맞도록 조정하라. view의 auto-resizing 요소도 설정하는 것을 잊으면 안된다. 이 요소는 사이즈 재조정이 필요할 때, 그에 맞게 재조정해주는 역할을 한다. 또는 setNeedsLayout() 메소드를 사용해도 된다.
  2. Navigation bar 아래로 두기 위해 NavC의 translucent를 true로 설정
  3. Toolbar 아래로 두기 위해 toolbar의 translucent를 true로 설정
  4. Status bar 아래로 두기 위해 VC의 wantsFullScreenLayout을 true로 설정

Navigation interface를 사용할 때는, window와 view의 사이즈가 반드시 적절히 정해져야한다. 다시 말해 UIScreenbounds 프로퍼티에 맞춰야한다. 사실 window 사이즈를 풀스크린 bounds로 맞추는 것이 좋긴 하다.

만약 modal로 VC를 띄운다면, 컨텐츠는 띄워진 VC에 의해 조정된다. Modal의 경우 bar 밑에 두고 말고 하는 것은 parent view를 따라가므로 참고하자!

Modifying the Navigation Stack

앞서 말했듯이 NavC를 위해 object를 만들어줘야한다고 했다. 그 예중 하나가 보여 줄 컨텐츠를 포함한 VC를 제공해야한다는 것이다. 이를 코드를 통해서 아니면 사용자와의 상호작용을 통해서 할 수 있다. 위에서 스택을 사용한다고 했었는데, 이 스택을 관리할 수 있는 몇가지 옵션들이 있다.

  • 다음 레벨의 데이터 보여주기

    Segue나 pushViewController(_:animated:) 메소드를 통해 스택에 새로운 VC를 추가할 수 있다.

  • 이전 레벨로 돌아가기

    뒤로가기 버튼을 주로 사용하고, popViewController(animated:)를 사용해서 스택에서 가장 위에 있는 VC를 삭제할 수 있다.

  • 스택을 이전 상태로 만들기

    setViewControllers(_:animated:)를 통해서 앱이 실행되었을 때 NavC를 이전 상태로 만들 수 있다. 이를 통해 다른 앱에서 작업을 하다가 돌아왔을 때 같은 화면에서 이어나갈 수 있게 된다. 물론 이전 상태로 복구하기 위해 현재 정보를 반드시 저장해 둘 필요가 있다. 저장했던 내용을 다시 읽는 것은 위 함수를 호출하기 전에 해야한다.

    이 함수를 통해 다른 데이터 계층으로 넘어갈 수도 있지만, 혼선을 줄 수 있기 때문에 특별한 경우에만 사용하자!

  • Root VC로 돌아가기

    popToRootViewController(animated:)를 사용하면 root VC를 제외한 나머지 VC가 스택에서 모두 사라진다. 따라서 root로 돌아갈 수 있다.

  • 특정 위치로 돌아가기

    popToViewController(_:animated:)로 원하는 VC로 돌아갈 수 있다. 이는 어떤 컨텐츠를 띄우기 보다는 어떤 컨텐츠를 수정하는 상황을 관리하는 용도로 사용할 수 있다. 예를 들면, 어떤 다수의 수정 작업을 한번에 모두 취소하고 싶을 때 같은 경우다.

메소드들을 보면 모든 animation과 관련된 인자들이 있다. NavC는 알아서 상황에 맞게 적절한 animation을 보여준다.

Monitoring Changes to the Navigation Stack

스택에 VC가 추가되고 삭제될때 마다 NavC는 VC들과 delegate에게 스택이 변했다고 메시지를 보낸다. 다음 그림은 이 과정을 나타낸다.

그림에서도 나오는 메소드들을 통해 VC들 간에 협력하게 할 수 있다. 예를 들면, 어떤 상태를 공유한다던가 말이다. 만약 다수의 VC를 넣거나 빼게된다면, 보였던 혹은 곧 보여질 VC, 단 하나와 관련된 메소드만 호출된다. 중간에 있는 VC의 메소드는 호출되지 않는다는 것이다.

Customizing the Navigation Bar Appearance

Navigation bar는 수없이 언급했지만 interface 내에서 흐름을 담당하는 view이고 특정 역할을 한다. 일관성을 유지하고 interface를 만드는데 필요한 일을 줄이기 위해, 각 NavC들은 자기 자신만의 navigation bar를 만들고 그 컨텐츠를 관리하는 가장 큰 책임을 맡고 있다. 필요하다면 다른 객체(여기서는 다른 VC를 언급)와 상호작용하기도 한다.

Configuring the Navigation Item Object

Navigation bar의 구조는 NavC의 구조와 매우 비슷하다. NavC처럼 다른 객체에 의해 제공되는 컨텐츠를 위한 container 역할을 한다. 또한 navigation bar는 UINavigationItem을 담기 위한 스택 구조를 가지고 있다. 이를 Navigation item stack이라고 한다.

다음 그림은 런타임 시 navigation bar와 관련있는 핵심 객체들을 보여준다. 이 bar의 주인은 필요할때마다 item을 넣거나 빼 줄 책임을 가진다. 여기서 주인은 우리의 코드일 수도 있고 NavC가 될 수 있다. 추가적으로 bar는 적절한 navigation 환경을 제공하기 위해 item 스택에 포인터를 유지하고 있다. 뒤로가기 버튼을 통해 이전 VC로 넘어갈 수 있는 것을 보면 쉽게 이해할 수 있다.

아까도 언급했지만, NavC와 함께 사용 중이라면 delegate는 그 NavC로 자동으로 연결되어있다. 이를 바꾸는 것은 에러를 발생시킨다!

Navigation interface에서 각 VC는 navigationItem 프로퍼티를 가지고 있다. 이를 통해 item에 접근할 수 있다. VC와 item 모두 스택 구조를 가지고 있다고 했는데 서로 병렬 구조이다. 즉, 같은 순서로 스택에 쌓인다는 말이다.

Bar에 둘 수 있는 item의 위치는 left, right, center이다. 관련 프로퍼티는 아래 정리와 같다.

  • Left - backBarButtonItem & leftBarButtonItem

    기본적으로 뒤로가기 버튼은 왼쪽에 위치한다. 이는 backBarButtonItem을 통해 얻을 수 있다. 만약 커스텀 버튼이나 view를 할당하고 싶다면 leftBarButtonItem을 사용하자!

  • Center - titleView

    기본적으로 title이 있는 커스텀 view가 중앙에 위치한다. 물론 다른 view로 바꿀 수 있다. 이때 title 값을 제공하지 않는다면 VC의 title을 사용한다.

  • Right - rightBarButtonItem

    기본적으로 비어있다. 따라서 보통 현재 스크린을 수정하는 용도의 버튼을 두곤 한다.

알아둬야 할 것은 특정 위치의 커스텀 control이 무시될 수 있다는 점이다. 이를 명심하자!

아래 그림은 navigation bar가 navigation interface에 어떻게 속해있는지를 보여준다. 그림에서처럼 현 VC에서는 가운데, 오른쪽에 item이 위치해있고, 이전 VC의 아이템이 왼쪽에 위치해있다.

참고로 우측에서 UISegmentedControl을 사용한 것 처럼 item을 만들 때 어떤 view를 래핑해서 만들 수도 있다. 그리고 title view를 제공하지 않는다면 앞서 말했듯이 현재 VC의 title을 가지고 item을 만들어준다.

Showing and Hiding the Navigation Bar

NavC와 함께 navigation bar를 사용했을 때 UINavigationControllersetNavigationBarHidden(_:animated:)를 통해서 bar를 가렸다 보여줬다 할 수 있다. 절대 isHidden 프로퍼티를 직접 사용해서 숨기면 안된다. 반드시 위 메소드를 사용하자! 이처럼 NavC의 메소드를 사용하는 것은 매우 정교한 작업을 공짜로 할 수 있다. 예를 들면 라이프싸이클 메소드와 함께 사용한다든지 말이다.

보통 사용자들은 뒤로가기 버튼이 반드시 필요하다고 생각하기 때문에 뒤로 갈 수 있는 방법을 제공하지 않고 bar를 숨기는 것은 하지 않는게 좋다. 가장 흔한 방법은 터치 이벤트를 가지고 bar를 가리고 나타나게 하고 하는 것이다. 스와이프 제스처로 할 수도 있지만 덜 명확한 방법이다.

Modifying the Navigation Bar Object Directly

Navigation interface에서는 NavC가 UINavigationBar 객체를 가지고 있고 그것을 관리할 책임을 가지고 있다. 따라서, 그 객체들의 boundsframe, alpha를 직접 변경하는 것은 허용되지 않는다. 그러나 다음 몇가지 프로퍼티로 수정해 볼 수는 있다.

  • barStyle
  • translucent
  • tintColor

다음 그림은 위 프로퍼티가 bar에 어떤 영향을 미치는지 보여준다.

참고로 translucent 스타일에서는, 어떤 VC의 main view가 스크롤 view라면, navigation bar는 bar 아래에서도 컨텐츠가 스크롤될 수 있게 컨텐츠의 inset 값을 자동으로 조정해준다고 한다. 뭐 투명한 거니깐 당연한 것 같다. 참고로 다른 view에 대해서는 이런 조정 작업이 발생하지 않는다.

만약에 이 bar를 가리고 싶다면, 다시 한번 말하지만 직접 바꾸지 말고, setNavigationBarHidden(_:animated:)를 사용하자!

특정 VC마다 bar의 생김새를 커스터마이징하고 싶다면, UINavigationItem 객체를 수정하면 된다. 이 값은 navigationItem 프로퍼티로 얻을 수 있다고 전에도 언급했다. VC는 우리가 요청하기 전까지 item을 만들지 않기 때문에, 필요한 경우에만 요청하는 것이 좋다.

만약 item을 수정하지 않기로 결정했다면, 모든 상황을 만족하는 default 객체가 만들어질 것이다. 어떤 커스텀이든 이 default보다 우선순위가 높다.

스택에서 가장 위에 있는 VC에서 왼쪽에 있는 item은 다음과 같은 룰을 통해 결정된다.

  • 만약 leftBarButtonItem 프로퍼티에 커스텀 버튼을 할당한다면, 그 item은 가장 높은 우선순위를 가진다.
  • 커스터마이징 하지 않고, 한 레벨 아래의 VC의 backBarButtonItem 프로퍼티에 유효한 item이 있다면, 그 item을 보여준다.
  • 어떤 VC에도 item이 설정되어있지 않으면, 기본 뒤로가기 버튼이 사용되고 이전 VC의 title 값이 그대로 사용된다. 만약 root VC라면 뒤로가기 버튼은 당연히 없다.

다음은 중앙에 있는 item과 관련된 룰을 살펴보자.

  • 가장 위에 있는 VC의 titleView 프로퍼티에 커스텀 view를 할당한다면, 그 view를 보여준다.
  • 커스터마이징하지 않았다면, VC의 title을 포함하는 view를 보여준다. 만약 title 값이 nil이라면 title 이름 그대로를 보여준다.

마지막으로 우측에 있는 item과 관련된 룰을 보자.

  • rightBarButtonItem 프로퍼티로 설정한 item이 있다면 그것을 보여준다.
  • 커스텀한 것이 없다면 아무것도 보여주지 않는다.

혹시 bar 위에 prompt 형식으로 텍스트를 띄워주고 싶다면, prompt 프로퍼티에 값을 할당해보자. 이 값은 UINavigationItem 객체에서 확인할 수 있다.

다음 그림은 커스텀 view와 prompt를 포함한 다양한 bar 구성을 보여준다.

코드가 있지만 Objective-C로 되어있어 건너뛰겠다.

Navigation item을 설정하는 것은 코드로 하는 것이 가장 흔한 방법이다. Interface Builder, 즉 스토리보드로 작성할 수도 있지만, 코드로 하는 것이 간단하다. 이 코드는 viewDidLoad()에서 하는 것이 좋다!

Using Edit and Done Buttons

편집 기능을 제공하는 view의 경우 그 기능을 끄고 켤 수 있는 특별한 버튼을 가질 수 있다. 바로 editButtonItem 프로퍼티인데, Edit과 Done을 왔다갔다하는 버튼이다. 이 버튼은 내부적으로 setEditing(_:animated:)를 호출한다. 코드가 역시 옛날 코드라서 한번 간단한 코드를 만들어봤다.

@IBOutlet weak var navigationBar: UINavigationBar!

override func viewDidLoad() {
    super.viewDidLoad()

    let navItem = UINavigationItem()
    navItem.rightBarButtonItem = self.editButtonItem
    navigationBar.setItems([navItem], animated: true)
}

override func setEditing(_ editing: Bool, animated: Bool) {
    super.setEditing(editing, animated: animated)
    if editing {
        print("can edit")
    } else {
        print("can't edit")
    }
}

코드를 보면 알겠지만, 반드시 setEditing(_:animated:)를 반드시 override 해야한다.

Displaying a Navigation Toolbar

iOS 3.0부터 navigation interface는 toolbar를 띄우고 관련 item을 채워 넣을 수 있다. 이 toolbar 자체는 NavC 객체에 의해 관리된다. 위 그림에서도 봤었다! 만약 Navigation 스택에 변화가 생기면, NavC는 그 변화를 애니메이션으로 보여준다. 또한, 사라지고 생기는 상황에서도 애니메이션을 적용시킬 수 있다.

Toolbar를 구성할 때 다음 사항을 반드시 따르도록 하자!

  • isToolbarHiddenfalse로 설정하여 toolbar를 보여주자.
  • UIBarButtonItem 객체를 toolbarItems 프로퍼티에 할당하자. 특정 VC에서는 toolbar를 가리도록 설정할 수도 있다.

다음 그림은 어떤 객체가 VC와 연관되어 toolbar를 구성하는지 보여준다. Toolbar에 들어가는 item은 배열에 넣은 순서대로 나타난다.

배열은 toolbarItems라는 프로퍼티로 확인할 수 있다. 여기에는 어떤 버튼 item(fixed & flexible space, 시스템 버튼, 커스텀 버튼)이든 들어갈 수 있다.

Specifying the Toolbar Items

들어갈 아이템을 구성할 때, 적절한 타겟과 액션을 버튼과 묶어야 된다는 것을 기억하라. 대부분 타겟은 VC 자체가 될 것이다. 아래 그림은 toolbar에 segmented control을 넣은 것을 보여주고 있다.

위 그림을 스토리보드로 구성하는 방법을 알아보자.

  1. 먼저 라이브러리에서 toolbar를 가져오자.
  2. 라이브러리에서 2개의 flexible space 아이템을 가져와서 배치하자.
  3. 역시 라이브러리에서 segmented control을 찾아 2번에서 추가한 아이템 사이에 배치하자.

물론 코드로도 할 수 있으나 옛날 코드라서 건너뛰도록 하겠다.

참고로 VC는 setToolbarItems(_:animated:)를 통해 동적으로 아이템을 추가하고 뺄 수 있다.

Showing and Hiding the Toolbar

특정 VC에서 toolbar를 가리고 싶을 때, hidesBottomBarWhenPushed 프로퍼티를 true로 설정해보자. 이렇게 하면 NavC가 새 VC를 만날 때마다 사라지게 할 것이다.

혹시 가끔 사라지게 하고 싶다면, setToolbarHidden(_:animated:)를 사용해보자. 이 함수는 위에서도 언급한 setNavigationBarHidden(_:animated:)와 같이 사용하여 사진 앱처럼 풀스크린으로 view를 보는 상황을 구현할 수 있다.

참고 사이트

Navigation Controllers

profile
NAVER WORKS iOS Dev

관심 있을 만한 포스트

0개의 댓글