[번역] View Controller Programming Guide for iOS - View Controller Definition - Preserving and Restoring State

원문

미리 요약

  • 앱이 백그라운드에서 포어그라운드로 진입할 때 이전 상태를 복원할 수 있다.
    • Restoration Identifier, Restoration Path 등을 사용한다.
  • 상태 보존을 원하는 어떠한 뷰 컨트롤러에 대하여, 해당 뷰 컨트롤러의 부모 뷰 컨트롤러도 상태 보존을 원하도록 설정되어있어야 한다.
  • UIViewController의 상태 복원 관리 관련 메소드를 사용한다.
    • restorationIdentifier / restorationClass
    • encodeRestorableState(with:) / decodeRestorableState(with:)

뷰 컨트롤러는 상태 보존 및 복원 프로세스에서 중요한 역할을 맡는다. 상태 보존은 앱이 중단되기 전에 앱의 구성을 기록하여 뒤이은 앱의 실행에서 구성이 복원될 수 있도록 한다. 이전 구성으로 앱을 되돌리는 것은 유저의 시간을 절약해주고 더 나은 사용자 경험을 제공한다.

보존 및 복원 프로세스는 대부분 자동으로 이루어지나, 어떤 앱의 부분이 보존될지를 iOS에게 알려야 한다. 앱의 뷰 컨트롤러를 보존하기 위한 단계는 다음과 같다.

  • (필수) 구성을 보존하고 싶은 뷰 컨트롤러에 복원 식별자를 할당하라. Tagging View Controllers for Preservation을 확인하라.
  • (필수) 실행 시점에 새로운 뷰 컨트롤러 객체를 만들거나 위치를 찾는 방법을 iOS에게 알려주어라. Restoring View Controllers at Launch Time을 확인하라.
  • (선택) 각각의 뷰 컨트롤러에 대하여, 뷰 컨트롤러에게 기존의 구성을 되돌려주기 위해 필요한 특정 구성 데이터를 저장하라. Encoding and Decoding Your View Controller’s State를 확인하라.

App Programming Guide for iOS에서 보존 및 복원 프로세스에 대한 개요를 확인하라.

Tagging View Controllers for Preservation

UIKit은 보존하라고 말한 뷰 컨트롤러에 대해서만 보존한다. 각각의 뷰 컨트롤러는 기본적으로 nil 값이 할당된 restorationIdentifier 프로퍼티를 갖는다. 이 프로퍼티를 유효한 문자열로 설정하여 UIKit에게 해당 뷰 컨트롤러와 뷰들이 보존되어야 함을 알릴 수 있다. 코드 또는 스토리보드 파일에서 복원 식별자를 할당할 수 있다

복원 식별자를 할당할 때 뷰 컨트롤러 계층에 있는 모든 부모 뷰 컨트롤러 또한 반드시 복원 식별자를 가져야 함을 기억하라. 보존 프로세스 동안 UIKit은 윈도우의 루트 뷰 컨트롤러에서 시작하여 뷰 컨트롤러 계층을 따라 내려간다. 해당 계층 안에 있는 뷰 컨트롤러가 복원 식별자를 가지고 있지 않다면, 해당 뷰 컨트롤러와 그것의 모든 자식 뷰 컨트롤러, 그리고 프레젠트되는 뷰 컨트롤러들은 무시된다.

Choosing Effective Restoration Identifiers

UIKit은 나중에 뷰 컨트롤러를 다시 만들기 위해 복원 식별자 문자열을 사용하므로, 코드에서 쉽게 식별할 수 있는 문자열을 선택하라. UIKit은 뷰 컨트롤러 중 하나를 자동으로 만들지 못하기 때문에, UIKit은 당신에게 뷰 컨트롤러와 그 모든 부모 뷰 컨트롤러들의 복원 식별자를 제공하면서 그것을 만들도록 요청한다. 이 식별자 체인은 뷰 컨트롤러에 대한 복원 패스restoration path로 표현되며 어떠한 뷰 컨트롤러가 요청될지를 결정하는 방법이다. 복원 패스는 루트 뷰 컨트롤러에서 시작하며, 요구되는 것을 포함하여 모든 뷰 컨트롤러를 포함한다.

복구 식별자는 일반적으로 뷰 컨트롤러의 클래스 이름이기만 하면 된다. 많은 곳에서 같은 클래스를 사용한다면 더 의미 있는 값으로 할당하기 원할 수 있다. 예를 들어 뷰 컨트롤러가 관리하는 데이터를 기반으로 한 문자열을 할당할 수 있을 것이다.

