View Controller Definition

Panther·2021년 7월 15일
0

Defining Your Subclass

앱의 컨텐트를 제공하기 위해 UIViewController의 커스텀 서브클래스를 사용하게 될 것입니다. 대부분의 커스텀 뷰 컨트롤러는 컨텐트 뷰 컨트롤러입니다. 즉 스스로 고유한 뷰를 갖고 있고 이와 같은 뷰에 있는 데이터에 대한 책임이 있습니다. 반면에 컨테이너 뷰 컨트롤러는 스스로 모든 뷰를 갖고 있는 것은 아닙니다. 몇 가지는 다른 뷰 컨트롤러에 의해 관리됩니다. 컨텐트와 컨테이너 뷰 컨트롤러를 정의하는 단계는 동일하고, 아래 섹션에서 내용을 다룰 것입니다.

컨텐트 뷰 컨트롤러에서 가장 일반적인 부모 클래스는 아래와 같습니다.

  • 뷰 컨트롤러의 메인 뷰가 테이블일 때 UITableViewController를 사용하시기 바랍니다.
  • 뷰 컨트롤러의 메인 뷰가 컬렉션 뷰일 때 UICollectionViewController를 사용하시기 바랍니다.
  • 다른 유형의 뷰 컨트롤러는 UIViewController를 사용하시기 바랍니다.

컨테이너 뷰 컨트롤러에서 부모 클래스는 존재하는 컨테이너 클래스를 수정하는지 혹은 스스로 생성할 것인지에 따라 달라집니다. 존재하는 컨테이너의 경우 수정하려는 뷰 컨트롤러 클래스를 선택하시기 바랍니다. 새로운 컨테이너 뷰 컨트롤러의 경우 보통 UIViewController를 서브클래싱 합니다. 컨테이너 뷰 컨트롤러의 생성에 관한 더 많은 정보는 Implementing a Container View Controller를 살펴보시기 바랍니다.

Implementing a Container View Controller는 이 글의 아래 뿐에 나옵니다.

Defining Your UI

Xcode에 있는 스토리보드를 사용해 뷰 컨트롤러에 대한 UI를 시각적으로 정의하시기 바랍니다. UI 생성을 코드로 작성할 수 있을지라도 스토리보드는 뷰 컨트롤러의 컨텐트를 시각화하기 위한 훌륭한 방법입니다. 또한, 필요한 경우 다른 환경에 따라 뷰 계층구조를 커스터마이징하는 좋은 방법이기도 합니다. UI를 시각적으로 설계하는 것은 변경을 빠르게 만들고 앱을 빌드하거나 실행할 필요없이 결과물을 볼 수 있도록 도와줍니다.

Figure 4-1은 스토리보드의 예시를 보여줍니다. 각각의 사각형 영역은 뷰 컨트롤러를 나타내고 있고 관련이 있는 뷰 또한 나타내고 있습니다. 뷰 컨트롤러 사이의 화살표는 뷰 컨트롤러의 관계 및 세그입니다. 관계는 컨테이너 뷰 컨트롤러와 자식 뷰 컨트롤러를 연결합니다. 세그는 인터페이스에 있는 뷰 컨트롤러 사이의 탐색(navigate)을 돕습니다.

Figure 4-1 A storyboard holds a set of view controllers and views

각각의 새로운 프로젝트는 하나 혹은 하나 이상의 뷰 컨트롤러를 이미 포함하고 있는 메인 스토리보드를 갖습니다. 라이브러리에서 드래그를 통해 새로운 뷰 컨트롤러를 캔버스에 추가할 수 있습니다. 새 뷰 컨트롤러는 연관된 클래스를 갖지 않는 상태입니다. 그렇기 때문에 아이덴티티 인스펙터를 사용해 클래스 할당을 해줘야 합니다.

아래와 같은 것들을 수행하기 위해 스토리보드 에디터를 사용하시기 바랍니다.

  • 뷰 컨트롤러에 뷰를 추가, 정렬, 설정할 수 있습니다.
  • outlet과 액션을 연결할 수 있습니다. Handling User Interactions를 살펴보시기 바랍니다.
  • 뷰 컨트롤러 사이의 관계와 세그를 생성할 수 있습니다. Using Segues를 살펴보시기 바랍니다.
  • 다른 사이즈 클래스를 위해 레이아웃과 뷰를 커스텀으로 구성할 수 있습니다. Building an Adaptive Interface를 살펴보시기 바랍니다.
  • 제스쳐 리코그나이저를 추가해 사용자와 뷰 사이의 상호작용을 처리할 수 있습니다. Event Handling Guide for iOS를 살펴보시기 바랍니다.

Handling User Interactions는 바로 아래에 등장합니다. Using Segues는 다음 글에 등장합니다. Building an Adaptive Interface는 이 시리즈의 네 번째 글에 등장합니다.

Event Handling Guide for iOS
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/EventOverview/Introduction/Introduction.html#//apple_ref/doc/uid/10000060i

인터페이스를 설계하는 데 스토리보드 사용이 처음이라면 Start Developing iOS Apps Today에서 단계적 지시사항을 볼 수 있습니다 (Retired).

Start Developing iOS Apps Today(Retired)
https://developer.apple.com/library/archive/referencelibrary/GettingStarted/RoadMapiOS-Legacy/chapters/Introduction.html

Handling User Interactions

앱의 리스폰더 개체는 들어오는 이벤트를 처리하고 적합한 액션을 취합닏다. 뷰 컨트롤러가 리스폰더 객체일지라도 터치 이벤트를 직접적으로 처리하는 경우는 거의 없습니다. 대신 뷰 컨트롤러는 아래와 같은 방식으로 이벤트를 처리합니다.

  • 뷰 컨트롤러는 상위 레벨 이벤트를 처리하기 위해 액션 메소드를 정의합니다. 액션 메소드는 아래 내용에 반응합니다.
    • 특정 액션으로, 컨트롤과 몇 가지 뷰는 특정 상호작용을 알리기 위해 액션 메소드를 호출합니다.
    • 제스처 리코그나이저로, 제스처 리코그나이저는 제스처의 현태 상태를 알리기 위해 액션 메소드를 호출합니다. 상태 변화를 처리하거나 끝난 제스쳐에 반응하기 위해 뷰 컨트롤러를 사용하시기 바랍니다
  • 뷰 컨트롤러는 시스템 혹은 다른 객체로부터 받을 수 있는 노티피케이션을 탐색합니다. 노티피케이션은 변경사항을 알리고, 뷰 컨트롤러의 상태를 업데이트하기 위한 방법입니다.
  • 뷰 컨트롤러는 다른 객체의 데이터 소스 혹은 딜리게이트 역할을 합니다. 뷰 컨트롤러는 테이블 뷰, 컬렉션 뷰를 위한 데이터를 관리하는 데 사용됩니다. 딜리게이트에서 업데이트된 위치의 값을 보내는 객체인 CLLocationManager 객체와 같은 것을 딜리게이트로써 뷰 컨트롤러를 사용할 수도 있습니다.

이벤트에 반응하는 것은 뷰의 컨텐트를 업데이트 하는 데 관여합니다. 컨텐트는 뷰에 대한 참조를 갖는 것을 요구하고 있을 것입니다. 뷰 컨트롤러는 이후 수정이 필요할 모든 뷰에 대한 outlet을 정의하기에 좋은 장소입니다. Listing 4-1에 보이는 구문을 사용해서 outlet을 속성으로써 선언하기 바랍니다. 커스텀 클래스는 두 가지 outlet(IBOutlet 키워드로 지정)과 한 가지 액션 메소드(IBAction 반환 타입으로 지정)를 정의하고 있습니다. outlet은 스토리보드에서 버튼과 텍스트 필드의 참조를 저장하고 있고, 액션 메소드는 버튼의 탭에 반응합니다.

Listing 4-1 Defining outlets and actions in a view controller class

OBJECTIVE-C

@interface MyViewController : UIViewController
@property (weak, nonatomic) IBOutlet UIButton *myButton;
@property (weak, nonatomic) IBOutlet UITextField *myTextField;
 
- (IBAction)myButtonAction:(id)sender;
 
@end
SWIFT

class MyViewController: UIViewController {
    @IBOutlet weak var myButton : UIButton!
    @IBOutlet weak var myTextField : UITextField!
    
    @IBAction func myButtonAction(sender: id)
}

스토리보드에서 뷰 컨트롤러의 outlet과 액션을 상응하는 뷰에 연결해야 하는 것을 기억하시기 바랍니다. 스토리보드에서 파일에서 outlet과 액션 연결은 뷰가 로드될 때 설정될 것을 보장해줍니다. outlet과 액션 연결을 인터페이스 빌더에서 연결하는 것과 관련한 더 많은 정보는 Interface Builder Connections Help를 살펴보시기 바랍니다. 이벤트 처리와 관련한 더 많은 정보는 Event Handling Guide for iOS를 살펴보시기 바랍니다.

