Search

UIViewController, UIView 톺아보기(1) - UIViewController 편

 들어가며

*공식문서 내용을 혼잣말을 섞어가며 정리한 글이라고 보시면 됩니다. 생각에 흐름대로 쓴 글이라서 흐름에 올라타서 읽으시면 됩니다..
어쩌다가 UIViewController, UIView를 톺아보게 되었는가?
프로젝트 리팩토링을 하다가 팝업 뷰를 UIViewController로 띄울지, UIView로 띄울지 고민이 되는 상황에 맞닥뜨렸습니다. 원래는 UIViewController로 띄우는 화면이었는데, UIView로 만들어도 충분히 괜찮지 않을까 하는 생각이 들더라구요.
그렇다면 애플이 원하는 UIViewController, UIView 각자의 역할은 뭘까 라는 생각이 또 들었습니다. 제가 애플에서 원하는 방식으로 UIViewController, UIView를 사용해오고 있는지 궁금했거든요.
UIViewController는 어떤 역할을 하도록 만든 Class일까? UIView는 어떤 역할을 하도록 만든 Class일까?
생각을 하다보니 왜 아직까지 UIViewController, UIView에 대한 공식문서를 본 적이 없지? 라는 생각이 또 들었습니다. 너무 기본적인거라서 무시했나봅니다. 하지만 이젠 무시할 수 없습니다.
역할을 확인하기 위해서 공식문서를 샅샅이 보려고 합니다.

 UIViewController

An object that manages a view hierarchy for your UIKit app. UIKit 앱의 뷰 계층 구조를 관리하는 객체
UIViewController는 뷰 계층 구조를 관리하는 객체입니다. 화면을 보여주는 역할을 한다는 내용이 있을줄 알았는데, 보여주는 화면의 뷰 계층 구조를 관리하는 것이 주 목적인 객체였습니다.

Overview

Overview에 따르면 UIViewController는 직접 Class 객체를 사용하기 보다는 subclass로 많이 사용한다고 합니다. 그리고 우리는 이미 자연스럽게 서브 클래싱해서 사용하고 있었습니다.
우리는 새로운 화면을 만들 때, 이렇게 코드를 작성합니다.
final class ViewController: UIViewController { }
Swift
복사
자연스럽게 UIViewController를 상속받아서 사용합니다. 그리고 UIViewController에서 제공해주는 뷰 계층을 관리하는 데 필요한 메서드와 속성을 가져와서 사용하게 됩니다.
그렇다면, 우리는 UIViewController가 어떤 역할(책임)을 해야한다고 생각하면서 코드를 작성해야할까요?
Overview에서는 view controller’s main responsibilities 4가지를 설명해줍니다.
View Controller’s main responsibilities
1.
데이터 변경에 의한 뷰 내용 업데이트
2.
유저 인터렉션에 대한 응답
3.
뷰 크기 조정 및 전체 인터페이스 레이아웃 관리
4.
앱 내에서 다른 ViewController들을 포함한 다른 객체와의 관계 관리(Coordinating)
전에 MVC 정리(아키텍쳐 패턴 - MVC 란?)를 진행한 적이 있는데, 그때 정의했던 Controller의 역할과 거의 똑같은 거 같습니다.
애니또 프로젝트 리팩토링 도전(1) - View / ViewController 역할 나눠보기 에서 정의했던 Controller의 역할을 하도록 UIViewController를 리팩토링 했던 적이 있는데, 3번 역할빼고는 UIViewController가 책임을 다 하도록 잘 나눠놓은 것 같네요.
다시 공식문서로 돌아가보면,
UIViewController는 뷰 계층에서 이벤트 처리를 관리하는 뷰와 밀접하게 연결이 되어 있다고 합니다.
왜 그럴까요?
UIViewController를 자세하게 들여다보면, UIViewController가 UIResponder 개체를 상속받고 있는걸 볼 수 있습니다.
UIResponder의 공식문서 내용에 보면 UIResponder가 UIKit앱의 이벤트 처리의 backbone 역할을 한다고 되어 있습니다. 즉, 주요한 이벤트 처리에 대한 일을 합니다.
만약에 ViewController의 view가 이벤트를 다루지 않는다면 ViewController가 그 일을 대신하거나 Superview가 대신하게 됩니다.
여기서 view 라는 게 UIView를 상속받는 UIButton, UILabel 등의 컴포넌트 Class도 포함된다고 저는 생각했습니다. 따라서 UIButton를 터치하는 이벤트가 발생했을 때 ViewController나 Button를 subview로 가지고 있는 Superview(Root View)가 이 일을 대신 처리한다 라고 저는 이해했습니다. 잘못된 부분이 있다면 말해주세요..
ViewController는 단독 사용하는 경우가 거의 없다고 설명합니다. 앱의 화면을 각각 구성하는 ViewController를 하나씩 만들기 때문이죠.
그래야만 하는 이유는 일반적으로 한 번에 한 View Controller 안의 뷰들만 볼 수 있기 때문입니다. 다른 뷰 컨트롤러에 있는 뷰를 보려면 해당 뷰 컨트롤러를 Present해서 띄우거나 다른 뷰 컨트롤러의 Content를 위한 Container 역할을 하면 됩니다.
모든 앱은 여러 개의 커스텀 UIViewController를 만들 수 있으며, 각각의 커스텀 ViewController는 앱의 appearance, behavior를 정의하고, 유저 인터렉션에 어떻게 반응할 지도 정의해둡니다.
Subclassing notes에서는 이런 여러 개의 커스텀 ViewController를 어떤 식으로 관리하는지 알아봅니다.