모든 뷰 컨트롤러에 대한 복원 패스는 반드시 유일해야 한다. 컨테이너 뷰 컨트롤러가 두 개의 자식을 가지고 있다면, 컨테이너는 각각의 자식에게 반드시 유일한 복원 식별자를 할당해야 한다. UIKit에 있는 몇몇 컨테이너 뷰 컨트롤러는 자동으로 그 자식 뷰 컨트롤러들의 모호함을 해결하여 각 자식에 대하여 같은 복원 식별자를 사용할 수 있게 해준다. 예를 들어 UINavigationController 클래스는 각 자식에게 내비게이션 스택 내에서의 위치에 기반한 정보를 추가한다. 관련 클래스 레퍼런스에서 주어진 뷰 컨트롤러의 동작에 관한 더 많은 정보를 확인하라.

Restoring View Controllers at Launch Time에서 뷰 컨트롤러를 만들기 위해 복원 식별자와 복원 패스를 사용하는 방법에 대한 더 많은 정보를 확인하라.

Excluding Groups of View Controllers

복원 프로세스로부터 뷰 컨트롤러의 전체 그룹을 배제하기 위해 부모 뷰 컨트롤러의 복원 식별자를 nil로 설정하라. 보존 데이터가 없으면 해당 뷰 컨트롤러는 나중에 복원되지 않는다.

하나 이상의 뷰 컨트롤러를 제외하는 것은 이어지는 복원 동안 그것들을 완전하게 제거하지 않는다. 실행 시점에서 앱의 디폴트 설정의 일부분이 되는 뷰 컨트롤러는 여전히 만들어진다. 그러한 뷰 컨트롤러는 디폴트 구성에 따라 다시 만들어지지만, 여전히 만들어진다.

뷰 컨트롤러를 자동 보존 프로세스에서 제외하는 것은 수동으로 그것을 보존하는 것을 막지 않는다. 복원 아카이브에서 뷰 컨트롤러에 대한 참조를 저장하여 뷰 컨트롤러와 그 상태 정보를 보존할 수 있다. 앱 델리게이트가 내비게이션 컨트롤러의 세 개의 자식들을 저장한다면, 그 상태들은 보존되어야 할 것이다. 복원 동안 앱 델리게이트는 그러한 뷰 컨트롤러들을 다시 만들어 내비게이션 컨트롤러의 스택에 푸시할 수 있다.

Preserving a View Controller’s Views

몇몇 뷰는 부모 뷰 컨트롤러가 아닌 뷰와 관련된 추가적인 상태 정보를 갖는다. 예를 들어 스크롤 뷰는 보존하기 원할 수 있는 스크롤 위치를 갖는다. 뷰 컨트롤러가 스크롤 뷰의 컨텐트를 제공할 책임이 있는 반면, 스크롤 뷰 자체는 그 시각적 상태를 보존할 책임이 있다.

뷰의 상태를 저장하기 위해 다음을 따르라.

  • 뷰의 restorationIdentifier 프로퍼티에 유효한 문자열을 할당하라.
  • 또한 유효한 복원 식별자를 가지고 있는 뷰 컨트롤러로부터 뷰를 사용하라.
  • 테이블 뷰와 컬렉션 뷰에 대하여, UIDataSourceModelAssociation 프로토콜을 채택하는 데이터 소스를 할당하라.

뷰에 복원 식별자를 할당하여 UIKit에게 뷰의 상태를 보존 아카이브에 써야 한다고 말할 수 있다. 뷰 컨트롤러가 나중에 복원될 때 UIKit은 복원 식별자를 갖는 뷰들의 상태도 복원한다.

Restoring View Controllers at Launch Time