Event Handling Guide for iOS
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/EventOverview/Introduction/Introduction.html#//apple_ref/doc/uid/10000060i

Displaying Your Views at Runtime

스토리보드는 뷰 컨트롤러의 뷰를 로딩하고 표시하는 처리를 매우 간단하게 만들어줍니다. UIKit은 필요할 때 스토리보드 파일로부터 뷰를 자동으로 로드합니다. 로딩 과정의 부분으로 UIKit은 아래와 같은 일련의 작업을 수행합니다.

  1. 스토리보드 파일에 있는 정보를 사용해 뷰를 인스턴스화합니다.
  2. 모든 outlet과 액션을 연결합니다.
  3. 뷰 컨트롤러의 뷰 속성에 루트 뷰를 할당합니다.
  4. 뷰 컨트롤러의 awakeFromNib 메소드를 호출합니다. 이 메소드가 호출될 때 뷰 컨트롤러의 특성 집합은 비어있습니다. 그리고 뷰는 마지막 위치에 놓여있지 않을 것입니다.
  5. 뷰 컨트롤러의 viewDidLoad 메소드를 호출합니다. 뷰를 추가하거나 삭제, 레이아웃 제약의 수정, 뷰를 위한 데이터 로드를 하려면 이 메소드를 사용하시기 바랍니다.

뷰 컨트롤러의 뷰를 화면에 표시하기 전에 UIKit은 뷰가 화면에 나타나기 전 혹은 후에 뷰를 준비할 수 있는 추가적인 기회를 줍니다. 구체적으로 UIKit은 아래와 같은 일련의 작업을 수행합니다.

  1. 뷰 컨트롤러의 viewWillAppear: 메소드를 호출함으로써 뷰가 화면에 나타날 것임을 알려줍니다.
  2. 뷰의 레이아웃을 업데이트합니다.
  3. 뷰를 화면에 나타냅니다.
  4. 뷰가 화면에 나타날 때 viewDidAppear: 메소드를 호출합니다.

뷰의 크기나 위치를 추가, 삭제, 수정하려고 할 때, 뷰에 적용할 수 있도록 모든 제약을 추가하거나 삭제해야 하는 것을 기억하시기 바랍니다. 뷰 계층구조에 대한 레이아웃 관련 변경사항을 주는 것은 UIKit이 레이아웃을 더럽히도록 만들 것입니다. 다음 업데이트 사이클 동안 레이아웃 엔진은 현재 레이아웃 제약을 사용해 뷰의 크기와 위치를 계산하고, 이와 같은 변경사항을 뷰 계층구조에 적용합니다.

스토리보드를 사용하지 않고 뷰를 생성하는 방법과 관련한 내용은 UIViewController 클래스 레퍼런스에 있는 view management information을 살펴보시기 바랍니다.

Managing View Layout

뷰의 크기와 위치가 바뀔 때, UIKit은 뷰 계층구조에 대해 레이아웃 정보를 업데이트합니다. 오토 레이아웃을 사용한 뷰의 설정의 경우 UIKit은 오토 레이아웃 엔진을 사용하고, 현재 제약에 따를 수 있도록 레이아웃을 업데이트하기 위해 오토 레이아웃 엔진을 사용할 것입니다. UIKit은 활성 프리젠테이션 컨트롤러와 같은 객체를 레이아웃 변경에 대해 알려주고 이를 따라 반응할 수 있도록 해줍니다.

레이아웃을 처리하는 동안 UIKit은 추가적인 레이아웃 관련 작업을 수행할 수 있도록 몇 가지를 알려줄 것입니다. 이 노티피케이션을 사용해서 레이아웃 제약을 수정하거나 레이아웃 적용 후 최종 수정을 하시기 바랍니다. 레이아웃이 처리되는 동안 UIKit은 각각의 뷰 컨트롤러에 영향을 미치는 아래 내용을 수행합니다.

  1. 필요한 경우 뷰 컨트롤러와 뷰의 특성 집합을 업데이트 합니다. When Do Trait and Size Changes happen?을 살펴보시기 바랍니다.
  2. 뷰 컨트롤러의 viewWillLayoutSubviews 메소드를 호출합니다.
  3. 현재 UIPresentationController 객체의 containerViewWillLayoutSubviews 메소드를 호출합니다.
  4. 뷰 컨트롤러 루트 뷰의 layoutSubviews 메소드를 호출합니다.
    이 메소드의 기본 구현은 사용 가능한 제약을 사용해 새로운 레이아웃 정보를 계산합니다. 메소드는 뷰 계층구조를 탐색하고 각각의 하위 뷰에 대해 layoutSubviews를 호출합니다.
  5. 뷰에 대해 계산된 레이아웃 정보를 적용합니다.
  6. 뷰 컨트롤러의 viewDidLayoutSubviews 메소드를 호출합니다.
  7. 현재 UIPresentationController 객체의 containerViewDidLayoutSubviews 메소드를 호출합니다.

뷰 컨트롤러는 레이아웃 처리에 영향을 미칠 수 있는 추가적인 업데이트를 수행하기 위해 viewWillLayoutSubviewsviewDidLayoutSubviews 메소드를 사용할 수 있습니다. 레이아웃이 이뤄지기 전에 뷰를 추가하거나 삭제할 수 있고 뷰의 크기, 위치를 업데이트할 수도 있습니다. 또한, 제약을 업데이트할 수도 있고 다른 뷰 관련 속성을 업데이트 할 수 있습니다. 레이아웃 이후 테이블 데이터를 리로드할 수도 있고, 다른 뷰의 컨텐트를 업데이트할 수도 있습니다. 또한, 뷰의 크기와 위치를 마지막으로 조정할 수도 있습니다.

레이아웃을 효과적으로 관리하기 위한 몇 가지 팁이 아래에 있습니다.

  • 오토 레이아웃을 사용하시기 바랍니다. 오토 레이아웃을 사용해 생성한 제약은 유연하고 다른 스크린 사이즈에 대한 컨텐트 위치를 설정할 수 있는 쉬운 방법입니다.
  • top과 bottom 레이아웃 가이드를 활용하시기 바랍니다. 이 가이드를 통해 컨텐트의 레이아웃을 설정하는 것은 컨텐트가 항상 시각적일 수 있음을 보장합니다. top 레이아웃 가이드의 위치는 status 바와 네비게이션 바의 높이에서 요소화할 수 있도록 합니다. 유사하게 bottom 레이아웃 가이드는 탭바 혹은 툴바의 높이를 요소화할 수 있도록 합니다.
  • 뷰를 추가하거나 삭제할 때 제약을 업데이트해야 합니다. 만약 뷰를 동적으로 추가하거나 삭제하려고 한다면, 그에 상응하는 제약을 업데이트해야 합니다.
  • 뷰 컨트롤러의 뷰를 애니메이션으로 동작하는 동안 일시적으로 제약을 삭제해야 합니다. UIKit Core Animation을 통해 뷰가 애니메이션으로 동작하는 동안 제약을 삭제하고 애니메이션이 끝나는 시점에 제약을 추가해야 합니다. 만약 뷰의 위치 혹은 크기가 애니메이션이 발생하는 동안 변경되었다면 제약을 업데이트해야 합니다.

프리젠테이션 컨트롤러와 뷰 컨트롤러 아키텍처에서 프리젠테이션 컨트롤러의 역할에 대한 더 많은 정보는 The Presentation and Transition Process에 있습니다.

The Presentation and Transition Process는 다음 글에 나옵니다.

Managing Memory Efficiently

대부분의 메모리 할당은 스스로 결정하기기에 달려있지만, Table 4-1은 메모리 할당, 해제와 관련해 UIViewController의 메소드를 보여주고 있습니다. 대부분의 해제는 객체의 강한 참조를 제거하는 것과 관련이 있습니다. 객체의 강한 참조를 제거하려면 속성과 변수에 대해 해당 객체가 nil을 가리킬 수 있도록 설정해야 합니다.

Table 4-1 Places to allocate and deallocate memory

TaskMethodsDiscussion
뷰 컨트롤러에 의해 요구되는 중요한 데이터 구조의 할당Initialization 메소드커스텀 초기화 메소드(init의 이름이거나 그렇지 않을지라도)는 뷰 컨트롤러가 정상적인 상태의 객체로 둘 수 있도록 해줘야 하는 책임이 있습니다. 정상적인 작동을 보장할 수 있도록 어떠한 데이터 구조라도 이 메소드를 사용해 할당할 수 있도록 해줘야 합니다.
뷰에 보여질 수 있도록 데이터를 할당하거나 로드viewDidLoadviewDidLoad 메소드를 사용해 보여주고자 하는 데이터 객체를 로드해야 합니다. 이 메소드가 호출될 때까지 뷰 객체는 존재한다는 것과 정상적인 상태를 보장받을 수 있습니다.
로우 메모리 노티피케이션에 대한 반응didReceiveMemoryWarning뷰 컨트롤러와 관련해 중요하지 않은 객체를 해제하기 위해 이 메소드를 사용하시기 바랍니다. 가능한 많은 메모리를 해제할 수 있도록 해야 합니다.
뷰 컨트롤러가 요구하는 중요한 데이터 구조의 해제dealloc뷰 컨트롤러 클래스를 완전히 비우기 위할 때에만 이 메소드를 오버라이드하시기 바랍니다. 시스템은 클래스의 인스턴스 변수와 속성에 저장된 객체를 자동으로 해제합니다. 이를 통해 해제와 관련한 명시적인 행동은 불필요합니다.

