[iOS Programming Big Nerd Ranch] 4. View Controller 들


View Controller를 하나 이상 두려면 어떻게 해야 하는가?


View of a View Controller

UIViewController의 subclass인 모든 view controller들은 var view: UIView! 라는 중요한 프로퍼티를 상속받는다.

이 프로퍼티는 뷰 컨트롤러의 뷰 계층의 루트인 UIView 인스턴스를 가리킨다. 뷰 컨트롤러의 뷰가 window의 subview에 더해지면, 뷰 컨트롤러의 전체 뷰 계층은 아래와 같이 더해진다.

image

뷰 컨트롤러의 뷰는 스크린에 뜰 때까지 생성되지 않는다. 이런 최적화를 lazy loading이라고 하고, 메모리를 아끼고 성능을 향상 시킬 수 있다.

뷰 컨트롤러가 뷰 계층을 생성하는 두 가지 방법이 있다.

  1. Interface Builder에서 스토리보드와 같은 interface file을 이용한다.
  2. 코딩으로, UIViewCOntroller의 메서드 loadView()를 오버라이딩한다.

지금은 Interface Builder를 이용해서 뷰 컨트롤러들을 보고, 다음 글에서 코드의 loadView()를 이용해서 뷰를 코드로 생성하도록 하겠다.

Setting the Initial View Controller

먼저 스토리보드에서 View Controller를 라이브러리에서 드래그 드롭해서 새로 생성하고, 이 뷰 컨트롤러의 View 부분을 선택해서 지운 다음, MKMapView를 라이브러리에서 찾아 드래그-드롭해서 생성해준다.

이 새로 생성된 View Controller를 Initial View Controller로 만드려면 inspector 창에서 is Initial View Controller 부분을 선택한다. 이러면 화면상에서 회색 화살표가 새로 만든 뷰 컨트롤러를 가리키는 것을 확인할 수 있다.

image

image

하지만 여기까지 했을 때도 문제가 생긴다. MKMapView는 현재 애플리케이션에 로드되지 않는 프레임 워크다. 프레임워크는 interface file이나 이미지 같은 관련된 리소스를 포함하는 공유되는 라이브러리 코드다. 앞서서 계속 사용했던 UIKitFoundation도 프레임 워크였다.

MKMapView를 로드해서 MapKit 프레임워크를 import 해야 한다. 하지만 이렇게 코드를 사용해서 MapKit를 import했는데 코드 상에서 이 프레임워크를 사용하는 부분이 없다면, 스토리보드에서 map view를 사용하고 있음에도 불구하고 컴파일러가이걸 제외시킬 것이다.

대신에, 수동으로 앱에 MapKit 프레임 워크를 link 한다.

프로젝트 네비게이터를 열어서, General tab에서 Frameworks, Libraries, .. 부분을 찾아서 +를 눌러 MapKit.framework를 추가한다.

image

이제 앱을 시뮬레이터로 실행시키면 새로 추가한 맵 뷰 컨트롤러가 먼저 뜨게 될 것이다.

Storyboard는 오직 하나의 초기 뷰 컨트롤러를 가질 수 있다. 이게 루트 레벨 UIWindow가 윈도우 계층에 어떻게 초기 뷰 컨트롤러의 뷰를 추가하는 지 알아보자.

UIWindowrootViewController 프로퍼티를 가진다. 뷰 컨트롤러가 window의 rootViewController로 설정되면, 뷰 컨트롤러의 뷰가 위도우의 뷰 계층에 추가된다. 만약 이 프로퍼티가 설정되면, window의 모든 존재하는 subview들은 지워지고 뷰 컨트롤러의 뷰가 적절한 Auto Layout 제약사항들을 가지로 window에 추가된다.

각 앱들은 하나의 메인 interface를 가지고 있고 이는 storyboard를 가리킨다. 앱이 실행되면, main interface의 초기 뷰 컨트롤러는 window의 rootViewController로 설정된다. 앱의 main interface는 프로젝트 세팅에서 설정할 수 있다. General tab의 Deployment Info의 Main Interface에서 이를 확인할 수 있다. 여기서 MainMain.storyboard를 말한다.

image

UITabBarController

뷰 컨트롤러를 전환하는 여러 방법이 있는데, 지금은 UITabBarController를 이용할 것이다. 스토리보드를 열고 View Controller를 선택한 다음 아래의 메뉴에서 Tab Bar Controller를 선택한다.

image

이렇게 하면 View Controller들을 Tab Bar Controller의 view controller 배열로 추가한다. Tab Bar Controller에서 View Controller로 선이 그어지는 것을 볼 수 있다. 추가로, Interface Builder는 Tab Bar Controller를 스토리 보드의 초기 뷰 컨트롤러로 만든다.

image

그리고 이제 원래 있던 다른 뷰 컨트롤러인 Conversion View Controller를 Tab Bar Controller의 뷰 컨트롤러 배열에 더한다. Tab Bar Controller를 컨트롤을 누른 상태에서 드래그 한 후 Conversion View Controller로 드롭한다. 그리고 view controllers를 팝업 창에서 선택한다.

이제 앱을 실행시키면 밑에 있는 탭 바를 선택해서 뷰 컨트롤러들을 전환할 수 있다.

image

하지만 밑에 있는 바의 탭이 모두 Item이라고 표시하고 있기 때문에 이를 구별하기 쉽지 않다.

image