Manage views

각 View Controller는 root view를 가지고 있습니다. 그리고 root view는 뷰 컴포넌트들을 저장하는 역할을 하고 뷰 컴포넌트들을 subview로 가지고 있으면서 뷰 계층을 관리합니다.
이런 root view의 사이즈, 위치는 루트 뷰를 소유한 상위 View Controller, 혹은 앱의 window에 의해서 변경되게 됩니다. 여기서 말하는 window는 AppDelegate, SceneDelegate에서 보이는 UIWindow 클래스 타입을 가진 window를 생각해주시면 될 거 같네요.
window가 ViewController를 소유하게 된다면 해당 ViewController는 이 앱의 root ViewController가 됩니다. 그리고 해당 ViewController가 가진 root view의 사이즈는 window를 꽉 채우는 사이즈가 됩니다.
이 부분은 우리가 SceneDelegate나 AppDelegate에서 처음 진입하는 루트 뷰 컨트롤러를 설정할 때, window의 rootViewController로 커스텀 ViewController를 설정해주는 상황을 생각해주시면 이해가 될 겁니다.
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let windowScene = (scene as? UIWindowScene) else { return } let window = UIWindow(windowScene: windowScene) let storyboard = UIStoryboard(name: "Splash", bundle: nil) guard let viewController = storyboard.instantiateViewController(withIdentifier: SplashViewController.className) as? SplashViewController else { return } window.rootViewController = viewController self.window = window window.makeKeyAndVisible() }
Swift
복사
window 안에 rootViewController로 넣어준 Splash 화면은 이 앱의 root ViewController가 될 것이고, 해당 화면은 window를 꽉 채우는 사이즈가 되겠죠.
ViewController는 가지고 있는 뷰들을 lazy하게 로드합니다. ViewController가 생성되자마자 함께 로드되는 것이 아니고 view property에 처음 접근했을 때 로드되거나 생성된다고 합니다.
해당 부분을 보면서 의문이 하나 생겼습니다. 우리가 lazy 키워드를 쓰지 않아도 뷰는 lazy하게 로드되는걸까 라는 의문이 들었습니다. lazy property는 처음부터 property를 생성하고 있는것이 아니고 처음 해당 property에 접근했을 때 로드되거나 생성되는걸로 저는 알고 있습니다. 근데 view property도 처음 접근했을 때 로드되거나 생성되는 것이라면 그 자체로 lazy한걸까 라는 생각이 들더라구요.
그래서 작은 실험을 한 번 진행해봤습니다.
ViewController에 View를 지정하는 방법을 공식문서에서는 총 3가지로 설명합니다.
Specify the views for a view controller
1.
Storyboard에서 ViewController에 View 지정
Storyboard는 뷰를 지정하는 방식 중 하나입니다. Storyboard를 사용해서 view와 ViewController 사이에 연결을 설정할 수 있습니다. 또한 ViewController들 간의 관계 및 segue를 설정할 수 있습니다.
Storyboard로부터 ViewController를 로드할 때는 instantiateViewController(withIdentifier:) 메서드를 사용해서 적절한 개체를 가져오도록 할 수 있습니다. Storyboard 개체가 ViewController를 생성하고 코드로 return 해줍니다.
2.
nib 파일을 사용해서 ViewController에 View 지정
nib파일은 View를 지정하는 건 가능하지만 ViewController 간의 segue, relationship은 정의할 수 없습니다. ViewController 자체에 대한 최소한의 정보만을 가지고 있습니다.
nib를 사용해서 ViewController를 초기화한다면 code로 ViewController를 만들고 init(nibName:bundle:) 메서드로 초기화합니다. View가 요청되면 ViewController가 nib 파일로부터 해당 View들을 로드합니다.
3.
loadView() 메서드를 사용하여 ViewController에 대한 View를 지정
해당 방법으로 code로 뷰 계층을 생성할 수 있습니다. 해당 계층의 root view를 ViewController의 view property로 할당합니다.
override func loadView() { self.view = subview() }
Swift
복사
3가지의 방식 모두 같은 결과를 나타냅니다. 모두 Views를 생성해내고 view property를 통해서 해당 Views를 보여줍니다.
우리가 중요하게 생각해야하는 부분은 ViewController가 View, Subview들의 유일한 소유자라는 겁니다. 하지만 영원히 View를 소유하지 않을겁니다. 이후에 ViewController가 released되면 View도 ViewController 부터 자유로워집니다. 아마도 released 된다는 건 메모리에서 해제, 즉 deinit 되는 순간인 것 같습니다.
Storyboard나 nib 파일을 사용하는 경우에는 ViewController가 요청하는 경우에 각 ViewController 개체가 해당 View의 자체 복사본을 자동으로 가져옵니다. 하지만 수동으로 Views를 만드는 경우(코드로 짜는 경우)에는 각 ViewController는 고유한 Views를 가지고 있어야 하고 이는 View Controller 간에 공유될 수 없습니다.
고유한 Views를 가지고 있어야 하고 View Controller 간에 공유가 불가능하다면 만들어진 View는 하나의 ViewController만을 위해서만 존재해야하는가 생각이 들었습니다. 원문으로도 each ViewController must have its own unique set of views 라고 적혀있습니다. own unique set of views를 가져야만 한다는 것이니.. 만약 다른 ViewController 사이에서 같은 View가 재사용해야되는 경우에는 이를 위반하는 걸까요. 이런 경우에는 View가 동일하게 사용되는 것이니 ViewController를 재사용하는걸까요?
ViewController를 재사용하는 것은 생각만 해도 내부가 복잡한 기분이네요..
계속 이어서, ViewController의 root view는 항상 할당된 공간에 딱 맞춰서 자리를 잡습니다. 그리고 뷰 계층 안에 있는 다른 Views는 Interface builder를 사용해서 각 View가 superview의 bound 내에서 배치되고 크기가 조정되도록 Auto layout constraint를 지정합니다. constraint를 code로 작성해서 적절한 타이밍에 view들이 constraint를 적용할 수 있도록 할 수도 있습니다.
Interface builder를 봤을 때, 시스템 내부에 Auto layout를 담당하는 빌더 같은 게 있나보다 했는데, Storyboard에서 Auto layout 잡을 때 유용하게 사용했던 툴들의 이름이었습니다. 이제 알았네요..