Implementing a Container View Controller

컨테이너 뷰 컨트롤러는 여러 뷰 컨트롤러로부터 컨텐트를 단일 UI에 결합할 수 있는 방법입니다. 컨테이너 뷰 컨트롤러는 네비게이션을 촉진하기 위해 사용되고, 존재하는 컨텐트에 기반해 새로운 UI 타입을 생성하기 위해 사용됩니다. UIKit에 존재하는 컨테이너 뷰 컨트롤러의 예시는 UINavigationController, UITabBarController, UISplitViewController와 UI의 다른 부분들 사이에서 네비게이션을 촉진시키는 모든 것을 포함합니다.

Designing a Custom Container View Controller

컨테이너 뷰 컨트롤러는 루트 뷰와 몇 컨텐트를 관리한다는 점에서 컨텐트 뷰 컨트롤러와 유사합니다. 컨테이너 뷰 컨트롤러는 다른 뷰 컨트롤러로부터 컨텐트의 일부분을 가져온다는 점이 차이점입니다. 컨테이너 뷰 컨트롤러가 가져오는 컨텐트는 다른 뷰 컨트롤러의 뷰로 제한됩니다. 다른 뷰 컨트롤러의 뷰는 해당 뷰 컨트롤러의 뷰 계층구조에 속합니다. 컨테이너 뷰 컨트롤러는 삽입된 모든 뷰에 대한 크기와 위치를 설정합니다. 그러나 기존 뷰 컨트롤러가 해당 뷰 내부에 있는 컨트롤러를 관리합니다.

컨테이너 뷰 컨트롤러를 설계할 때 컨테이너 뷰 컨트롤러와 포함되는 뷰 컨트롤러 사이의 관계를 항상 이해해야 합니다. 뷰 컨트롤러 사이의 관계는 어떻게 컨텐트가 화면에 나타나야 할지에 대한 내용과 어떻게 컨테이너가 내부적으로 뷰 컨트롤러들을 관리해야 하는지를 알려주도록 도와줍니다. 디자인 프로세스 동안 아래와 같은 질문을 스스로 해보시기 바랍니다.

  • 컨테이너의 역할은 무엇이며, 컨테이너가 갖는 자식의 역할은 무엇인지 생각해야 합니다.
  • 동시에 얼마나 많은 자식이 보여져야 하는지 생각해야 합니다.
  • 형제 뷰 컨트롤러 사이의 관계가 무엇인지 생각해야 합니다.
  • 어떻게 자식 뷰 컨트롤러가 컨테이너에 추가되어야 하는지 혹은 제거되어야 하는지를 생각해야 합니다.
  • 자식이 갖는 크기와 위치가 변경될 수 있는지를 생각해야 하며, 어떤 조건에서 변경이 발생하는지 생각해야 합니다.
  • 컨테이너가 어떠한 장식물 혹은 네비게이션 관련 뷰를 제공하고 있는지 생각해야 합니다.
  • 컨테이너와 자식 사이에서 요구되는 커뮤니케이션의 종류가 무엇인지 생각해야 합니다. UIViewController에 정의된 표준이 아닌 이벤트를 컨테이너가 자식에게 알려야 하는지에 대해 생각해야 합니다.
  • 컨테이너의 모양이 다른 방식으로 설정될 수 있는지 생각해야 하며, 만약 그렇다면 어떻게 이뤄지는지를 생각해야 합니다.

컨테이너 뷰 컨트롤러의 구현은 다양한 객체의 역할을 정의한 이후 상대적으로 단순합니다. UIKit으로부터 지켜야 하는 요구사항은 컨테이너 뷰 컨트롤러와 자식 뷰 컨트롤러 사이의 관계를 형식적은 부모-자식으로 설정해야 하는 것입니다. 부모-자식 관계는 자식이 관련있는 모든 시스템 메시지를 받을 수 있음을 보장합니다. 그 외에도 대부분의 실제 작동은 각각의 컨테이너에 대한 레이아웃과 포함된 뷰의 관리가 발생하는 동안 이뤄집니다. 컨테이너의 컨텐트 영역 모든 곳에 뷰를 위치시킬 수 있고, 뷰를 원하는 크기로 만들 수 있습니다. 데코레이션을 제공하거나 네비게이션을 돕기 위해서 뷰 계층구조에 커스텀 뷰를 추가할 수도 있습니다.

Example: Navigation Controller

UINavigationController 객체는 계층구조적 데이터 집합을 통해 네비게이션을 지원합니다. 네비게이션 인터페이스는 한 번에 하나의 자식 뷰 컨트롤러를 제공합니다. 인터페이스 상단의 네비게이;션 바는 데이터 계층구조 내에서 현재 위치를 표시합니다. 그리고 한 단계 돌아가기 위한 돌아가기 버튼을 표시합니다. 데이터 계층 구조의 탐색은 자식 뷰 컨트롤러에게 맡겨지고, 테이블 혹은 버튼의 사용이 포함될 수 있습니다.

뷰 컨트롤러 사이의 탐색은 네비게이션 컨트롤러와 네비게이션 컨트롤러가 갖는 자식이 공동으로 관리합니다. 사용자가 버튼 혹은 자식 뷰 컨트롤러의 테이블 행과 상호작용할 때, 자식은 새로운 뷰 컨트롤러를 뷰에 나타내도록 할지를 네비게이션 컨트롤러에게 묻습니다. 자식은 새로운 뷰 컨트롤러의 컨텐츠에 대한 설정을 처리합니다. 네비게이션 컨트롤러는 애니메이션 전환을 관리합니다. 또한, 네비게이션 컨트롤러는 상단의 뷰 컨트롤러를 해제하기 위한 돌아가기 버튼을 보여주는 네비게이션 바를 관리합니다.

Figure 5-1은 네비게이션 컨트롤러와 네비게이션 컨트롤러가 갖는 뷰의 구조를 보여줍니다. 대부분의 컨텐트 영역은 상단 자식 뷰 컨트롤러에 의해 채워집니다. 그리고 네비게이션 바는 작은 부분만 차지합니다.

Figure 5-1 Structure of a navigation interface

컴팩트와 레귤러 환경 모두에서 네비게이션 컨트롤러는 한 번에 하나의 자식 뷰 컨트롤러만 표시합니다. 네비게이션 컨트롤러는 사용 가능한 영역을 채우기 위해 네비게이션 컨트롤러가 갖는 자식의 크기를 변화시킵니다.

Example: Split View Controller

UISplitViewController 객체는 마스터-디테일 정렬 안에서 두 뷰 컨트롤러의 컨텐트를 보여줍니다. 이 정렬 안에서 하나의 뷰 컨트롤러(마스터)가 갖는 컨텐트는 다른 뷰 컨트롤러에 의해 어떤 디테일이 보여져야 하는지를 결정합니다. 두 뷰 컨트롤러의 시각화는 설정이 가능합니다. 그러나 현재 환경 하에 이뤄져야 합니다. 수평 환경에서 스플릿 뷰 컨트롤러는 자식 뷰 컨트롤러들을 양옆으로 보여줄 수 있습니다. 혹은 마스터를 숨겼다가 필요할 때 다시 보여질 수 있도록 할 수도 있습니다. 컴팩트 환경에서 스플릿 뷰 컨트롤러는 한 번에 하나의 뷰 컨트롤러만 보여줍니다.

Figure 5-2는 스플릿 뷰 컨트롤러의 구조를 보여줍니다. 그리고 수평 환경의 뷰를 보여줍니다. 스플릿 뷰 컨트롤러 자체는 기본값으로 컨테이너 뷰만 갖습니다. 이 예시에서 두 자식 뷰는 양옆으로 보여지고 있습니다. 마스터 뷰의 시각화와 마찬가지로 자식 뷰의 크기는 설정이 가능합니다.

Figure 5-2 A split view interface

Configuring a Container in Interface Builder

디자인 타임에 부모-자식 관계를 생성하려면 Figure 5-3에 보이는 것처럼 스토리보드 씬에 컨테이너 뷰 객체를 추가하시기 바랍니다. 컨테이너 뷰 객체는 자식 뷰 컨트롤러의 컨텐츠를 나타내는 플레이스홀더 객체입니다. 자식의 루트 뷰를 컨테이너에 있는 다른 뷰와 연관되도록 크기를 조정하고 위치시키기 위해 해당 뷰를 사용하시기 바랍니다.