UITabBarControllerUIViewController의 subclass이다. UITabBarController의 뷰는 두 개의 subview로 이루어져 있다. 하나는 위의 스샷에서 본 하단의 탭 바이고 다른 하나는 선택된 뷰 컨트롤러의 뷰다.

image

Tab bar items

탭 바의 각 탭은 이미지와 타이틀을 표시할 수 있고, 각 뷰 컨트롤러는 이를 위해 tabBarItem 프로퍼티를 가지고 있다. UITabBarController에 의해 뷰 컨트롤러가 포함될 때, 이 tab bar item이 tab bar에 나타난다. 아래는 아이폰의 전화 앱 예시다.

image

먼저 탭 바의 이미지에 들어갈 이미지를 추가한다.

image

스토리보드에서 뷰 컨트롤러마다 tab bar item 설정을 바꿔준다. Title과 image를 바꿨다.

image

그리고 캔버스의 탭 바 컨트롤러의 하단 탭 바의 아이콘을 드래그해서 탭 바 컨트롤러의 viewControllers 배열 순서를 바꿀 수도 있다.

실행시키면 탭 바의 아이콘과 이름이 잘 바뀐 걸 확인할 수 있다.

image

Loaded and Appearing Views

이제 두 개의 뷰 컨트롤러들이 있고, 여기서 뷰의 lazy loading 이 더 중요하게 된다. 앱이 실행되면, 탭 바 컨트롤러는 뷰 컨트롤러 배열의 첫번째 뷰를 불러온다. 불러와지는 뷰 말고 다른 뷰들은 유저가 탭을 클릭해서 보지 않는 이상 필요가 없다.

뷰 컨트롤러가 뷰를 로딩하는 것을 마쳤다면 viewDidLoad() 함수가 불러와지고, 이 메서드를 오버라이딩 해서 불러와지는 순간에 작업을 할 수 있다.

이제 각 뷰 컨트롤러의 viewDidLoad()에 해당 뷰 컨트롤러가 로드되었다는 프린트 문을 찍고 앱을 실행시키면, 뷰 컨트롤러의 뷰가 화면에 노출 될 때 로그가 찍히는 것을 확인할 수 있다. 두 뷰가 모두 불러와진 상태에서 탭을 왔다갔다 해서 화면을 전환해도, 더 이상의 로그는 찍히지 않는다. 그 이유는 모든 뷰들이 다 로드가 되었기 때문에, 탭을 전환하는 것이 더 이상 viewDidLoad()를 불러오지 않기 때문이다.

Accessing subviews

Interface Builder에 정의된 subview들이 사용자에게 노출되기 전에 추가의 초기화나 설정을 하고 싶을 때가 많을 것이다. 그렇다면 subview는 어디에서 접근해야 하는가? 두 가지 선택지가 있다.

  1. viewDidLoad() 메서드가 lazy loading 할 때 불러와지는 걸 이용하는 것이다. 이 메서드는 뷰 컨트롤러의 outlet이 모두 적절한 객체를 참조하는 순간인 뷰 컨트롤러의 인터페이스 파일이 로드 되는 직후에 호출된다.
  2. UIViewController의 메서드인 viewWillAppear(_:) 메서드를 사용한다. 이 메서드는 뷰 컨트롤러의 뷰가 window에 추가되기 직전에 불러와진다.

그렇다면 이 둘 중 무엇을 골라야 할까?

  1. viewDidLoad()는 앱을 실행하는 도중 설정이 단 한 번만 되어야 할 때 오버라이딩 한다.
  2. viewWillApear()는 뷰 컨트롤러의 뷰가 화면에 뜰때마다 설정이 되어야 할 때 오버라이딩 한다.

Interacting with View COntrollers and Their Views

뷰 컨트롤러와 뷰의 생명주기 도중에 호출되는 메서드들을 좀 더 보도록 하겠다. 이 중 일부는 이미 본 것이고, 몇 개는 새로운 것이다.

  • init(coder:) : 스토리보드에서 생성된 UIViewController 인스턴스를 위한 초기화 메서드다. 만약 뷰 컨트롤러의 인스턴스가 스토리보드에서 생성되면, init(coder:)는 한 번 호출된다.
  • init(nibName:bundle:) : UIViewController의 지정 초기화 메서드다. 만약 뷰 컨트롤러의 인스턴스가 스토리보드 없이 생성되면 이 메서드는 한 번 호출된다. 몇 앱에서는 같은 뷰 컨트롤러의 클래스의 인스턴스를 여러개 생성해버릴 수도 있다는 것을 주의해야 한다. 이 메서드는 각 뷰 컨트롤러가 생성되면 한 번 호출된다.
  • loadView() : 뷰 컨트롤러의 뷰를 코드로 생성하기 위해 오버라이딩 된다.
  • viewDidLoad()는 인터페이스 파일을 로딩해서 생성된 뷰를 구성하기 위해 오버라이딩 된다. 이 메서드는 뷰 컨트롤러의 뷰가 생성된 직후에 호출된다.
  • viewWillAppear(_:) : 인터페이스 파일을 로딩해서 생성한 뷰를 구성하기 위해 오버라이딩 된다. 이 메서드와 viewDidAppear(_:)는 뷰 컨트롤러가 화면에 띄워질 때마다 호출된다. viewWillDisappear(_:)viewDidDisappear(_:)는 뷰 컨트롤러가 화면에서 사라질때마다 불러와진다.