[iOS Study] 뷰 컨트롤러 |
출처 : 아론 힐리가스의 iOS 프로그래밍
-
뷰 컨트롤러는 UIViewController 의 하위 클래스 인스턴스이다.
뷰 컨트롤러는 뷰 계층구조를 관리한다.
뷰 컨트롤러는 계층구조를 구성하는 뷰 객체들을 만들고 뷰 객체와 관련된 이벤트를 제어하고, 윈도우에 뷰 계층구조를 추가하는 역할을 한다.
-
UIViewController 의 하위 클래스는 중요한 프로퍼티를 하나 상속받는다.
@property (nonatomic, strong) UIView *view;
이 프로퍼티는 UIView 인스턴스를 가리킨다.
뷰 컨트롤러의 View 가 윈도우의 하위뷰로 추가되면, 뷰 컨트롤러의 뷰계층 전부가 윈도우에 추가된다.
-
뷰 컨트롤러의 view 는 화면에 보여줄 필요가 있을 때까지 만들어지지 않는다.
이러한 최적화를 지연 로딩(lazy loading) 이라 하고 종종 메모리를 절약하면서 성능을 향상시킬 수 있다.
-
뷰 컨트롤러가 뷰 계층구조를 만들 수 있는 두 가지 방법이 있다.
1. 프로그래밍으로 UIViewController 의 loadView 메소드를 재정의한다.
2. 인터페이스 빌더를 통해 NIB 파일을 읽어들이도록 한다. ( 프로그램에서 읽어들이는 파일이 NIB 파일이고, XIB 파일은 인터페이스 빌더에서 편집할 수 있는 파일이라는 것을 상기한다. )
-
뷰 컨트롤러가 만들어질 때 그 컨트롤러의 view 프로퍼티는 nil 이다.
뷰 컨트롤러는 view 를 요청하여 그 view 가 nil 이면 뷰 컨트롤러는 loadView 메시지를 받는다.
-
뷰 컨트롤러의 뷰 계층구조를 윈도우에 추가하는 편리한 메소드가 있다.
UIWindow 의 setRootViewController: 메소드이다.
뷰 컨트롤러를 rootViewController 로 설정하면 뷰 컨트롤러의 view 가 윈도우의 하위뷰로 추가된다.
또한 자동으로 뷰의 크기를 윈도우와 동일하게 조절한다.
-
뷰 컨트롤러가 NIB 파일을 불러와 뷰 계층구조를 갖게 되면 loadView 를 재정의하지 않는다.
loadView 의 기본 구현은 NIB 파일을 불러와 처리하는 법을 알고 있다.
ViewController 는 불러올 NIB 파일이 어디에 있는지 알아야 한다.
UIViewController 의 지정 초기화 메소드에서 이를 처리할 수 있다.
-(instancetype)initWithNibName:(NSString *)nibName bundle:(NSBundle *)nibBunble;
이 메소드에 불러올 NIB 파일명과 NIB 파일을 검색할 번들명을 전달한다.
-
mainBundle 메시지를 보내서 얻게 되는 번들은 앱 번들이다.
이 번들은 앱의 실행파일과 그 앱이 사용할 리소스(NIB 파일 등)을 포함한 파일시스템상의 디렉터리이다.
ViewController.xib 파일이 이 안에 있다.
-
-[UIViewController _loadViewFromNibNamed:bundle:] loaded the “ViewController” nib but the view outlet was not set.
위와 같은 에러 메시지에서는 실행 중인 앱에서 ViewController 와 인스턴스화된 객체들 간을 연결하지 않아서 나오는 에러이다.
뷰 컨트롤러의 view 프로퍼티도 포함된다.
뷰 컨트롤러가 화면에 추가할 view 를 얻고자 할 때 view 프로퍼티가 nil 이기 떄문에 예외를 던지는 것이다.
-
File’s Owner 는 플레이스홀더 객체이다.
이는 XIB 파일에서 의도적으로 둔 연결 구멍이다.
NIB 을 로딩하는 과정은 두 부분으로 나뉜다.
XIB 에 아카이브된 모든 객체를 인스턴스화하고 나서 NIB 을 로딩 중인 그 객체를 File’s Owner 구멍에 놓는다.
그리고 준비한 커넥션들을 설정한다.
실행 중에 NIB 을 로딩하는 객체에 연결하고자 한다면 XIB 에서 작업할 때 File’s Owner 를 연결해야 한다.
첫 단계는 XIB 파일에 ViewController 의 의 인스턴스가 File’s Owner 가 될 것이라고 알리는 것이다.
-
식별 인스펙터(identity inspector) 의 Custom Class 의 Class 부분에
ViewController 를 연결해주어야 한다.
-
view 에서부터 드래그하여 캔버스 위의 UIView 객체도 연결해주어야 한다.
-
datePicker 아웃렛을 weak 로 선언했다.
아웃렛을 weak 로 선언하는 것은 예전 iOS 버전부터 내려온 관례이다.
이들 버전에서 뷰 컨트롤러의 view 는 시스템 메모리가 부족한 경우 자동으로 소멸됐다.
그리고 나중에 필요한 경우 다시 생성됐다.
뷰 컨트롤러가 하위뷰들에 대해 단지 약한 소유권만을 가지는 것은 뷰 컨트롤러의 view 가 소멸될 때 모든 하위뷰들이 소멸되어 메모리 누수를 피한다는 것을 의미한다.
-
UITabBarController 는 뷰 컨트롤러들의 배열을 유지한다.
또한 이 배열의 각 뷰 컨트롤러용 탭(tab) 을 가진 화면 하단의 탭바(tab bar)를 관리한다.
UITabBarController* tabBarController = [[UITabBarController alloc] init];
tabBarController.viewControllers = @[hvc, rvc];
-
UITabBarController 는 UIViewController 의 하위 클래스이다.
-
탭바의 각 탭은 제목과 이미지를 표시할 수 있다.
각각의 뷰 컨트롤러는 이러한 용도로 tabBarItem 프로퍼티를 관리한다.
뷰 컨트롤러가 UITabBarController 에 포함되면 해당 탭과 항목이 탭바에 나타난다.
// self 는 view controller
self.tabBarItem.title = @"title";
self.tabBarItem.image = [UIImage imageNamed:@"TabImage.png"];
-
Images.xcassets 의 좌측 이미셋 목록에 이미지를 드래그하여 리소스를 추가할 수 있다.
-
UIViewController 의 init 메소드를 호출해도 되는 이유는
지정 초기화 메소드가 initWithNibName:bundle: 이기 때문이다.
뷰 컨트롤러에 init 메시지를 보내면 두 인자를 nil i로 initWithNibName:bundle: 메소드를 호출한다.
-
NIB 파일을 사용하는 뷰 컨트롤러에 init 메시지를 보내면 어떻게 될까?
이전과 동일하게 동작할 것이다.
뷰 컨트롤러가 초기화될 때 NIB 이름이 nil 이면 클래스의 이름으로 NIB 파일을 찾는다.
그리고 번들에 nil 을 전달하면 뷰 컨트롤러는 메인 앱 번들에서 찾는다.
-
로컬 노티피케이션(local notification) 은 현재 랩이 실행 중이 아니더라도 앱에서 사용자에게 알림을 보내는 방법이다.
( 앱은 백엔드 서버를 사용하여 구현된 푸시 노티피케이션(push notification)을 사용할 수 있다.)
-
알림을 보기 위해서는 앱이 포그라운드 상태가 아니어야 한다.
-
뷰 컨트롤러가 뷰의 로딩 작업을 완료할 때 viewDidLoad 메시지를 받는다.
-
지연 로딩의 이점을 유지하려면 initWithNibName:bundle: 에서 뷰 컨트롤러의 view 프로퍼티에 절대 접근해서는 안 된다.
초기화 메소드에서 view 를 요청하면 뷰 컨트롤러가 너무 이르게 view 를 불러오기 때문이다.
-
사용자에게 화면을 표시하기 전에, XIB 파일에 정의된 하위뷰들에 추가적인 초기화가 필요한 경우가 종종 있다.
하지만 뷰 컨트롤러의 초기화 메소드에서는 NIB 파일이 아직 로드되지 않았기 때문에 이러한 추가 작업을 할 수 없다.
만약 초기화 메소드에서 뷰 컨트롤러에 선언된 어떤 포인터를 사용한다면 결국 그 포인터는 nil 인 하위뷰들을 참조할 것이다.
-
하위뷰 접근은 필요에 따라 두 가지 방법이 있다.
첫 번째 방법은 지연 로딩을 설정하기 위해 재정의한 viewDidLoad 메소드를 사용하는 것이다.
뷰 컨트롤러는 NIB 파일이 로딩된 후 이 메시지를 받는다.
이 때 뷰 컨트롤러의 모든 포인터들은 유효한 객체를 가리키고 있다.
두 번째 방법은 UIViewController 의 또 다른 메소드 viewWillAppear: 를 사용하는 것이다.
뷰 컨트롤러는 view 가 윈도우에 추가되기 직전에 이 메시지를 받는다.
차이점은 viewDidLoad 는 앱을 실행할 때 한 번 설정해야 하는 경우에 재정의하고
viewWillAppear: 는 화면에 뷰 컨트롤러가 나타날 때마다 설정이 필요한 경우 재정의한다는 점이다.
-
때때로 뷰 컨트롤러의 view 는 소멸되고 다시 로드될 수 있다.
하지만 이는 최신 장비에서는 일반적인 방식이 아니다.
-
viewWillAppear: 메소드의 animated 플래그는 뷰가 나타나거나 사라질 때 애니메이션 효과를 적용할지 여부를 가리킨다.
UITabBarController 의 경우에는 전환시에 애니메이션을 사용하지 않는다.
-
뷰 컨트롤러와 뷰의 생존 주기 동안 호출되는 메소드들은 다음과 같다.
application:didFinishLaunchingWithOptions: 는 랩의 루트 뷰 컨트롤러를 인스턴스화하고 설정했던 곳이다.
이 메소드는 앱이 시작될 때 한 번만 호출된다.
다른 앱으로 이동했다가 돌아와도 이 메소드는 다시 불리지 않는다.
폰을 재부팅하거나 앱을 다시 시작해야만 application:didFinishLaunchingWithOptions: 가 다시 호출된다.
initWithNibName:bundle: 은 UIViewController 의 지정 초기화 메소드이다.
뷰 컨트롤러 인스턴스가 생성되면 initWithNibName:bundle: 메소드가 한 번 호출된다.
동일한 뷰 컨트롤러 클래스의 인스턴스를 여러 개 만들 수도 있다.
이 메소드는 인스턴스가 각각 만들어질 때마다 호출된다.
loadView: 는 뷰 컨트롤러의 뷰를 프로그래밍으로 만드는 경우에 재정의한다.
viewDidLoad 는 NIB 파일을 불러와 뷰들을 구성하는 경우에 재정의할 수 있다.
이 메소드는 뷰 컨트롤러의 뷰가 만들어진 후 호출된다.
viewWillAppear: 는 NIB 파일을 불러와 뷰들을 구성하는 경우에 재정의할 수 있다.
이 메소드와 viewDidAppear: 메소드는 뷰 컨트롤러가 화면에 나타날 때마다 호출된다.
반대로 viewWillDisappear: 와 viewDidDisappear: 는 뷰 컨트롤러가 화면에서 사라질 때마다 호출된다.
-
NIB 파일을 불러올 때 키 값 코딩(key-value coding, KVC) 이라는 기법을 사용하여 아웃렛들이 설정된다.
키-값 코딩은 NSObject 에 정의되고, 이름으로 프로퍼티의 값을 가져오거나 설정하는 메소드들의 집합이다.
-(id)valueForKey:(NSString *)k;
-(void)setValue:(id)v forKey:(NSString *)k;
valueForKey:는 범용 세터 메소드이다.
다음과 같이 어떤 객체의 fido 프로퍼티의 값을 요청할 수 있다.
id currentFido = [selectedObj valueForKey:@“fido”];
이 경우 fido 메소드(fido 게터)가 있다면, 그 메소드가 호출되고 그 반환값을 이용한다.
fido 메소드가 없다면, 시스템은 _fido나 fido 라는 이름의 인스턴스 변수를 찾을 것이다.
인스턴스 변수가 존재하면 그 값을 이용하고, 접근자나 인스턴스 변수가 없다면 예외를 던진다.
setValue:forKey: 는 범용 세터 메소드이다. 다음과 같이 객체의 fido 프로퍼티의 값을 설정한다.
[selectedObject setValue:userChoice forKey:@“fido”];
setFido: 메소드가 있다면, 그 메소드가 호출된다.
만약 그 메소드가 없다면, 시스템은 _fido 나 fido 라는 이름의 변수를 찾아 그 값을 직접 대입한다.
접근자나 인스턴스 변수가 없다면 예외를 던진다.
NIB 파일이 로드될 때 아웃렛은 setValue:forKey: 를 사용하여 설정된다.
따라서 제대로 매핑이 안 되면 아래와 같은 에러가 날 수 있다.
[<ViewController 0x1424234> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key KEY.
-
이 절에서 가장 중요한 교훈은 코드를 읽게 될 다른 사람들을 위해 멋진 이름을 짓는 것보다 접근자 메소드 이름 규칙을 사용하는 것이 더 중요하다는 것이다.
-
코어 그래픽스 함수를 사용했다면 그래픽 요소들이 장비마다 다르게 보일 수 있다.
( 선, 곡선 등은 vector 이고, image 는 raster 이다. )
이미지가 장비 화면 종류에 따라 제작되어 있지 않으면 비트맵 이미지(JPEG 이나 PNG 파일 등)는 그리 매력적이지 않다.
안티 엘리어싱(anti-aliasing)이 적용되며 이미지는 격자가 생기는 대신 흐릿흐릿해진다.
대신 큰 이미지를 사용할 수도 있지만 평균화(averaging)로 인해 비레티나 디스플레이에서 이미지가 줄어들 때 다른 문제가 생길 수 있다.
유일한 해법은 앱에 두 이미지 파일을 포함하는 것이다.
비레티나 디스플레이의 화면 포인트 수와 동일한 픽셀을 가진 이미지와 레티나 디스플레이의 포인트 수의 각각 두 배 크기의 픽셀을 가진 이미지가 필요하다.
다행히도 어느 이미지를 어느 장비에서 불러야 하는지 제어하는 코드를 작성할 필요가 없다.
모든 것은 고해상도 이미지 파일 이름에 @2x를 붙이면 해결된다.
그리고 나서 UIImage 의 imageNamed: 메소드를 사용하여 이미지를 불러오면 된다.
이 메소드는 번들을 검색해서 각 장비에 맞게 적절한 이미지 파일을 가져온다.
cf) datePicker.minimumDate = [NSDate dateWithTimeIntervalSInceNow:60]; // 현재로부터 60초 후의 시간만 정의할 수 있도록 한다.
'프로그래밍 놀이터 > iOS' 카테고리의 다른 글
[iOS Study] UITableView 와 UITableViewController (0) | 2016.02.20 |
---|---|
[iOS Study] 델리게이션과 텍스트 입력 (0) | 2016.02.19 |
[iOS Study] 뷰 다시 그리기와 UIScrollView (0) | 2016.02.17 |
[iOS Study] 뷰와 뷰 계층구조 (0) | 2016.02.16 |
[iOS Study] ARC 를 통한 메모리 관리 (0) | 2016.02.15 |
댓글