Figure 5-3 Adding a container view in Interface Builder

하나 혹은 하나 이상의 컨테이너 뷰와 함께 뷰 컨트롤러를 로드할 때 인터페이스 빌더는 뷰들과 관련이 있는 자식 뷰 컨트롤러를 함께 로드합니다. 자식은 적합한 부모-자식 관계가 생성될 수 있도록 부모와 동시에 인스턴스화 되어야 합니다.

부모-자식 컨테이너 관계를 인터페이스를 사용하지 않고 설정하려면, Adding a Child View Controller to Your Content에 나와있는 것처럼 각각의 자식을 컨테이너 뷰 컨트롤러에 추가하는 것을 통해 코드 작성으로 관계를 생성해야 합니다.

Adding a Child View Controller to Your Content는 아래 부분에 나옵니다.

Implementing a Custom Container View Controller

컨테이너 뷰 컨트롤러를 구현하려면 뷰 컨트롤러와 자식 뷰 컨트롤러 사이의 관계를 형성해줘야 합니다. 이와 같은 부모-자식 관계를 형성하는 것은 모든 자식 뷰 컨트롤러의 뷰를 관리하기 전에 요구됩니다. 그러게 하는 것은 UIKit이 뷰 컨트롤러가 자식의 크기와 위치를 관리하고 있다는 것을 알 수 있도록 해줍니다. 인터페이스 빌더에서 이 관계를 생성할 수도 있고 코드 작성을 통해 생성할 수도 있습니다. 코드로 부모-자식 관계를 생성할 때, 뷰 컨트롤러 셋업의 부분으로써 자식 뷰 컨트롤러의 추가와 삭제를 명시적으로 작성해야 합니다.

Adding a Child View Controller to Your Content

코드 작성을 통해 자식 뷰 컨트롤러를 컨텐트에 통합하려면, 아래에 보이는 것처럼 관련이 있는 뷰 컨트롤러 사이의 관계를 부모-자식으로 생성해야 합니다.

  • 컨테이너 뷰 컨트롤러의 addChildViewController: 메소드를 호출합니다.
  • 이 메소드는 UIKit에게 컨테이너 뷰 컨트롤러가 지금부터 자식 뷰 컨트롤러의 뷰를 관리하고 있음을 알려줍니다.
  • 컨테이너의 뷰 계층구조에 자식의 루트 뷰를 추가합니다.
  • 이 과정에서 자식의 프레임에 대한 크기와 위치를 설정해야 함을 놓치지 않아야 합니다.
  • 자식의 루트 뷰에 대한 크기와 위치를 관리할 수 있는 모든 제약을 추가해야 합니다.
  • 자식 뷰 컨트롤러의 didMoveToParentViewController: 메소드를 호출합니다.

Listing 5-1은 어떻게 컨테이너가 컨테이너 속에 자식 뷰 컨트롤러를 끼워넣는지 보여줍니다. 부모-자식 관계를 형성한 후 컨테이너는 자식의 프레임을 설정하고, 자식의 뷰를 컨테이너의 뷰 계층구조에 추가합니다. 자식의 뷰에 대한 프레임 사이즈를 설정하는 것은 중요합니다. 그리고 이를 통해 컨테이너 안에서 정확한 위치에 나타나는 것을 보장해줍니다. 뷰를 추가한 후 컨테이너는 자식의 didMoveToParentViewController: 메소드를 호출해 뷰 소유권에 대한 변경을 반응할 수 있는 기회를 자식 뷰 컨트롤러에게 줍니다.

Listing 5-1 Adding a child view controller to a container

- (void) displayContentController: (UIViewController*) content {
   [self addChildViewController:content];
   content.view.frame = [self frameForContentController];
   [self.view addSubview:self.currentClientView];
   [content didMoveToParentViewController:self];
}

앞의 예시에서 자식의 메소드인 didMoveToParentViewController: 메소드만을 호출한 것을 기억해야 합니다. addChildViewController: 메소드는 자식의 willMoveToParentViewController: 메소드를 호출하기 때문입니다. didMoveToParentViewController: 메소드를 호출하는 이유는 그 메소드가 자식의 뷰를 컨테이너의 뷰 계층구조에 끼워넣는 이후까지 호출될 수 없기 때문입니다.

오토 레이아웃을 사용할 때, 자식을 컨테이너의 뷰 계층구조에 추가한 후에 컨테이너와 자식 사이의 제약을 설정해야 합니다.

Removing a Child View Controller

컨텐트로부터 자식 뷰 컨트롤러를 제거하려면 뷰 컨트롤러 사이에서 부모-자식 관계를 제거해야 합니다. 아래처럼 진행합니다.

  1. 자식의 willMoveToParentViewController: 메소드를 nil 값과 함께 호출합니다.
  2. 자식의 루트뷰에 설정한 모든 제약을 제거합니다.
  3. 컨테이너의 뷰 계층구조로부터 자식의 루트 뷰를 제거합니다.
  4. 부모-자식 관계의 끝을 마무리하기 위해 자식의 removeFromParentViewController 메소드를 호출합니다.

자식 뷰 컨트롤러를 영구적으로 제거하는 것은 부모와 자식 사이의 관계를 끝냅니다. 더 이상 참조할 필요가 없을 때에만 자식 뷰 컨트롤러를 제거하시기 바랍니다. 예를 들어 네비게이션 컨트롤러는 네비게이션 스택에 새로운 것이 등장할 때 갖고 있는 현재 자식 뷰 컨트롤러를 제거하지 않습니다. 단지 스택에서 꺼낼 때에만 제거합니다.

Listing 5-2는 컨테이너로부터 자식 뷰 컨트롤러를 제거하는 방법을 보여줍니다. willMoveToParentViewController 메소드를 nil 값과 함께 호출하는 것은 자식 뷰 컨트롤러에게 변경을 준비할 수 있는 기회를 제공합니다. removeFromParentViewController 메소드는 또한 nil 값을 전달하면서 자식의 didMoveToParentViewController: 메소드를 호출합니다. 부모 뷰 컨트롤러에 nil 값을 설정하는 것은 컨테이너로부터 자식의 뷰에 대한 제거를 마무리 짓습니다.

Listing 5-2 Removing a child view controller from a container

- (void) hideContentController: (UIViewController*) content {
   [content willMoveToParentViewController:nil];
   [content.view removeFromSuperview];
   [content removeFromParentViewController];
}

Transitioning Between Child View Controllers

하나의 자식 뷰 컨트롤러를 다른 자식 뷰 컨트롤러로 대체하는 애니메이션을 원할 떄, 전환 애니메이션 프로세스에 자식 뷰 컨트롤러의 추가와 삭제를 통합해야 합니다. 애니메이션 전에 자식 뷰 컨트롤러들이 컨텐트의 일부분인 것을 확실히 하고, 현재 자식이 나가게 되어야 한다는 것을 알 수 있도록 해야 합니다. 애니메이션 동안 새로운 자식의 뷰를 위치에 이동시키고 기존 자식의 뷰를 제거해야 합니다. 애니메이션이 끝나는 시점에 자식 뷰 컨트롤러의 제거를 완료해야 합니다.

Listing 5-3은 전환 애니메이션을 사용해 하나의 자식 뷰 컨트롤러가 다른 자식 뷰 컨트롤러로 전환되는 방법의 예시를 보여줍니다. 이 예시에서 새로운 뷰 컨트롤러는 기존 뷰 컨트롤러가 차지하고 있었던 부분에 사각형으로 나타나게 됩니다. 애니메이션이 끝난 후 컴플리션 블록은 컨테이너로부터 자식 뷰 컨트롤러를 제거합니다. 이 예시에서 transitionFromViewController:toViewController:duration:options:animations:completion: 메소드는 자동으로 컨테이너의 뷰 계층구조를 업데이트하기 때문에 뷰를 추가하거나 제거해주지 않아도 됩니다.

Listing 5-3 Transitioning between two child view controllers

- (void)cycleFromViewController: (UIViewController*) oldVC
               toViewController: (UIViewController*) newVC {
   // Prepare the two view controllers for the change.
   [oldVC willMoveToParentViewController:nil];
   [self addChildViewController:newVC];
 
   // Get the start frame of the new view controller and the end frame
   // for the old view controller. Both rectangles are offscreen.
   newVC.view.frame = [self newViewStartFrame];
   CGRect endFrame = [self oldViewEndFrame];
 
   // Queue up the transition animation.
   [self transitionFromViewController: oldVC toViewController: newVC
        duration: 0.25 options:0
        animations:^{
            // Animate the views to their final positions.
            newVC.view.frame = oldVC.view.frame;
            oldVC.view.frame = endFrame;
        }
        completion:^(BOOL finished) {
           // Remove the old view controller and send the final
           // notification to the new view controller.
           [oldVC removeFromParentViewController];
           [newVC didMoveToParentViewController:self];
        }];
}