Handle view-related notifications

뷰의 visibility가 변경되면 UIViewController는 자동적으로 자신의 메소드를 호출시켜서 subclass 들도 변화에 응답할 수 있도록 합니다.
즉, 뷰의 보이는 상태가 바뀌면 UIViewController에서 자동으로 ViewDidLoad, ViewWillAppear 등을 호출시키고 하위에 있는 클래스들도 이에 맞춰서 상태를 변화시킵니다.
위의 그림에서는 ViewController에서 발생할 수 있는 State Transition를 볼 수 있습니다.
모든 will 메서드가 did 메서드와 쌍을 이루는 것은 아닙니다. will 메서드로 프로세스를 시작하는 경우더라도, did, will 방식 모두를 사용해서 프로세스를 종료시킬 수 있습니다.

Handle view rotations

여기서는 iOS 6, 7에 대한 rotation 관련 이야기도 나오는데, 해당 메서드들이 deprecated되었기 때문에 iOS 8 이후에 관련한 내용부터 적어보도록 하겠습니다.
iOS 8부터는 rotation과 관련된 모든 메서드들이 사용되지 않습니다.(iOS 6, 7에서 사용했던 willRotate, willAnimateRotation 등 …) 대신 iOS 8부터는 rotation를 View Controller의 View의 사이즈 변화로 간주해서 viewWillTransition(to:with:) 메서드를 사용해서 관리합니다.
인터페이스의 화면 방향이 변경되면, UIKit이 window의 root view controller에서 viewWillTransition 메서드를 호출해서 Child View Controller들에게 이를 알리면서 View Controller 계층 전체에 해당 메시지가 전달되게 됩니다.
viewWillTransition Call 해보기
앱이 런치될 때, 앱은 항상 portatit orientation(세로 방향)으로 interface를 설정합니다. 그리고 application(_: didFinishLaunchingWithOptions:)가 완료한 후에 앱은 window 표시 전에 rotation 매커니즘을 사용해서 뷰를 적절한 방향으로 회전시킵니다.