실행 시점에 UIKit은 앱을 이전 상태로 복원한다. 이 시점에 UIKit은 앱에게 보존된 유저 인터페이스를 구성하는 뷰 컨트롤러 객체를 만들거나 위치를 찾으라고 요청한다. UIKit은 뷰 컨트롤러의 위치를 찾으려고 할 때 다음의 순서를 따라 찾는다.

  • 뷰 컨트롤러가 복원 클래스를 가지고 있다면, UIKit은 이 클래스에게 뷰 컨트롤러를 제공하라고 요청한다. UIKit은 뷰 컨트롤러를 되찾기 위해 연관된 복원 클래스의 viewController(withRestorationIdentifierPath:coder:) 메소드를 호출한다. 이 메소드가 nil을 반환한다면 앱은 뷰 컨트롤러를 다시 만들기를 원하지 않는다고 가정하여 UIKit은 뷰 컨트롤러의 위치를 찾는 것을 중지한다.
  • 뷰 컨트롤러가 복원 클래스를 가지고 있지 않다면, UIKit은 앱 델리게이트에게 뷰 컨트롤러를 제공하라고 요청한다. UIKit은 앱 델리게이트의 application(_:viewControllerWithRestorationIdentifier:coder:) 메소드를 호출하여 복원 클래스 없이 뷰 컨트롤러를 찾는다. 이 메소드가 nil을 반환한다면 UIKit은 암시적으로 뷰 컨트롤러를 찾으려고 한다.
  • 올바른 복원 패스를 가진 뷰 컨트로럴가 이미 있다면, UIKit은 이 객체를 사용한다. 앱이 실행 시점에 뷰 컨트롤러를 만들고(코드를 통하든, 스토리보드를 통해 로드했든), 그러한 뷰 컨트롤러가 복원 식별자를 가지고 있다면, UIKit은 그것들의 복원 패스에 기반하여 암시적으로 뷰 컨트롤러의 위치를 찾는다.
  • 뷰 컨트롤러가 스토리보드 파일에서 로드되었다면, UIKit은 저장된 스토리보드 정보를 사용하여 뷰 컨트롤러의 위치를 알아내고 만든다. UIKit은 복원 아카이브에 뷰 컨트롤러의 스토리보드에 대한 정보를 저장한다. 복원 시점에 UIKit은 그 정보를 사용하여 같은 스토리보드 파일을 찾고, 뷰 컨트롤러를 찾을 수 있는 다른 방법이 없다면, 부합하는 뷰 컨트롤러의 인스턴스를 만든다.

뷰 컨트롤러에 복원 클래스를 할당하여 UIKit이 암시적으로 뷰 컨트롤러를 검색하는 것을 막을 수 있다. 복원 클래스를 사용하여 정말로 뷰 컨트롤러를 만들고 싶은 것인지에 대한 더 많은 제어를 제공할 수 있다. 예를 들어 viewController(withRestorationIdentifierPath:coder:) 메소드는 클래스가 뷰 컨트롤러는 다시 만들어지면 안된다고 결정했다면 nil을 반환할 수 있다. 복원 클래스가 존재하지 않는다면, UIKit은 뷰 컨트롤러를 찾고 만들어서 복원하기 위해 할 수 있는 모든 것을 한다.

복원 클래스를 사용할 때 viewController(withRestorationIdentifierPath:coder:) 메소드는 클래스의 새로운 인스턴스를 만들고, 최소한의 초기화를 수행하며, 결과 객체를 반환해야 한다. 다음은 스토리보드에서 뷰 컨트롤러를 로드하기 위해 메소드를 사용하는 예시를 보여준다. 뷰 컨트롤러는 스토리보드에서 로드되기 때문에 stateRestorationViewControllerStoryboardKey 프로퍼티를 사용하여 아카이브에서 스토리보드를 얻는다. 이 메소드는 뷰 컨트롤러의 데이터 필드를 구성하려 하지 않는 것에 주목하라. 그 단계는 뷰 컨트롤러의 상태가 나중에 디코드될 때 발생한다.

func viewController(withRestorationIdentifierPath identifierComponents: [String], coder: NSCoder) -> UIViewController? {
let sb = coder.decodeObject(of: UIStoryboard.self, forKey: stateRestorationViewControllerStoryboardKey)!
let vc = sb.instantiateViewController(withIdentifier: "MyViewController")
vc.restorationIdentifier = identifierComponents.last!
vc.restorationClass = MyViewController.self
return vc
}

뷰 컨트롤러를 수동으로 다시 만들 때 복원 식별자와 복원 클래스를 다시 할당하는 것은 좋은 습관이다. 복원 식별자를 복원하는 가장 간단한 방법은 identifierComponents 배열에 있는 마지막 아이템을 잡아 뷰 컨트롤러에 할당하는 것이다.

실행 시점에서 앱의 메인 스토리보드 파일에서 생성된 객체에 대하여, 각 객체의 새로운 인스턴스를 생성하지 마라. UIKit이 암시적으로 그 객체들을 찾도록 하거나, 기존 객체를 찾기 위해 앱 델리게이트의 application(_:viewControllerWithRestorationIdentifierPath:coder:) 메소드를 사용하라.

Encoding and Decoding Your View Controller’s State