Managing Appearance Updates for Children

컨테이너에 자식을 추가한 후 컨테이너는 자동으로 appearance-related 메시지를 자식에게 전달합니다. 이것이 원하는 정상적인 작동입니다. 모든 이벤트가 적절하게 전달되는 것을 보장하기 때문입니다. 그러나 가끔 기본값의 작동이 원하는 순서대로 이벤트를 전달하지 않을 수 있습니다. 예를 들어 여러 자식이 동시에 뷰 상태를 변경하는 중이라면, appearance 콜백이 논리적인 순서로 동시에 발생하기 위해 변경을 통합하길 원할 수 있습니다.

appearnce 콜백에 대한 책임을 넘겨받으려면, 컨테이너 뷰 컨트롤러에서 shouldAutomaticallyForwardAppearanceMethods 메소드를 오버라이드하고 NO 값을 반환해야 합니다. Listing 5-4에서 보이는 것처럼 합니다. NO를 반환하는 것은 UIKit이 컨테이너 뷰 컨트롤러가 자식의 apperance에 대한 변경을 알 수 있도록 해줍니다.

Listing 5-4 Disabling automatic appearance forwarding
- (BOOL) shouldAutomaticallyForwardAppearanceMethods {
    return NO;
}

appearance의 전환이 발생할 때 자식의 beginAppearanceTransition:animated: 메소드 혹은 endAppearanceTransition 메소드를 호출해야 합니다. 예를 들어 컨테이너가 하나의 자식 속성에 의해 참조되는 하나의 자식을 갖고 있다면, 컨테이너는 자식에게 Listing 5-5에 보이는 메시지를 전달합니다.

Listing 5-5 Forwarding appearance messages when the container appears or disappears

-(void) viewWillAppear:(BOOL)animated {
    [self.child beginAppearanceTransition: YES animated: animated];
}
 
-(void) viewDidAppear:(BOOL)animated {
    [self.child endAppearanceTransition];
}
 
-(void) viewWillDisappear:(BOOL)animated {
    [self.child beginAppearanceTransition: NO animated: animated];
}
 
-(void) viewDidDisappear:(BOOL)animated {
    [self.child endAppearanceTransition];
}

Suggestions for Building a Container View Controller

새로운 컨테이너 뷰 컨트롤러에 대한 디자인, 개발, 테스트는 시간이 걸립니다. 개별적인 작동을 그렇게 하는 것은 단순할지라도 전체 컨트롤러에 대해서는 꽤 복잡합니다. 컨테이너 클래스 구현 시 아래 팁들을 고려하시기 바랍니다.

  • 자식 뷰 컨트롤러의 루트 뷰에만 접근하시기 바랍니다. 컨테이너는 각각의 자식이 갖는 루트 뷰에만 접근해야 합니다. 즉 자식의 뷰 속성에 의해 반환되는 뷰를 말합니다. 자식의 다른 뷰에 대한 접근이 이뤄지면 안 됩니다.

  • 자식 뷰 컨트롤러는 컨테이너에 대한 최소한의 정보만 갖고 있어야 합니다. 자식 뷰 컨트롤러는 스스로 갖고 있는 컨텐트에만 초점을 맞추고 있어야 합니다. 만약 컨테이너가 자식에 의해 영향을 받을 수 있는 동작을 허용한 경우 상호작용을 관리하기 위해 딜리게이션 디자인 패턴을 사용해야만 합니다.

  • 우선 레귤러 뷰를 사용해 컨테이너를 디자인하시기 바랍니다. 레귤러 뷰를 사용하는 것(자식 뷰 컨트롤러의 뷰 대신)은 단순한 환경에서 레이아웃 제약과 애니메이션 전환을 테스트할 수 있는 기회를 제공합니다. 레귤러 뷰가 기대한 것처럼 동작할 때, 자식 뷰 컨트롤러의 뷰로 교체하시기 바랍니다.

Delegating Control to a Child View Controller

컨테이너 뷰 컨트롤러는 가지고 있는 하나 혹은 하나 이상의 appearance를 자식에게 딜리게이트로 넘겨줄 수 있습니다. 아래와 같은 방법으로 딜리게이트 컨트롤를 할 수 있습니다.

  • 자식 뷰 컨트롤러가 상태바 스타일을 결정할 수 있도록 합니다. 상태바의 appearance를 자식에게 딜리게이트하려면, 컨테이너 뷰 컨트롤러에서 childViewControllerForStatusBarStylechildViewControllerForStatusBarHidden 메소드 중 하나 이상을 오버라이드 하시기 바랍니다.

  • 자식이 선호하는 크기를 구체화하도록 합니다. 유연한 레이아웃을 갖는 컨테이너는 자식의 preferredContentSize 속성을 사용할 수 있습니다. 이는 자식의 크기를 결정하는 것을 돕습니다.

Supporting Accessibility

접근 가능한 앱은 기능성과 유용성을 유지하면서 도움이 되는 도구로써 모든 사람이 사용할 수 있는 앱입니다. 접근 가능한 상태가 되려면 iOS 앱은 VoiceOver에 UI 요소에 대한 정보를 제공해야 합니다. 그렇게 함으로써 시각에 문제가 있는 사용자가 해당 요소와 상호작용할 수 있습니다. UIKit 객체는 기본값으로 접근 가능한 상태입니다. 동시에 접근성을 전달할 수 있는 뷰 컨트롤러의 관점에서의 무엇인가가 존재합니다. 아래 내용을 포함합니다.

  • 컨트롤과 레이블 같은 정적 요소를 포함해 인터페이스에 있는 모든 사용자 요소가 접근 가능함을 보장해야 합니다.
  • 접근 가능한 요소가 정확하고 도움이 되는 접오를 전달할 수 있도록 보장해야 합니다.

앱에서 VoiceOVer 사용자의 경험을 강화할 수 있습니다. 이는 VoiceOver 포커스 울림을 코드로 설정하는 것, 특정 VoiceOver 제스쳐에 반응하도록 하는 것, 접근성 노티피케이션을 탐색하는 것으로 가능합니다.

Moving the VoiceOver Cursor to a Specific Element

앱이 스크린에 새로운 뷰를 제공할 때 VoiceOver 커서의 위치 설정을 고려하시기 바랍니다. 스크린 변와에 따라 레이아웃이 변할 때 VoiceOver 커서라고도 알려져 있는 VoiceOver 포커스가 울립니다. 그리고 스크린에 표시되고 있었던 첫 번째 요소의 위치가 왼쪽에서 오른쪽으로, 위에서 아래로 재설정됩니다. 더 적합한 요소에 커서를 올려두는 것은 인터페이스에서 사용자의 네비게이션 속도를 빠르게 합니다. 예를 들어 네비게이션 컨트롤러의 스택에 새로운 뷰 컨트롤러를 보내면, VoiceOver 커서는 네비게이션 바의 돌아가기 버튼이 놓여집니다. 네비게이션 바의 헤딩에 해당 커서를 움직이길 원할 것이며, 혹은 새롭게 보여지는 페이지의 요소 위에 움직이길 원할 것입니다.

커서의 위치를 변경시키려면 UIAccessibilityPostNotification 함수를 사용해 UIAccessibilityScreenChangedNotification 노티피케이션을 포스트해야 합니다. 노티피케이션은 VoiceOver에게 스크린의 컨텐츠가 변경되었음을 알려줄 것입니다. 노티피케이션을 포스트할 때 포커스를 할당할 요소를 구체화해야 합니다. 아래에 보이는 Listing 6-1처럼 할 수 있습니다.

Listing 6-1 Posting an accessibility notification can change the first element read aloud

@implementation MyViewController
- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
 
    // The second parameter is the new focus element.
    UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification,
                                    self.myFirstElement);
}
@end

기기 화면 회전에 의한 것을 포함해 레이아웃 변경은 VoiceOver 커서의 위치를 재설정합니다. 뷰 컨트롤러의 레이아웃이 변경될 때, UIAccessibilityLayoutChangedNotification 노티피케이션을 포스트해야 합니다. UIAccessibilityScreenChangedNotification 노티피케이션처럼 VoiceOver에 대한 새로운 첫 번째 요소가 되기를 원하는 객체를 구체화할 수 있습니다.

Responding to Special VoiceOver Gestures

VoiceOver는 앱의 특정 액션을 발생시키기 위한 다섯 가지 특별한 제스쳐가 있습니다.

  • Escape. 모달 대화상자를 해제하거나 네비게이션 계층구조에서 한 단계 이전으로 돌아가기 위한 두 손가락의 Z 모양 제스쳐입니다.
  • Magic Tap. 의도한 행동을 수행하기 위한 두 손가락의 더블탭입니다.
  • Three-Finger Scroll. 컨텐트를 수직 혹은 수평으로 스크롤하기 위한 세 손가락 스와이프입니다.
  • Decrement. 요소의 값을 감소시키기 위한 한 손가락 아래 방향의 스와이프입니다.