Implement a container view controller

위에서도 설명했듯 ViewController는 Container ViewController로도 사용합니다.
Container ViewController는 다른 ViewController의 Content를 보여주며 Container ViewController에 포함된 Child View Controller는 Container ViewController가 가진 View와 함께 display 됩니다.
Container ViewController의 subclass는 해당 subclass에 포함된 Child들과 연결할 공용 interface를 선언해야 합니다. 메서드의 특성은 사용자에게 달려있으며 사용자가 생성하려는 Container의 의미에 따라서 달라지게 됩니다.
사용자는 Container ViewController를 생성하면서 3가지를 고려해야 합니다.
1.
한 번에 display할 수 있는 child 수
2.
언제 child를 display 할 건지
3.
ViewController의 View hierarchy에서 표시되는 위치
Clean Public Interface를 설정하면, Container에 대한 너무 많은 세부 정보를 접근하지 않고 Child를 논리적으로 사용 가능합니다.
Container ViewController를 사용할 때, child의 root view를 뷰 계층에 추가하기 전에 child ViewController와 자신을 연결해야 합니다.
그래야 Child ViewController와 Controller가 관리하는 View로 적절하게 이벤트를 라우팅할 수 있습니다.
연결과 마찬가지로 뷰 계층에서 child root view를 제거할 때에는 Child ViewController와의 연결을 끊어야 합니다. 연결을 맺고 끊는 것은 base class에 있는 특정 메서드를 호출해서 구현하면 됩니다.
1.
addChild(_:)
2.
removeFromParent()
3.
willMove(toParent:)
4.
didMove(toParent:)
Container ViewController를 만들 때, 메서드 재정의할 필요가 없습니다. 기본적으로 rotation, appearance 콜백은 자동으로 제공됩니다. shouldAutomaticallyForwardRotationMethods(), shouldAutomaticallyForwardAppearanceMethods 메서드는 옵션으로 override해서 사용할 수 있습니다.
위에서 나오는 shouldAutomaticallyForwardRotationMethods() 메서드는 deprecated되어서 이젠 사용할 수 없고, shouldAutomaticallyForwardAppearanceMethods는 사용 가능합니다. 해당 메서드는 Appearance 메서드를 Child View Controller로 전달할지 여부를 나타내는 Boolean값을 반환합니다.
View Controller는 중요한 순간에 메모리 설치 공간을 줄일 수 있는 내장 지원 기능을 제공합니다. UIViewController 클래스는 필요 없는 메모리를 해제하는 didReceiveMemoryWarning() 메서드를 통해 메모리 부족 상태의 일부를 자동으로 처리하는 기능을 제공합니다.
restoration Identifier property에 값을 할당한다면, 앱이 background로 전환될 때 시스템이 View Controller에 자체 인코딩을 요청할 수 있습니다. View Controller는 restoration Identifier를 포함하는 뷰 계층의 모든 뷰 상태를 보존합니다. ViewController는 다른 상태는 자동으로 저장하지 않습니다.
사용자가 만든 Container ViewController를 구현하는 경우, Child ViewController를 직접 인코딩해야 합니다. 인코딩하는 각 Child는 고유한 restoration Identifier가 있어야 합니다.
이게 무슨 내용인가 싶을겁니다. 저도 background로 전환될 때 뷰 상태를 보존한다는게 어떤 말인지 이해가 안됐습니다. 해당 섹션 하단에 example 프로젝트가 있어서 해당 프로젝트를 돌려봤는데 굉장히 놀랐습니다.
다른 뷰로 들어간 후에 앱 자체를 종료해버렸는데 앱을 다시 키니깐 해당 화면부터 뜨는겁니다. 이런건 UserDefault를 사용해서 해당 state를 저장해두는건가 했는데, restoration Identifier를 사용해서 구현하는 거였더라구요. 정말 신기했습니다.

 마치며

View까지 알아보기엔 내용이 너무 많아서 이번 포스팅에서는 UIViewController 공식 문서까지만 봤습니다. 새로 알아가는 메서드들도 많았고, 왜 그렇게 써야 하는가에 대한 답변도 얻어가는 기분입니다.
중간중간 들어있는 링크들에도 읽어야 하는 내용들이 많이 포함되어 있어서 부지런히 공부해야할 듯 합니다.
아직 ViewController 내부에 있는 Views가 lazily하다는 점과 ViewController 내부에 Views는 unique해야 한다는 점에 궁금증이 남아있긴 하지만 점차 알아가보면 될 듯 합니다.
아자! ٩( ᐛ )و

 참고자료