보존하기로 예정된 각각의 객체에 대하여 UIKit은 객체의 encodeRestorableState(with:) 메소드를 호출하여 상태를 저장할 기회를 제공한다. 복원 프로세스 동안 UIKit은 상태를 디코드하고 객체에 적용하기 위해 알맞은 decodeRestorableState(with:) 메소드를 호출한다. 이 메소드들의 구현은 선택적이나 추천된다. 다음의 타입에 대한 정보를 저장하고 복원하기 위해 메소드를 사용할 수 있다.

  • 표시될 데이터에 대한 참조 (데이터 자체가 아님)
  • 컨테이너 뷰 컨트롤러에 대하여, 자식 뷰 컨트롤러에 대한 참조
  • 현재 선택에 대한 정보
  • 유저가 구성할 수 있는 뷰를 가진 뷰 컨트롤러에 대하여, 뷰의 현재 구성에 대한 정보

인코드 및 디코드 메소드에서 객체와 코더가 재원하는 데이터 타입을 인코드할 수 있다. 뷰와 뷰 컨트롤러를 제외한 모든 객체에 대하여, 객체는 반드시 NSCoding 프로토콜을 채택해야 하며 상태를 쓰기 위해 프로토콜의 메소드를 사용해야 한다. 뷰와 뷰 컨트롤러에 대하여 코더는 객체의 상태를 저장하기 위해 NSCoding 프로토콜을 사용하지 않는다. 대신 코더는 객체의 복원 식별자를 저장하고 보존 가능한 객체 리스트에 추가하며, 이는 객체의 encodeRestorableState(with:) 메소드가 호출되는 원인이 된다.

뷰 컨트롤러의 encodeRestorableState(with:)decodeRestorableState(with:) 메소드는 구현의 어느 지점에서 반드시 super를 호출해야 한다. super를 호출하여 부모 클래스가 추가 정보를 저장하고 복원할 기회를 제공할 수 있다.

func encodeRestorableState(with coder: NSCoder) {
super.encodeRestorableState(with: coder)
coder.encode(number, forKey: MyViewControllerNumber)
}

func decodeRestorableState(with coder: NSCoder) {
super.decodeRestorableState(with: coder)
number = coder.decodeInteger(forKey: MyViewControllerNumber)
}

코더 객체는 인코드 및 디코드 프로세스 동안 공유되지 않는다. 보존 가능한 상태를 갖는 각각의 객체는 그 자신만의 코더 객체를 받는다. 유일한 코더의 사용은 사용하는 키 간 네임스페이스 충돌을 걱정할 필요가 없다는 것을 의미한다. 그러나 stateRestorationBundleVersionKey, stateRestorationUserInterfaceIdiomKey, stateRestorationViewControllerStoryboardKey 키를 직접 사용하지 마라. 이 키들은 UIKit이 뷰 컨트롤러의 상태에 대한 추가 정보를 저장하기 위해 사용하는 것들이다

UIViewController 클래스 레퍼런스에서 뷰 컨트롤러에 대한 인코드 및 디코드 메소드를 구현하는 더 많은 정보에 대해 확인하라.

Tips for Saving and Restoring Your View Controllers

뷰 컨트롤러에 상태 보존 및 복원 지원을 추가하기 위해 다음의 가이드라인을 준수할 것을 고려하라.

  • 모든 뷰 컨트롤러를 보존하기 원하지 않을 수 있음을 기억하라. 몇몇 경우에는 뷰 컨트롤러를 보존하는 것이 올바르지 않을 수 있다. 예를 들어 앱이 변경을 표시하는 중이었다면, 작업을 취소하고 앱을 이전 화면으로 복원하고 싶을 수 있다. 이러한 경우에 새로운 패스워드 정보를 요청하는 뷰 컨트롤러를 보존하면 안될 것이다.
  • 복원 프로세스 동안 뷰 컨트롤러 클래스를 서로 바꾸지 마라. 상태 보존 시스템은 보존하는 뷰 컨트롤러의 클래스를 인코드한다. 복원 동안 앱은 기존 객체와 클래스가 일치하지 않거나 서브클래스가 아닌 객체를 반환한다면, 시스템은 뷰 컨트롤러에게 상태 정보를 디코드하라고 요청하지 않는다. 그러므로 완벽히 다른 뷰 컨트롤러와 서로 맞바꾸는 것은 객체의 완전한 상태를 복원하지 않는다.
  • 상태 보존 시스템은 그것이 의도한 대로 뷰 컨트롤러를 사용할 것임을 기대한다. 복원 프로세스는 인터페이스를 다시 만들기 위해 뷰 컨트롤러의 포함 관계에 의존한다. 컨테이너 뷰 컨트롤러를 적절하게 사용하지 않는다면 보존 시스템은 뷰 컨트롤러를 찾을 수 없다. 예를 들어 부합하는 뷰 컨트롤러 간에 포함 관계가 없다면, 다른 뷰 안에 뷰 컨트롤러의 뷰를 절대 임베드해서는 안된다.