뷰와 뷰 컨트롤러에 관련이 있는 작업을 수행하기 위해 이 제스쳐들을 사용하시기 바랍니다. UIKit은 제스쳐에 상응하는 메소드를 구현하기 위해 메소드를 탐색합니다. 리스폰더 체인을 사용해서 메소드를 찾습니다. 그리고 VoiceOver 포커스를 갖는 요소를 시작하면서 이뤄집니다. 적합한 메소드를 구현하는 객체가 없으면 UIKit은 해당 제스쳐에 대한 기본값 시스템 액션을 수행합니다. 예를 들어 매직탭 제스쳐는 앱 딜리게이트에서 현재 뷰로부터 매직탭 구현이 발견되지 않는 경우 음악 앱으로부터 음악을 재생하거나 일시정지합니다.

핸들러에서 원하는 액션을 취하고 있을지라도 VoiceOver 사용자는 아래와 같은 가이드라인을 따르는 앱의 동작을 기대합니다. 모든 제스쳐처럼 VoiceOver 제스쳐의 구현은 접근 가능한 앱 상호작용이 직관적일 수 있도록 패턴을 따라야 합니다.

NOTE
모든 VoiceOver 제스쳐 메소드는 리스폰더 체인을 통해 전파할지 여부에 대해 결정하는 불리언 값을 반환합니다. 전파를 중지하려면 YES를 반환해야 하고 반대의 경우 NO를 반환해야 합니다.

Escape

이스케이프 제스쳐를 처리하려면 accessibilityPerformEscape 메소드를 사용하시기 바랍니다. 모달 대화상자 혹은 경고와 같은 컨텐트를 오버레이하는 뷰에 대해서는 메소드를 사용해 오버레이를 해제하시기 바랍니다. 이스케이프 제스쳐의 함수는 컴퓨터 키보드에서 Esc 키와 같은 기능입니다. 일시적인 대화상자 혹은 주요 컨텐트를 드러내는 시트를 취소시킵니다. 커스텀 네비게이션 계층구조에서 한 단계 이전으로 돌아가기 위해 이스케이프 제스쳐를 사용하기도 합니다. 이 제스쳐 처리가 이미 구현되어 있는 UINavigationController 객체를 사용하고 있다면, 이 제스쳐를 구현할 필요는 없습니다.

Magic Tap

매직탭 제스쳐를 처리하려면 accessibilityPerformMagicTap 메소드를 처리해야 합니다. 매직탭 제스쳐는 자주 사용되거나 대부분의 의도한 동작을 빠르게 수행합니다. 예를 들어 폰 앱에서 매직탭은 전화 통화를 픽업하거나 끊고, 시계 앱에서 매직탭은 스톱워치를 시작하거나 중지합니다. 이 제스쳐는 VoiceOver 커서가 강조하고 있는 요소와 필수적으로 관련이 있지는 않은 동작을 발생시키기 위해 사용하려고 할 것입니다. 앱의 모든 곳에 대해 매직탭 제스쳐를 처리하려면, 앱 딜리게이트에서 accessibilityPerformMagicTap 메소드를 구현하시기 바랍니다.

Three-Finger Scroll

VoiceOver 사용자가 세 손가락 스크롤 제스쳐를 수행할 때를 위해 accessibilityScroll: 메소드를 구현해 커스텀 뷰가 갖는 컨텐트가 스크롤될 수 있도록 만들 수 있습니다. 책의 페이지를 보여주는 커스텀 뷰는 페이지를 전환하기 위해 이 제스쳐를 사용하게 될 것입니다. 메소드에 넘겨지는 파라미터는 스크롤 방향을 나타냅니다.

Increment and Decrement

요소의 값을 증가시키거나 감소시키기 위해 accessibilityIncrementaccessibilityDecrement 메소드를 사용하시기 바랍니다. UIAccessibilityTraitAdjustable 특성을 갖는 요소들은 이 메소드를 구현해야 합니다.

Observing Accessibility Notifications

UIKit은 관련이 있는 이벤트에 대한 정보를 앱에 알려줄 수 있도록 접근성 노티피케이션을 보냅니다. 앱의 객체들은 모든 관련 노티피케이션을 탐색할 수 있고 적합한 작업을 수행하기 위해 이 노티피케이션을 사용하기도 합니다. 예를 들어 iBoos 앱은 페이지를 전환하기 위해 UIAccessibilityAnnouncementDidFinishNotification 노티피케이션을 사용합니다. 그리고 VoiceOver가 페이지의 마지막 줄을 읽어주는 것을 마무리할 때 읽는 것을 계속하도록 하기도 합니다. 이 작동은 매끄럽고 방해받지 않는 읽기 경험을 제공합니다.

접근성 노티피케이션에 대한 옵저버로써 등록하기 위해 기본값 노티피케이션 센터를 사용하시기 바랍니다. Listing 6-2는 사용자에 의해 읽는 것이 생공했거나 방해받았다는 것을 기록하는 뷰의 예시를 보여주고 있습니다.

Listing 6-2 Registering as an observer for accessibility notifications

@implementation MyViewController
- (void)viewDidLoad
{
    [super viewDidLoad];
 
    [[NSNotificationCenter defaultCenter]
        addObserver:self
           selector:@selector(didFinishAnnouncement:)
               name:UIAccessibilityAnnouncementDidFinishNotification
             object:nil];
}
 
- (void)didFinishAnnouncement:(NSNotification *)dict
{
    NSString *valueSpoken = [[dict userInfo] objectForKey:UIAccessibilityAnnouncementKeyStringValue];
    NSString *wasSuccessful = [[dict userInfo] objectForKey:UIAccessibilityAnnouncementKeyWasSuccessful];
    // ...
}
@end

구독을 위한 다른 유용한 노티피케이션은 UIAccessibilityVoiceOverStatusChanged 노티피케이션입니다. 이 노티피케이션은 VoiceOver가 토글이 온 혹은 오프를 감지할 수 있도록 사용할 수 있습니다. 앱이 정지된 상태에서 이 노티피케이션이 발생하면, 앱이 다시 foreground로 바뀔 때 이 노티피케이션을 받습니다.

탐색할 수 있는 접근성 노티피케이션의 리스트는 UIAccessibility Protocol Reference를 살펴보시기 바랍니다.

UIAccessibility Protocol Reference
https://developer.apple.com/documentation/objectivec/nsobject/uiaccessibility

Preserving and Restoring State

뷰 컨트롤러는 상태 보존과 복구 프로세스에 대한 중요한 역할을 수행합니다. 상태 보존은 앱이 중지되기 전까지 설정한 내역을 기록해서 설정이 차후의 앱 launch에서 복구될 수 있도록 합니다. 이전 설정으로 앱으로 돌아오는 것은 사용자의 시간을 절약해주고 더 나은 경험을 제공합니다.

보존과 복구 프로세스는 대부분 자동입니다. 하지만 어느 부분이 보존되어야 하는지에 대해서 iOS에세 알려줘야 합니다. 앱의 뷰 컨트롤러를 보존하는 단계는 아래와 같습니다.

  • (필수) 복구하고자 하는 설정을 갖는 뷰 컨트롤러에 복구 아이덴티파이어를 할당해야 합니다. Tagging View Controllers for Preservation를 살펴보시기 바랍니다.
  • (필수) iOS에게 앱 launch 시 새로운 뷰 컨트롤러 객체를 어떻게 생성하고 위치시킬지에 대해 알려줘야 합니다. Restoring View Controllers at Launch Time을 살펴보시기 바랍니다.
  • (선택) 각가의 뷰 컨트롤러에 대해 뷰 컨트롤러가 기존 설정을 반환하기 위해 필요한 특정 설정 데이터를 저장해야 합니다. Encoding and Decoding Your View Controller’s State를 살펴보시기 바랍니다.

참고를 권하는 세 가지 모두 아래에 등장합니다.

보존과 복구 프로세스의 전반적 내용은 App Programming Guide for iOS를 살펴보시기 바랍니다.

문서에서 App Programming Guide for iOS에 연결하는 링크는 아래와 같습니다.

App Programming Guide for iOS
https://developer.apple.com/documentation/uikit#//apple_ref/doc/uid/TP40007072

Tagging View Controllers for Preservation

UIKit은 오직 보존하겠다고 선언한 뷰 컨트롤러만 보존합니다. 각각의 뷰 컨트롤러는 resotrationIdentifier 속성을 가집니다. 기본값은 nil입니다. 유효한 문자열로 이 속성을 설정하는 것은 UIKit에게 뷰 컨트롤러와 뷰가 보존되어야 함을 알려줍니다. 코드 작성 혹은 스토리보드 파일에 복구 아이덴티파이어를 할당할 수 있습니다.

복구 아이덴티파이어를 할당할 때, 뷰 컨트롤러 계층구조에 있는 모든 부모 뷰 컨트롤러 역시 복구 아이덴티파이어를 가져야 한다는 것을 기억해야 합니다. 보존 프로세스가 진행되는 동안 UIKit은 윈도우의 루트 뷰 컨트롤러에서 시작하고, 뷰 컨트롤러 계층구조를 따라갑나다. 해당 계층구조에 있는 뷰 컨트롤러가 복구 아이덴티파이어를 갖지 않는다면, 해당 뷰 컨트롤러, 그 뷰 컨트롤러가 갖는 모든 자식 뷰 컨트롤러, 나타나고 있는 뷰 컨트롤러는 무시됩니다.

Choosing Effective Restoration Identifiers

UIKit은 이후에 뷰 컨트롤러를 재생성하기 위해 복구 아이덴티파이어 문자열을 사용합니다. 그렇기 때문에 코드에서 쉽게 식별할 수 있는 문자열을 선택해야 합니다. 만약 UIKit이 뷰 컨트롤러를 자동으로 생성할 수 없다면, 뷰 컨트롤러의 복구 아이덴티파이어와 모든 뷰 컨트롤러의 복구 아이덴티파이어를 제공하면서 생성할 것을 요구할 것입니다. 이와 같은 아이덴티파이어의 체인은 뷰 컨트롤러에 대한 복구 경로를 제공합니다. 그리고 아이덴티파이어 체인은 곧 어떤 뷰 컨트롤러가 요청되고 있는지를 어떻게 결정할지와 같은 내용입니다. 복구 경로는 루트 뷰 컨트롤러에서 시작합니다. 그리고 모든 뷰 컨트롤러와 요청되었던 것까지 포함합니다.

복구 아이덴파이어는 보통 뷰 컨틀롤러의 클래스 이름입니다. 많은 곳에서 같은 클래스를 사용한다면, 더 의미있는 값들을 할당하길 원할 것입니다. 예를 들어 뷰 컨트롤러에 의해 관리되고 있는 데이터에 기반해 문자열을 할당하게 될 것입니다.

모든 뷰 컨트롤러에 대한 복구 경로는 고유해야 합니다. 만약 컨테이너 뷰 컨트롤러가 둘의 자식을 갖고 있다면, 둘 모두 고유한 복구 아이덴티파이어를 할당해야 합니다. UIKit에 있는 몇 가지 컨테이너 뷰 컨트롤러는 각각의 자식에 대한 같은 복구 아이덴티파이어를 허용해주면서 자동으로 자식 뷰 컨트롤러를 명확하게 해줍니다. 예를 들어 UINavigationController 클래스는 네비게이션 스택 상에 위치에 기반해 각각의 자식에게 정보를 추가합니다. 주어진 뷰 컨트롤러의 작동에 대한 더 많은 정보는 그에 상응하는 클래스 레퍼런스를 살펴보시기 바랍니다.

뷰 컨트롤러 생성에서 복구 아이덴티파이어와 복구 경로를 사용하는 방법에 대한 내용은 Restoring View Controllers at Launch Time을 살펴보시기 바랍니다.

Restoring View Controllers at Launch Time은 아래 부분에 나옵니다.

Excluding Groups of View Controllers

복구 과정에서 뷰 컨트롤러 그룹 전체를 배제하려면 부모 뷰 컨트롤러의 복구 아이덴티파이어를 nil로 설정해야 합니다. Figure 7-1은 복구 아이덴티파이어를 nil로 설정한 경우 뷰 컨트롤러 계층구조에 미치는 영향을 보여주고 있습니다. 보존 데이터가 부족하면 뷰 컨트롤러는 나중에 복구되지 않습니다.

Figure 7-1 Excluding view controllers from the automatic preservation process

하나 혹은 하나 이상의 뷰 컨트롤러를 배제하는 것은 이후의 복구가 이뤄지는 동안 제거되는 것인 아닙니다. launch타임에 앱 기본값 설정의 부분인 모든 뷰 컨트롤러는 생성됩니다. Figure 7-2가 이를 보여주고 있습니다. 이와 같은 뷰 컨트롤러들은 기본값 설정으로 재생성됩니다.

Figure 7-2 Loading the default set of view controllers

자동 보존 프로세스로부터 뷰 컨트롤러를 배제하는 것이 수동으로 보존하는 것을 막지는 않습니다. 복구 아카이브에서 뷰 컨트롤러에 레퍼런스를 저장하는 것은 뷰 컨트롤러를 보존하고 뷰 컨트롤러의 상태 정보 또한 보존합니다. 예를 들어 만약 Figure 7-1에 있는 앱 딜리게이트가 네비게이션 컨트롤러의 세 자식을 저장했다면, 자식들의 상태는 보존덜 굇업니다. 복구가 이뤄지는 동안 앱 딜리게이트는 해당 뷰 컨트롤러들을 재생성할 수 있고 네비게이션 컨트롤러의 스택에 넣을 것입니다.

Preserving a View Controller’s Views

몇 가지 뷰들은 뷰와 관련이 있는 추가적인 상태 정보를 갖습니다. 하지만 부모 뷰 컨트롤러에 대한 상태 정보를 갖지는 않습니다. 예를 들어 스크롤뷰는 보존하기를 의도했던 스크롤 포지션을 갖고 있을 것입니다. 뷰 컨트롤러가 스크롤뷰의 컨텐트를 제공하는 데 책임이 있는 동안 스크롤뷰는 시각적 상태를 보존하는 책임을 갖습니다.

뷰의 상태를 저장하려면 아래처럼 해야 합니다.

  • 뷰의 restorationIdentifier 속성에 유효한 문자열을 할당해야 합니다.
  • 뷰 컨트롤러 역시 유효한 복구 아이덴티파이어를 가져야 합니다.
  • 테이블뷰와 컬렉션뷰의 경우 UIDataSourceModelAssociation 프로토콜을 채택한 데이터 소스를 할당해야 합니다.

뷰에 복구 아이덴티파이어를 할당하는 것은 UIKit에게 보존 아카이브에 뷰의 상태 내용을 써야한다고 알려줍니다. 뷰 컨트롤러가 이후 복구될 때, UIKit은 복구 아이덴티파이어를 가지고 있었던 모든 뷰의 상태를 복구할 것입니다.

Restoring View Controllers at Launch Time

launch 타임에 UIKit은 앱의 이전 상태로 복구하는 것을 시도할 것입니다. 그때 UIKit은 보존된 UI를 구성하는 뷰 컨트롤러 객체의 생성 혹은 위치시키는 것을 앱에 요청합니다. UIKit은 뷰 컨트롤러를 위치시키고자 시도할 때 아래와 같은 순서로 탐색합니다.

  • 만약 뷰 컨트롤러가 복구 클래스를 갖고 있었다면, UIKit은 뷰 컨트롤러 제공을 그 클래스에 요청합니다. UIKit은 뷰 컨트롤러를 회수하기 위해서 관련이 있는 복구 클래스의 viewControllerWithRestorationIdentifierPath:coder: 메소드를 호출합니다. 만약 그 메소드가 nil을 반환하면, 앱이 뷰 컨트롤러를 재생성하지 않기를 바라는 것으로 가정하고 UIKit은 탐색을 멈춥니다.

  • 만약 뷰 컨트롤러가 복구 클래스를 갖고 있지 않았다면, UIKit은 뷰 컨트롤러 제공을 위해 앱 딜리게이트에 요청할 것입니다. UIKit은 복구 클래스가 없는 뷰 컨트롤러를 탐색하기 위해 앱 딜리게이트의 application:viewControllerWithRestorationIdentifierPath:coder: 메소드를 호출합니다. 만약 이 메소드가 nil을 반환하면, UIKit은 뷰 컨트롤러를 암묵적으로 찾기를 시도합니다.

  • 만약 정확한 복구 경로를 갖는 뷰 컨트롤러가 이미 존재한다면, UIKit은 그 객체를 사용합니다. 앱이 launch 타임(코드로 작성하거나 스토리보드로부터 로딩하는 경우)에 뷰 컨트롤러를 생상하고 뷰 컨트롤러가 복구 아이덴티파이어를 갖고 있다면, UIKit은 복구 경로에 기반해 뷰 컨트롤러를 찾습니다.

  • 만약 뷰 컨트롤러가 원래부터 스토리보드 파일로부터 로드된 것이라면, UIKit은 뷰 컨트롤러를 위치시키고 생성하는 것에 있어 스토리보드에 저장된 정보를 사용합니다. UIKit은 복구 아카이브 내부에 뷰 컨트롤러의 스토리보드에 대한 정보를 저장합니다. 복구 시점에 다른 수단에서 뷰 컨트롤러가 발견되지 않았을 경우 UIKit은 같은 스토리보드에 위치시키기 위해 그 정보를 사용할 것이고, 상응하는 뷰 컨트롤러를 인스턴스화 합니다.

뷰 컨트롤러에 복구 클래스를 할당하는 것은 UIKit이 해당 뷰 컨트롤러를 암묵적으로 찾는 것을 방지할 수 있습니다. 복구 클래스를 사용하는 것은 뷰 컨트롤러를 생성할지에 대한 여부를 더 잘 컨트롤할 수 있습니다. 예를 들어 viewControllerWithRestorationIdentifierPath:coder: 메소드는 만약 클래스가 뷰 컨트롤러는 재생성되지 않아야 한다고 결정하고 있는 경우 nil 값을 반환합니다. 어떠한 복구 클래스라도 존재하지 않을 경우 UIKit은 뷰 컨트롤러를 찾거나 생성, 복구할 수 있는 모든 작업을 수행할 수 있습니다.

복구 클래스를 사용하는 경우 viewControllerWithRestorationIdentifierPath:coder: 메소드는 클래스의 새로운 인스턴스를 생성해야 하고, 최소한의 초기화를 수행하며, 결과물이 되는 객체를 반환합니다. Listing 7-1은 스토리보드로부터 뷰 컨트롤러를 로드하기 위한 메소드 사용의 예시입니다. 뷰 컨트롤러가 원래부터 스토리보드로에서 로드되었기 때문에 이 메소드는 UIStateRestorationViewControllerStoryboardKey 키를 사용해 아카이브로부터 스토리보드를 가져옵니다. 이 메소드는 뷰 컨트롤러의 데이터 필드를 설정하려고 하지는 않습니다. 그에 대한 단계는 뷰 컨트롤러의 상태가 디코딩되었을 때 발생합니다.

Listing 7-1 Creating a new view controller during restoration

+ (UIViewController*) viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents
                      coder:(NSCoder *)coder {
   MyViewController* vc;
   UIStoryboard* sb = [coder decodeObjectForKey:UIStateRestorationViewControllerStoryboardKey];
   if (sb) {
      vc = (PushViewController*)[sb instantiateViewControllerWithIdentifier:@"MyViewController"];
      vc.restorationIdentifier = [identifierComponents lastObject];
      vc.restorationClass = [MyViewController class];
   }
    return vc;
}

복구 아이덴티파이어와 복구 클래스를 재할당하는 것은 뷰 컨트롤러를 직접 재생성할 때에 있어 좋은 습관입니다. 복구 아이덴티파이어를 복구할 수 있는 가장 쉬운 방법은 identifierComponents 배열에 있는 마지막 아이템을 쥐고 뷰 컨트롤러에 할당하는 것입니다.

launch 타임에 앱의 메인 스토리보드로부터 생성되었던 객체의 경우 각 객체에 대해 새로운 인스턴스를 생성하지 않아야 합니다. UIKit이 암묵적으로 해당 객체들을 찾도록 두거나 앱 딜리게이트에서 application:viewControllerWithRestorationIdentifierPath:coder: 메소드를 사용해 존재하는 객체를 찾을 수 있도록 해야 합니다.

Encoding and Decoding Your View Controller’s State

보존이 예정된 각각의 객체들의 경우 UIKit은 객체들이 자신의 상태를 저장할 수 있는 기회를 마련할 수 있도록 객체의 encodeRestorableStateWithCoder: 메소드를 호출합니다. 복구가 처리되는 동안 UIKit은 해당 상태를 디코딩하고 객체 적용하기 위해 일치하는 decodeRestorableStateWithCoder: 메소드를 호출합니다. 이 메소드의 구현은 선택적입니다. 하지만 뷰 컨트롤러를 위해 권장되는 사항입니다. 아래 정보의 유형을 저장하거나 복구하기 위해 사용하게 될 것입니다.

  • 표시되어야 하는 모든 데이터에 대한 참조(데이터 자체가 아닙니다)입니다.
  • 컨테이너 뷰 컨트롤러에 있어 자식 뷰 컨트롤러에 대한 참조입니다.
  • 현재 선택에 대한 정보입니다.
  • 사용자가 설정 가능한 뷰를 갖는 뷰 컨트롤러의 경우 해당 뷰에 대한 현재 설정을 갖는 정보입니다.

인코딩과 디코딩 메소드에서 코더에서 지원하는 객체와 모든 데이터 타입을 인코딩할 수 있습니다. 뷰와 뷰 컨트롤러를 제외한 모든 객체에 있어 객체는 NSCoding 프로토콜을 채택해야만 하며, 상태를 쓰기(write) 위해 NSCoding 프로토콜의 메소드를 사용해야 합니다. 뷰와 뷰 컨트롤러의 경우 코더는 객체의 상태를 저장하기 위해 NSCoding 프로토콜을 사용하지 않습니다. 대신 코더는 객체의 복구 아이덴티파이어를 저장하고, 해당 객체의 encodeRestorableStateWithCoder: 메소드 호출을 발생시키는 보존 가능한 객체의 리스트에 복구 아이덴티파이어를 추가합니다.

뷰 컨트롤러의 encodeRestorableStateWithCoder: 메소드와 decodeRestorableStateWithCoder: 메소드는 구현에 있어 어딘가의 시점에 super를 호출해야 합니다. super를 호출하는 것은 부모 클래스가 모든 추가적인 정보를 저장하고 복구할 수 있도록 해줍니다. Listing 7-2는 특정 뷰 컨트롤러를 확인하기 위해 사용되는 숫자값을 저장하는 메소드의 구현에 대한 샘플입니다.

Listing 7-2 Encoding and decoding a view controller’s state.

- (void)encodeRestorableStateWithCoder:(NSCoder *)coder {
   [super encodeRestorableStateWithCoder:coder];
 
   [coder encodeInt:self.number forKey:MyViewControllerNumber];
}
 
- (void)decodeRestorableStateWithCoder:(NSCoder *)coder {
   [super decodeRestorableStateWithCoder:coder];
 
   self.number = [coder decodeIntForKey:MyViewControllerNumber];
}

코더 객체는 인코딩과 디코딩 프로세스가 이뤄지는 동안 공유되지 않습니다. 보존 가능한 상태를 갖는 각각의 객체는 고유한 코더 객체를 받습니다. 고유한 코더의 사용은 키에 있어 이름공간의 충돌을 걱정하지 않아도 된다는 것을 의미합니다. 그러나 키 이름에서 UIApplicationStateRestorationBundleVersionKey, UIApplicationStateRestorationUserInterfaceIdiomKey, UIStateRestorationViewControllerStoryboardKey를 사용해서는 안 됩니다. 이 키들은 뷰 컨트롤러의 상태에 대한 추가적인 정보를 저장하기 위해 UIKit이 사용합니다.

뷰 컨트롤러에 대한 인코딩, 디코딩 메소드의 구현에 대한 더 많은 정보는 UIViewController Class Reference를 살펴보시기 바랍니다.

UIView Controller Class Reference
https://developer.apple.com/documentation/uikit/uiviewcontroller

Tips for Saving and Restoring Your View Controllers

뷰 컨트롤러에 상태 보존과 복구의 지원를 지원하려면 아래 가이드라인을 고려하시기 바랍니다.

  • 모든 뷰 컨트롤러를 보존하지 않아도 된다는 것을 기억해야 합니다. 어떤 경우에는 뷰 컨트롤러를 보존할 이유가 없을 수 있습니다. 예를 들어 앱이 변경사항을 표시하고 있었다면, 작동을 취소하고 이전 스크린으로 앱을 복구할 수 있습니다. 이와 같은 경우 새로운 암호 정보를 요청하는 뷰 컨트롤러를 보존하지 않습니다.
  • 복구 프로세스가 처리되는 동안 뷰 컨트롤러 클래스를 교체하는 것을 피하시기 바랍니다. 상태 보존 시스템은 보존하는 뷰 컨트롤러의 클래스를 인코딩합니다. 복구가 진행되는 동안 앱이 클래스가 기존 객체에 일치(혹은 하위 객체가 아닌) 객체를 반환한다면, 시스템은 어떠한 상태 정보에 대해서도 디코딩하는 것을 뷰 컨트롤러에 요청하지 않습니다. 그러므로 기존 뷰 컨트롤러를 완전히 다른 한 가지로 교체하는 것은 객체의 전체 상태를 복구하지 않습니다.
  • 상태 보존 시스템은 의도한대로 뷰 컨트롤러를 사용하길 기대합니다. 복구 프로세스는 인터페이스를 다시 빌드하기 위해서 뷰 컨트롤러의 포함관계에 의존합니다. 컨테이너 뷰 컨트롤러를 적절하게 사용하지 않는다면, 보존 시스템은 뷰 컨트롤러를 찾을 수 없습니다. 예를 들어 상응하는 뷰 컨트롤러들 사이에 포함관계가 없는 한 뷰 컨트롤러의 뷰를 다른 뷰에 끼워넣지 않아야 합니다.

0개의 댓글