'iPhone'에 해당되는 글 20건

  1. 2010.10.08 [iPhone] - 시작하세요! 아이폰 3 프로그래밍 - Part 17. 애플리케이션 지역화 1
  2. 2010.10.04 [iPhone] - 시작하세요! 아이폰 3 프로그래밍 - Part 16. 아이폰 카메라와 포토 라이브러리
  3. 2010.09.15 [iPhone] - 시작하세요! 아이폰 3 프로그래밍 - Part 15. 야호! 가속도센서!
  4. 2010.09.14 [iPhone] - 시작하세요! 아이폰 3 프로그래밍 - Part 14. 여기가 어디지? 코어 로케이션을 이용한 길 찾기
  5. 2010.09.05 [iPhone] - 시작하세요! 아이폰 3 프로그래밍 - Part 13. 탭, 터치 그리고 제스처
  6. 2010.08.23 [iPhone] - 시작하세요! 아이폰 3 프로그래밍 - Part 12. 쿼츠와 OpenGL을 이용한 그리기 1
  7. 2010.08.12 [iPhone] - 시작하세요! 아이폰 3 프로그래밍 - Part 11. 기본적인 데이터 저장 방법
  8. 2010.08.12 [iPhone] - 소스 모음 추천사이트 (appsamuck)
  9. 2010.08.10 [iPhone] - 시작하세요! 아이폰 3 프로그래밍 - Part 10. 애플리케이션 설정과 사용자 기본값 1
  10. 2010.08.10 [iPhone] [동영상]iPhone SDK Programming : A Beginner's Guide - James Brannan

[iPhone] - 시작하세요! 아이폰 3 프로그래밍 - Part 17. 애플리케이션 지역화

iPhone/[위키북스]시작하세요! 아이폰3 프로그래밍 2010. 10. 8. 16:17
* 애플리케이션 지역화
- 아이폰의 아키텍처는 지역화를 적용하기에 적합하도록 설계되었기 때문에 애플리케이션의 리소스를 다양한 언어로 쉽게 변경할 수 있을 뿐만 아니라, 한 언어의 여러 방언으로 쉽게 변경할 수 있다.
- 하지만, 일반적인 경우에는 이미 만들어진 애플리케이션에 다국어 기능을 추가하는 것은 처음부터 애플리케이션을 다시 만드는 것보다 더 어려운 일이다.

* 지역화 아키텍처
- 다국어를 지원하지 않는 애플리케이션을 실행하면 애플리케이션의 텍스트들은 기본 언어로 설정된 개발자의 모둑어가 적용되어 있을 것이다.
- 개발자가 애플리케이션에 다국어 기능을 추가하려고 할 때 각각의 언어를 표시하는 데 사용할 파일들을 저장하기 위해 애플리케이션 번들 디렉터리에 언어별로 하위 디렉터리를 생성할 것이다. 이와 같이 생성된 각각의 하위 디렉터리를 '지역화 프로젝트(localization project)' 또는 '지역화 폴더(localization folder)'라고 부른다. 지역화 폴더 이름의 맨 마지막에는 항상 .lproj라는 확장자가 붙는다.
- 사용자는 설정Settings 애플리케이션에서 언어와 지역을 설정할 수 있다.
- 다국어를 지원하는 애플리케이션이 이미지, 프로퍼티 리스트, nib와 같은 리소스 파일들을 로딩하려 할때 애플리케이션은 사용자가 설정한 언어와 지역을 확인하고 설정에 맞는 지역화 폴더를 찾는다. 만일 폴더를 찾았다면 기본으로 설정된 언어 대신에 현재 설정된 언어에 필요한 리소스 파일들을 로딩할 것이다.
- 사용자가 언어와 지역을 프랑스어와 프랑스로 선택했다면 애플리케이션은 fr_FR.lproj라는 이름의 지역화 폴더를 찾을 것이다. fr_FR에서 앞의 두 글자는 프랑스어의 ISO 국가 코드를 의미한다. 밑줄 뒤에 따라오는 두 글자는 프랑스의 2자리 ISO코드를 의미한다.
- 애플리케이션이 fr_FR.lproj라는 이름의 지역화 폴더를 찾을 수 없다면, fre_FR이나 fra_FR이라는 이름의 지역화 폴더를 찾을 것이다.
- 만일 애플리케이션이 지역화 폴더를 찾지 못하면 지역 코드를 무시하고 언어 코드로만 일치하는 지역화 폴더를 찾으려 할 것이다. 예를 들어, 프랑스와 프랑스어로 설정되어 있을 경우 애플리케이션은 fr.lproj라는 이름의 지역화 폴더를 찾을 것이다.
- 애플리케이션이 언어/지역으로 구성된 지역화 폴더나 언어로만 구성된 지역화 폴더를 찾지 못했다면, 개발 시에 기본으로 설정된 언어의 리소스 파일들을 사용할 것이다. 반대로 설정된 언어에 적합한 지역화 폴더를 찾았다면, 필요한 리소스 파일들을 지역화 폴더에서 먼저 찾을 것이다. 예를 들어 여러분이 imageNamed:를 사용하여 UIImage객체에 이미지를 로딩한다면, 애플리케이션은 먼저 지역화 프로젝트 안에서 명시된 이름의 이미지 파일을 찾을 것이다. 이미지 파일을 찾으면 그 이미지를 사용할 것이고, 찾지 못한다면 기본 언어 폴더의 이미지 파일을 사용할 것이다.
- 만일 애플리케이션이 fr_FR.lproj와 fr.lproj라는 이름을 사용하는 2개의 지역화 폴더를 가지고 있다면, 좀 더 구체적인 이름을 사용하는 지역화 폴더의 리소스 파일들을 먼저 사용할 것이다. 이 경우에는 fr_FR.lproj가 이에 해당된다.
- 애플리케이션이 fr_FR.lproj폴더에서 리소스 파일들을 찾지 못하면 fr.lproj폴더에서 리소스 파일들을 찾으려고 할 것이다. 이러한 특징을 활용하면 특정 언어에서 지역에 관계없이 공통적으로 사용되는 표현들은 fr.lproj와 같은 형식의 폴더에 저장하고, 지역에 따라 표현이 다른 텍스트나 이미지 파일들은 fr_FR.lproj와 같은 형식을 사용하는 지역화 폴더에 저장하면된다.

* 문자열 파일 사용하기
- 만일 다국어 지원하는 애플리케이션을 만들 계획이라면 애플리케이션의 경고창이나 메시지 전달에 사용될 텍스트들을 소스코드 안에 직접 작성해서는 안 된다.
- 가장 좋은 방법은 이러한 문자열들을 '문자열파일'이라고 부르는 텍스트 파일에 저장하는 것이다. 문자열 파일은 주석이 달려 있는 한 쌍의 문자열의 리스트를 저장하고 있는 유니코드(UTF-16)텍스트 파일이다.
/* Used to ask the user his/her first name */
"First Name" = "First Name";

/* Used to get the user's last name */
"Last Name" = "Last Name";

/* Used to ask the user's birth date */
"Birthday" = "Birthday"

- /* 와 */ 사이의 글자들은 주석이며 애플리케이션에서 사용되지 않고 안전하게 제외될 것이다.
- 문자열 리스트의 각 라인에 같은 문자열이 두 번씩 표기된 것에 주목하자. 이퀄 기호(=)의 왼쪽 문자열은 키를 나타내며, 설정된 언어에 상관없이 동일한 값을 유지한다. 이퀄 기호의 오른쪽 문자열은 특정 언어로 번역된 문자열이다. 만일 프랑스어로 지역화된 문자열 파일을 열어보면 아래와 같을 것이다.
/* Used to ask the user his/her first name */
"First Name" = "Prenom";

/* Used to get the user's last name */
"Last Name" = "Nom de famille";

/* Used to ask the user's birth date */
"Birthday" = "anniversaire"

* 문자열 파일 만들기
- 실제로 여러분이 문자열 파일을 직접 만들 필요는 없다. 대신 코드 안에 지역화해야 할 모든 텍스트 문자열을 특정 매크로로 감싸는 작업을 해야 한다. 코드를 작성하고 지역화 할 준비가 끝나면 genstrings라는 콘솔 프로그램을 실행할 것이다. 이 프로그램은 매크로를 찾기 위해 모든 코드 파일들을 검색하고, 그 중 필욯나 문자열들을 추출하여 지역화하는 데 사용할 문자열 파일에 저장한다.
- 일반적인 문자열 선언문은 아래와 같이 사용된다.
NSString *myString = @"First Name";

- 이 문자열을 추출하려면 아래와 같이 수정해야 한다.
NSString *myString = NSLocalizedString(@"First Name", @"Used to ask the user his/her first name");

- NSLocalizedString 매크로는 2개의 인자를 사용한다. 첫 번째 인자는 기본 언어로 사용할 문자열 변수이다. 만약 다국어를 지원하지 않는다면 애플리케이션은 이 문자열을 사용할 것이다. 또 다른 인자는 문자열 파일 안의 주석으로 사용된다.
- NSLocalizedString 매크로는 애플리케이션 번들 디렉토리의 지역화 폴더에서 Localizable.strings라는 문자열 파일을 찾는다. 만일 이 파일을 찾지 못하면 자신의 첫 번째 인자로 전달받은 문자열을 반환하고 애플리케이션에는 반환된 문자열이 보여질 것이다.
- NSLocalizedString 매크로가 문자열 파일을 찾으면 첫 번째 인자와 일치하는 문자열 쌍을 찾기 위해 파일의 내용을 검색할 것이다. 안에서 설명했던 예제의 경우에 NSLocalizedString매크로는 "First Name"을 찾기 위해 문자열 파일을 검색할 것이다. 만일 지역화 폴더에서 일치하는 문자열을 찾지 못했다면, 기본 언어 폴더 안의 문자열 파일을 검색할 것이고, 문자열 파일 안의 문자열을 사용할 것이다. 기본 언어 폴더의 문자열 파일에서도 찾지 못하면 NSLocalizedString 매크로의 첫 번째 인자로 전달된 문자열을 화면 상에 보여줄 것이다.

* 실생활에서의 아이폰 : 애플리케이션 지역화히기
- 애플리케이션의 NSLocale 인스턴스는 사용자의 언어와 지역 정보를 가지고 있다. NSLocale 인스턴스는 애플리케이션의 화면에 표시되는 언어,날짜,통화,시간 정보에 대한 기준으로 사용될 것이다.
- 애플리케이션 상단에 위치한 언어와 지역 정보는 사용자의 로케일로부터 가져온 것이다. 화면의 왼쪽에 배치된 글자들은 nib 파일 안에 설정된 텍스트를 보여주는 정적인 문자열이다. 오른쪽에는 배치된 글자들은 아웃렛을 사용하여 프로그램이 실행될 때 동적으로 변경될 정보들이다. 화면 하단의 기발 이미지는 정적인 UIImageView 객체이다.
- Xcode에서 뷰 기반의 애플리케이션 템플릿을 이용하여 새 프로젝트를 생성하고, LocalizeMe라고 입력한다.  기존 폴더에서 Base Language라는 이름의 폴더가 있으며, 이 폴더에서 icon.png와 flah.png라는 이믈의 파일을 Resources 폴더로 옮긴다.
- Info.plist를 클릭하고 Icon file의 값을 icon.png로 설정한다.

#### LocalizeMeViewController.h ####
#import 

@interface LocalizeMeViewController : UIViewController 
{
	UILabel *localeLabel;
	UILabel *label1;
	UILabel *label2;
	UILabel *label3;
	UILabel *label4;
	UILabel *label5;
}

@property (nonatomic, retain) IBOutlet UILabel *localeLabel;
@property (nonatomic, retain) IBOutlet UILabel *label1;
@property (nonatomic, retain) IBOutlet UILabel *label2;
@property (nonatomic, retain) IBOutlet UILabel *label3;
@property (nonatomic, retain) IBOutlet UILabel *label4;
@property (nonatomic, retain) IBOutlet UILabel *label5;

@end

- LocalizeMeViewController.xib 파일을 더블클릭하여 인터페이스 빌더를 실행한다.
- 라이브러리 창에서 레이블(Label) 하나를 드래그하여 뷰 윈도우의 상단에 배치한다. 왼쪽 블루 가이드 라인에서 오른쪽 블루 가이드 라인까지 뷰의 전체 너비를 차지할 수 있도록 레이블의 크기를 조절한다. 레이블을 선택하고 레이블의 글자를 진하게 만들고 텍스트를 가운데 정렬한다. 텍스트 색상을 밝은 파랑으로 변경한다. 속성 인스펙터 창의 Adjust to fit 옵션이 설정되었다면, 텍스트 크기가 너무 크더라도 크기는 자동으로 저절될 것이다.
- File's Owner 아이콘에서 컨트롤-드래그하여 레이블 위로 끌어온 뒤에 localeLabel 아웃렛을 선택한다.
- 라이브러리 창에서 레이블 5개를 드래그하고, 레이블들을 블루 가이드 라인을 사용하여 왼쪽 가장자리에 나란히 붙인다.
- 맨 위의 레이블을 더블클릭하고, Label에서 Onw으로 변경하다. 다음으로 마지막 Five까지 순서대로 변경한다.
- 라이브러리 창에서 5개의 레이블을 드래그하여 오른쪽 측면에 나란히 배치한다. 텍스트 정렬을 오른쪽으로 변경하고 여유롭게 크기를 조절한다. 그런다음 File's Owner아이콘을 각각의 5개의 레이블로 컨트롤 드래그하여 레이블의 아웃렛으로 연결한다. 해당 레이블에 텍스트는 삭제한다.
- 라이브러리 창에서 이미지 뷰(Image View)를 드래그하여 뷰의 하단에 배치한다. 속성 인스펙터창에서 View의 Image항목을 flag.png로 변경하고, 이미지의 너비가 왼쪽 블루 가이드 라인과 오른쪽 블루 가이드 라인에 맞춰질 수 있도록 이미지 뷰의 크기를 조절한다. 다음으로 Mode를 Center에서 Aspect Fit으로 변경한다. 이 옵션을 설정하면 이미지 뷰에 붙여진 이미지의 크기를 자동으로 조절해서 이미지 뷰의 크기에 맞게 해주며 이미지의 크기가 조절되어도 이미지의 비율은 그대로 유지될 것이다.

#### LocalizeMeViewController.m ####
#import "LocalizeMeViewController.h"

@implementation LocalizeMeViewController
@synthesize localeLabel;
@synthesize label1;
@synthesize label2;
@synthesize label3;
@synthesize label4;
@synthesize label5;

-(void)viewDidLoad
{
	// 현재 로케일 정보를 얻기 위해 NSLoale인스턴스를 생성
	// NSLocale의 인스턴스 객체를 사용하면 설정 애플리케이션에 설정된 언어와 지역에 대한 정보를 얻을수 있다.
	// NSLocale 클래스는 딕셔너리처럼 키-밸류 형태로 사용할수 있다.
	// NSLocale에는 현재 사용자가 설정한 언어/지역 정보와 통화,날짜 포맷 등의 정보가 들어 있다.
	NSLocale *locale = [NSLocale currentLocale];
	
	// locale인스턴스를 통해 식별자를 가져오는 코드.
	// 이 식별자는 현재 로케일에 대한 언어/지역을 나타낸다.
	// localeIdentifier의 자료형은 NSString* 이므로 식별자는 문자열의 형태로 사용된다.
	// ex : [locale localeIdentifier] = @"fr_FR"
	// displayNameForKey:value: 메서드는 요청된 키에 대한 밸류를 반환하며 설정된 언어에 따라 반환값이 다르다.
	NSString *displayNameString = [locale displayNameForKey:NSLocaleIdentifier value:[locale localeIdentifier]];
	localeLabel.text = displayNameString;
	
	
	label1.text = NSLocalizedString(@"One", @"The Number 1");
	label2.text = NSLocalizedString(@"Two", @"The Number 2");
	label3.text = NSLocalizedString(@"Three", @"The Number 3");
	label4.text = NSLocalizedString(@"Four", @"The Number 4");
	label5.text = NSLocalizedString(@"Five", @"The Number 5");
	
	[super viewDidLoad];
	
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
}

- (void)viewDidUnload {
	self.localeLabel = nil;
	self.label1 = nil;
	self.label2 = nil;
	self.label3 = nil;
	self.label4 = nil;
	self.label5 = nil;
	[super viewDidUnload];
}


- (void)dealloc {
	[localeLabel release];
	[label1 release];
	[label2 release];
	[label3 release];
	[label4 release];
	[label5 release];
    [super dealloc];
}

@end

* Nib 파일 지역화하기
- 이제 nib 파일을 지역화해보자. 어떤 파일이든 지역화하는 과정은 동일하다.
- Xcode에서 LocalizeMeViewController.xib 파일을 클릭하고, Func+ I 을 눌러서 Info 윈도우를 실행한다. General탭을 클릭하고 이동한다. 그런 다음 윈도우의 왼쪽 아라에 위치한 Make File Localizable 버튼을 클릭한다.
- Make File Localizable 버튼을 클릭하면 윈도우는 Targets탭으로 변경될 것이다. Info 윈도우를 닫고 Xcode의 Group & Files창을 살펴보자 LocalizeMeViewController.xib 파일의 왼쪽에는 삼각형 모양의 아이콘이 있따. 이것을 클릭하면 리스트가 펼쳐지고 하위 목록들이 나타날것이다.

* 지역화된 프로젝트 구조 살펴보기
- 프로젝트의 LocalizeMeViewControoler.xib는 English라는 하위 항목 하나를 가지고 있다. 이것은 기본 언어이며 자동으로 생성되었다.
- Groups & File창의 LocalizeMeViewController.xib를 클릭하고 Func + I 을 눌러서 Info 윈도우가 나타나도록만든다. General탭의 Add Localization 버튼을 클릭한다. 지역화를 폴더의 이름을 입력받는 창이 나타날것이다. 프랑스어에 대한 지역화를 추가하기 위해 fr이라고 입력한다. 드롭다운 메뉴의 French를 선택해서는 않된다.
- 로케일 처리할 때 언어 코드는 소문자를 사용하지만, 국가 코드는 대문자를 사용한다.
- fr을 입력하고 리턴 키를 누르면 프로젝트 폴더 안에 fr.lproj라는 이름의 새로운 지역화 폴더가 생성될 것이다. 이제 LocalizeMeViewController.xib의 하위 항목으로 English와 fr이 보일 것이다. fr을 더블클릭하여 nib파일이 열리도록 만든다.
- 인터페이스 빌더에 로딩된 nib 파일은 앞에서 만들었던 nib 파일의 복사본이기 때문에 모든 내용이 동일 할 것이다. 이 파일에 수정한 내용들은 프랑스어로 설정한 사용자들에게 보여질 것이다. 왼편에 위치한 레이블들을 더블클릭하고 그것들을 각각 Un,Deux, Trois, Quatre, Cinq로 변경한다. 변경이 끝나면 nib을 저장하고 Xcode로 이동한다.
- 아이폰은 설정 내용을 캐시하여 사용하므로 지금 애플리케이션을 컴파일하고 실행하면 옛날과 동일한 모습만 보일것이다.
- 시뮬레이터에서의 iPhone Simulator 메뉴의 밑에 Reset Content and Settings... 메뉴가 있다. 이 메뉴를 지금 선택해 보자. 설정 애플리케이션을 실행하고 일반을 클릭한다. 그런 다음 다국어(International)메뉴를 클릭한다. 여기에서 언어와 지역에 대한 설정을 변경할 수 있다.
- 지역 포맷(Region Format)을 미국(United States)에서 프랑스(France,프랑스어 French의 하위 항목이다.)로 변경하고, 언어(Language)를 영어(English)에서 프랑스어(Francais)로 변경한다.
- Xcode로 이동하여 Build 메뉴의 Clean을 선택한다.
- Clean을 실행하고 나서 다시 LocalizeMe 애플리케이션을 빌드하고 실행한다. 이번에는 왼쪽에 위치한 글자들이 프랑스어로 보일 것이다.

* 이미지 지역화 하기
:

[iPhone] - 시작하세요! 아이폰 3 프로그래밍 - Part 16. 아이폰 카메라와 포토 라이브러리

iPhone/[위키북스]시작하세요! 아이폰3 프로그래밍 2010. 10. 4. 20:27

* 아이폰 카메라와 포토 라이브러리
- 아이폰은 샌드박스 메커니즘으로 설계되었기 때문에 애플리케이션에서 사진이나 샌드박스 외부에 저장된 데이터에 접근하는 것은 불가능하다. 하지만 이미지 피커(Image Picker)를 사용하면 카메라와 이미지 라이브러리에 접근할 수 있다.
- 피커에 설정된 이미지 소스 안의 이미지들을 선택하여 사용할 수 있게 해주는 객체이다. 일반적으로 이미지 피커는 이미지 리스트를 소스로 사용하지만, 카메라를 소스로 명시하면 카메라로 촬영한 이미지를 전달받는것도 가능한다.

* 이미지 피커와 UIImagePickerController 사용하기
- 이미지 피커의 인터페이스는 UIImagePickerController라는 모달 컨트롤러 클래스를 사용한다. 따라서 UIImagePickerController 클래스의 인스턴스를 생성하고 인스턴스에 델리게이트를 지정해야 한다. 그리고 이미지 소스 타입을 명시한 다음 모달 형태로 지정한다. 그러고 나면 이미지 피커는 아이폰에서 저장한 사진들 중 하나를 선택할 수 있게 모달 창을 띄우거나 카메라를 사용하여 새로운 사진을 촬영할 것이다.
- 사용자가 취소 버튼을 누르지 않으면 카메라로 촬영하거나 라이브러리에서 선택한 이미지는 델리게이트 메서드로 전달될 것이다.
- 이미지가 선택되거나 취소되는 것과는 별개로 델리게이트는 사용자가 애플리케이션의 뷰 화면으로 돌아갈 수 있도록 이미지 피커를 종료시켜야 한다.
- UIImagePickerController를 생성하는 것은 매우 간단하다. 단지 클래스의 인스턴스를 메모리에 할당하고 초기화하면 된다. 하지만 사용자가 카메라로 산진을 촬영하기 전에 프로그램이 실행되고 있는 장치가 카메라를 지원하는지 확인해야 한다. UIImagePickerController 클래스의 메서드를 사용하여 확인할 수 있다.
if([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypePhotoLibrary]) {

- 이 예제는 UIImagePickerControllerSourceTypePhotoLibrary 인자를 사용하여 사용자가 포토 라이브러리의 사진을 선택할 수 있도록 만들었다. 포토 라이브러리가 사용 가능한 상태이면 isSourceTypeAvailable:메서드는 YES로 반환할 것이다.
  • UIImagePickerControllerSourceTypeCamera를 사용하면 내장된 카메라를 사용하여 사진을 촬영할 수 있다.
    촬영된 사진은 델리게이트에게 전달될 것이다.
  • UIImagePickerControllerSourceTypeSavedPhotosAlbum을 사용하면 카메라 룰의 사진들을 선택할 수 있다.

- 카메라가 지원되는 장치에서 애플리케이션이 실행중인지 확인하고 나면, 이미지 피커를 실행하는 것은 어렵지 않다.

UIImagePickerController *picker = [[UIImagePickerController alloc] init];
picker.delegate = self
picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
[self presentModalViewController:pircker animated:YES];
[picker release];

- UIImagePickerController의 인스턴스를 생성하고 설정한 다음 화면에 이미지 피커를 보여주기 위해 presentModalViewController:animated:메서드를 사용하였다.
- presentModalViewController:animated: 메서드는 현재 화면에 그려진 뷰에 대한 뷰 컨트롤러 객체의 presentModalViewController:animated: 메서드를 호출한다면, 뷰 컨트롤러는 모달 창의 형태로 사용자에게 보여질 것이다.

* 이미지 피커 컨트롤러의 델리게이트 구현하기

- 사용자가 이미지 피커 창을 닫았을 때 특정 객체로 노티피케이션이 전달되게 하려면 객체는 UIImagePickerControllerDelegate의 프로토콜을 따라야 한다.
- UIImagePickerControllerDelegate 프로토콜은 imagePickerController:didFinishPickingImage:editingInfo:와 imagePickerControllerDidCancel:메서드를 정의하고 있다.
- imagePickerController:didFinishPickingImage:editingInfo:메서드는 사용자가 사진 촬영에 성공했거나 포토 라이브러리로부터 사진 하나를 선택했을 때 호출된다. 첫번째 인자는 앞에서 생성한 UIImagePickerController의 포인터를 넘겨주는데 사용된다. 두번째 인자는 사용자가 실제 선택한 사진이 저장된 UIImage의 인스턴스이다. 마지막 인자는 이미지 편집기능이 설정되었을 때만 유효한 NSDictionary의 인스턴스이다. 이 딕셔너리의 UIImagePickerControllerOriginalImage키에는 편집되지 않은 원본 이미지가 들어 있다.
-(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingImage:(UIImage *)image editingInfo:(NSDictionary *)editingInfo
{
  UIImage *selectedImage = image;
  UIImage *originalImage = [editingInfo objectForKey:UIImagePickerControllerOriginalImage];
  [picker dismissModalViewControllerAnimated:YES];
}

- editingInfo 딕셔너리의 UIImagePickerControllerCropRect 키에 저장된 NSValue 객체를 사용하여 이미지를 편집할 때 선택한 영역을 가져올수 있다.
- 다음과 같은 방법으로 UIImagePickerControllerCropRect키를 사용하여 NSValue를 CGRect으로 변환할수 있다.
NSValue *cropRect = [editingInfo objectForKey:UIImagePickerControllerCropRect];
CGRect theRect = [cropRect CGRectValue];

- NSValue를 CGRect로 변환하였다면 theRect에는 편집할 때 선택한 영역의 정보가 저장되어 있을 것이다.
- 델리게이트 메서드인 imagePickerControllerDidCancel:은 사진 촬영이나 사진 선택 창에서 취소 버튼을 눌렀을 때 호출된다. 이미지 피커가 imagePickerControllerDidCancel: 델리게이트 메서드를 호출하면 사용자가 이미지를 선택하지 않고 이미지 피커 창을 닫은 것이다.
- UIImagePickerControllerDelegate 프로토콜의 두 메서드는 모두 선택적 메서드이지만 실제로는 선택 사항이 아니라 반드시 구현해야만 하는 메서드이다. 왜냐하면 이미지 피커와 같은 모달 뷰는 사용이 끝나면 반드시 창을 닫아야 하기 때문에 사용자가 이미지 피커의 취소 번튼을 눌렀을 때 특별히 처리할 일이 없다고 하더라도 피커를 종료하는 코드는 반드시 실행되어야 한다. 따라서 프로그램이 정상 동작하려면 imagePickerControllerDidCancel:메서드는 적어도 다음과 같은 내용을 포함해야 한다.
-(void)imagePickerControllerDidCancel:(UIImagePickerController *)picker
{
  [picker dismissModalViewControllerAnimated:YES];
}

* 카메라와 라이브러리 길거리 테스트
- 카메라로 촬영한 사진이나 포토 라이브러리에서 선택한 사진을 이미지 뷰에서 보여주는 기능을 가진 애플리케이션을 만들것이다.
- 만일 카메라가 없는 장치를 사용한다면 "Take Picture"버튼과 "Pick from library"버튼은 숨겨질 것이고 오로지 포토 라이브러리로부터 사진을 선택하는 버튼만을 보여줄것이다.
- Xcode에서 뷰 기반의 애플리케이션 템플릿을 사용하여 새 프로젝트를 하나 생성하고, 프로젝트 이름을 Camera라고 입력한다.
- Camera애플리케이션은 3개의 아웃렛을 필요로 한다. 하나는 이미지 피커에서 선택한 이미지를 화면에 나타낼 때 사용할 이미지 뷰의 포인터이다. 그리고 나머지 2개는 Take New Picture 버튼과 Select from Camera Roll 버튼을 가리키기 위한 포인터이다. 따라서 우리가 사용할 장치가 커메라를 지원 하지 않는다면 아웃렛을 사용하여 이 버튼들을 숨길 수 있다. 또한 2개의 액션 메서드가 필요하다. 하나는 Take New Picture버튼과 Select from Camera Roll 버튼을 위해 사용될 것이다. 또 하나는 사용자가 포토라이브러리로 사진을 선택하는 데 사용될 것이다.

#### CameraViewController.h ####
#import 

// UIImagePickerController는 UINavigationController의 하위 클래스 이므로 두 프로토콜 모두를 따라야 한다.
// UINavigationControllerDelegate의 메서드들은 모두 선택적 메서드이며 애플리케이션이 이미지 피커를
// 사용할 때는 이 메서들이 필요하지 않다. 하지만 컴파일 할때 경고 메시지가 출력되지 않게 하려면 메서드가
// 필요하지 않아도 UINavigationControllerDelegate 프로토콜을 따라야만 한다.
@interface CameraViewController : UIViewController 
{
	// 이미지 피커에서 선택한 이미지를 화면에 나타낼 때 사용할 이미지 뷰의 포인터
	UIImageView *imageView;
	// Take New Picture 버튼을 가리키는 포인터
	UIButton *takePictureButton;
	// Select from Camera Roll 버튼을 가리키는 포인터
	UIButton *selectFromCameraRollButton;
}

@property (nonatomic, retain) IBOutlet UIImageView *imageView;
@property (nonatomic, retain) IBOutlet UIButton *takePictureButton;
@property (nonatomic, retain) IBOutlet UIButton *selectFromCameraRollButton;
-(IBAction)getCameraPicture:(id)sender;
-(IBAction)selectExistingPicture;
@end

* 인터페이스 설계하기
- 라이브러리 창에서 Round Rect Buttons 3개를 드래그하여 View윈도우 위에 놓는다. 그리고 난뒤 버튼을 세로로 하나씩 배치한다.
- 맨 위의 버튼을 더블클릭하고, 'Take New Picture'라고 입력한다.
- 중간의 버튼을 더블클릭하고 'Pick from Camera Roll'이라고 입력한다.
- 하단의 버튼을 더블클릭하고 'Pick from Library'라고 입력한다.
- 라이브러리로부터 이미지 뷰(Image View)를 드래그하여 버튼들의 위쪽에 배치한다. 뷰가 버튼 위의 전체 영역을 감싸도록 늘린다.
- File's Owner아이콘에서 이미지 뷰로 컨트롤-드래그하여 imageView아웃렛을 선택한다. 
- File's Owner아이콘에서 Take New Picture버튼으로 드래그하고 takePictureButton 아웃렛을 선택한다.
- File's Owner아이콘에서 Pick from Camera Roll 버튼으로 컨트롤-드래그 하고, selectFromCameraRollButton 아웃렛을 선택한다.
- Take New Picture버튼을 선택하고 Func + 2를 눌러서 커넥션 인스펙터 창을 띄운다. Touch Up Inside 이벤트로부터 File's Owner로 드래그 하고, getCameraPicture:액션을 선택한다.
- Pick from Camera Roll 버튼을 클릭하고 커넥션 인스펙터의 Touch Up Inside 이벤트로부터 File's Owner로 드래그 한 뒤에 getCameraPicture: 액션을 선택한다.
- Pick from Library 버튼을 선택하고 인스펙터의 Touch Up Inside 이벤트로부터 File's Owner로 드래그하고 selectExistingPicture 액션을 선택한다.

* 카메라 뷰 컨트롤러 구현하기
#### CameraViewController.m ####
#import "CameraViewController.h"

@implementation CameraViewController
@synthesize imageView;
@synthesize takePictureButton;
@synthesize selectFromCameraRollButton;

-(void)viewDidLoad
{
	// 애플리케이션이 실행되는 장치가 카메라를 지원하는지 확인한다.
	if(![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera])
	{
		// 만약 카메라가 없는 장치에서 실행되고 있다면 카메라를 사용하는 기능과 관련된
		// 2개의 버튼을 숨기도록 만들었다.
		takePictureButton.hidden = YES;
		selectFromCameraRollButton.hidden = YES;
	}
}

- (void)didReceiveMemoryWarning {
	// Releases the view if it doesn't have a superview.
    [super didReceiveMemoryWarning];
	
	// Release any cached data, images, etc that aren't in use.
}

- (void)viewDidUnload {
	self.imageView = nil;
	self.takePictureButton = nil;
	self.selectFromCameraRollButton = nil;
	[super viewDidUnload];
}

- (void)dealloc {
	[imageView release];
	[takePictureButton release];
	[selectFromCameraRollButton release];
    [super dealloc];
}

#pragma mark -
-(IBAction)getCameraPicture:(id)sender
{
	// UIImagePickerController 인스턴스를 할당하고 초기화
	UIImagePickerController *picker = [[UIImagePickerController alloc] init];
	// 이미지 피커의 델리게이트에 self를 할당한다.
	picker.delegate = self;
	// 사용자가 이미지를 촬영한 뒤에 편집할 수 있도록 allowsImageEditing에 YES를 할당한다.
	picker.allowsEditing = YES;
	// 사용자가 어떤 버튼을 눌렀는지 확인하여 sourceType을 설정한다.
	// 만일 사용자가 Take New Picture을 눌렀다면 피커가 카메라를 사용할수 있도록 UIImagePickerControllerSourceTypeCamera
	// 사용자가 Pick from camera Roll 버튼을 눌렀다면 UIImagePickerControllerSourceTypeSavedPhotosAlbum
	picker.sourceType = (sender == takePictureButton) ? 
							UIImagePickerControllerSourceTypeCamera :
							UIImagePickerControllerSourceTypeSavedPhotosAlbum;
	
	// 이미지 피커를 모달 윈도우 형태로 실행하고 인스턴스를 해제 한다.
	[self presentModalViewController:picker animated:YES];
	[picker release];
}

-(IBAction)selectExistingPicture
{
	// 만일 사용자가 포토 라이브러리에서 사진을 선택할 수 있는 기능을 제공한다면 이미지 피커를 생성한다.
	if([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypePhotoLibrary])
	{
		UIImagePickerController *picker = [[UIImagePickerController alloc] init];
		picker.delegate = self;
		picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
		[self presentModalViewController:picker animated:YES];
		[picker release];		
	} else {
		// 사용자가 포토 라이브러리를 사용할수 없다면 경고 창을 띄어준다.
		UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error accessing photo library"
														message:@"Devie does not support a photo library"
													   delegate:nil
											 cancelButtonTitle:@"Drat!"
											  otherButtonTitles:nil];
		[alert show];
		[alert release];
	}
}

#pragma mark -
// 델리게이트 메서드
// imagePickerController:didFinishPickingImage:editingInfo: 메서드는 사용자가 이미지 피커의 사용을 마쳤을 때 호출된다.
// 또한, sourceType에 상관없이 사용자가 사진을 선택했을 때도 호출된다.
-(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingImage:(UIImage *)image editingInfo:(NSDictionary *)editingInfo
{
	// imageView에 그려질 이미지가 할당될 것이다.
	imageView.image = image;
	// 사용자가 애플리케이션의 뷰 화면으로 돌아갈수 있도록 이미지 피커를 종료시킨다.
	[picker dismissModalViewControllerAnimated:YES];
}

// 사용자가 취소버튼을 누르면 그 밖의 다른 어떤 동작도 필요하지 않지만, 이미지 피커를 종료시키는 작업을 해야한다.
// 그렇게 되지 않으면 뷰를 가린 채로 이미지 피커가 여전히 화면상에 나타나 있을것이다.
-(void)imagePickerControllerDidCancel:(UIImagePickerController *)picker
{
	[picker dismissModalViewControllerAnimated:YES];
}

@end

:

[iPhone] - 시작하세요! 아이폰 3 프로그래밍 - Part 15. 야호! 가속도센서!

iPhone/[위키북스]시작하세요! 아이폰3 프로그래밍 2010. 9. 15. 20:53

* 야호! 가속도센서!
- 아이폰과 아이팟 터치의 가장 큰 특징 중 하나는 가속도 센서(Accelerometer)를 내장하고 있다는 것이며, 이 작은 센서를 사용하면 아이폰이 정지 상태인지 이동 중인지를 판단할 수 있다.

* 가속도 센서의 물리법칙
- 가속도 센서는 특정 방향으로 작용하는 관성력을 감지하여 가속도와 중력을 측정한다.
- 가속도 센서는 x,y,z의 세방향에 대한 가속도의 크기를 측정할 수 있기 때문에 3차원 공간에서의 움직임이나 중력을 감지하는 것이 가능하다. 결국, 가속도 센서는 x,y축을 기준으로 아이폰이 자동회전 후 정지 상태인지 판단할 수 있을 뿐만 아니라 아이폰이 탁자 위에 놓여져 있는 상태라면 z축을 기준으로 액정 화면이 위와 아래 중 어느 쪽을 향하는지까지도 확인할수 있다.
- 가속도 센서는 중력이 작용하는 범위 안에서 측정된 가속도 값을 제공한다. 그래서 가속도 센서의 반환 값이 1.0이면, 특정 한 방향으로 1g(1g는 자연상태의 중력 가속도이며, 9.8m/s2)의 힘이 작용하고 있음을 나타낸다.
- 만일 아이폰이 움직임 없이 가만히 멈춰 있다면, 지구가 끌어 당기는 방향으로 1g가량의 힘이 작용하고 있는 것이다. 아이폰이 세로 방향으로 세워져 있다면 가속도 센서는 y축의 방향으로 1g의 힘이 작용하고 있다고 알려줄 것이며, 아이폰이 비스듬한 모양으로 정지해 있다면 아이폰의 기울기에 따라 1g의 힘은 각각의 방향으로 분산되어 작용하고 있을 것이다. 예를 들어, 아이폰이 45º의 각도로 정지해 있다면, 1g의 힘은 아이폰을 기준으로 양쪽 방향으로 균등하게 분산되어 작용하고 있을 것이다.
- 한 가지 중요한 점은 가속도 센서는 힘이 위쪽 방향으로 작용할 때 y좌표계를 사용한다는 것이다. 이것은 쿼츠 2D의 y좌표계와는 대칭되기 때문에 쿼츠 2D를 사용하여 가속도를 반영할 때는 y좌표계를 변환하는 작업이 필요하다. 반면, OpenGL ES을 사용하여 가속도 값을 반영할 때는 좌표계를 변환하여 사용할 필요가 없다.


* 가속도 센서 사용하기
- UIAccelerometer클래스는 싱글턴으로 설계되었다. 따라서, 이 클래스의 레퍼런스를 생성하려면 아래와 같이 sharedAccelerometer메서드를 호출해야한다.
UIAccelerometer *accelerometer = [UIAccelerometer sharedAccelerometer];

- 가속도 센서로부터 데이터를 가져오는 방법은 코어 로케이션을 사용하는것과 비슷하다.
- 먼저 UIAccelerometerDelegate 프로토콜을 따르는 클래스를 생성하고, 가속도 센서로부터 데이터를 전달 받을 메서드를 구현한다. 그런다음 클래스의 인스턴스를 생성하여 인스턴스를 가속도 센서의 델리게이트로 할당한다.
- 델리게이트로 할당할 때 업데이트 주기를 초 단위로 명시할 필요가 있다. 비록 업데이트 횟수나 업데이트간격이 정확하게 지켜진다는 보장은 없지만 가속도 센서는 초당 100회에 이르는 폴링이 가능하다. 아래와 같은 방법을 사용하면 델리게이트를 할당하고 폴링 주기를 초당 60회로 설정할수 있다.
accelerometer.delegate = self;
accelerometer.updateInterval = 1.0f/60.0f;

- 델리게이트 객체를 할당하고 폴링 주기를 설정하고 나면, accelerometer:didAccelerate:메서드를 구현하여 가속도 센서가 델리게이트를 업데이트 할 수 있도록 만들어야 한다.
- accelerometer:didAccelerate:메서드는 2개의 인자를 가지고 있다. 첫번째 인자는 UIAccelerometer인스턴스 레퍼런스이며, 두번째 인자는 가속도 센서로부터 받은 실제 데이터를 가지고 있는 UIAcceleration객체의 인스턴스이다.

* UIAcceleration
- UIAcceleration는 x,y,z라는 3개의 프로퍼티를 가지고 있으며, 각각의 프로퍼티는 부동 소수점 값을 저장하고 있다. 만일 특정 프로퍼티의 값이 0이면, 가속도 센서가 프로퍼티가 나타내는 방향으로 어떠한 움직임도 감지하지 못했다는 것을 의미한다. 반면에 양(+)이나 음(-)의 값을 가진다면 그 방향으로 가속도가 발생했음을 의미한다. 예를 들어 y가 음수이면 아래 방향으로 당기는 힘이 작용한 것이고, 아마도 아이폰은 똑바로 세워진 채로 정지해 있을 것이다. 반대로 y가 양수이면 아이폰의 위쪽 방향으로 힘이 작용한 것이며, 아이폰은 거꾸로 세워져 있거나 위쪽 방향으로 이동하고 있음을 나타내는 것이다.

* accelerometer:didAccelerate:메서드 구현하기
- 가속도 센서의 정보를 받을 델리게이트 클래스는 accelerometer:didAccelerate:메서드가 구현되어 있어야 한다.
- UILabel을 사용하여 가속도 값을 표시하려면 accelerometer:didAccelerate:메서드에 아래와 같은 코드를 구현해야 한다.
-(void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration
{
  NSString *newText = [[NSString alloc] initWithFormat:@"x: %g\t y:%g\t z:%g", acceleration.x, acceleration.y, acceleration.z];
  label.text = newText;
  [newText release];
}

- 앞에서 설정한 updateInterval값에 따라 이 메서드의 호출 빈도가 결정된다.

* 흔들기 인식하기
- 체스처와 마찬가지로 흔들기 인식은 애플리케이션에 사용자의 명령을 전달하는 수단으로 사용될 것이다.
- 흔들기 인식하는 것은 어려지 않다. 흔들기를 인식하기 위해서는 세 방향으로 작용하는 가속도 중 하나가 기준이 되는 값 이상인지를 확인하면된다. 특정 방향으로 1.3gs 가량의 가속도가 발생하는 것은 흔한 일이지만, 인의적인 힘을 가하지 않고서는 발생시킬 수 없는 비교적 큰 힘이다.
- 흔들기 인식을 구현할 때 가변운 흔들기를 인식하고 가속도가 1.5보다 큰 값인지 확인하고, 강한 흔들기를 인식하고 싶다면 가속도가 2.0보다 큰 값인지 확인하면된다. 가속도의 크기를 비교하는 방법은 다음과 같다.
-(void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration
{
  if(fabsf(acceleration.x) > 2.0 || fabsf(acceleration.y) > 2.0 || fabsf(acceleration.z) > 2.0)
  {
    // Do Something here....
  }
}

- 다음과 같이 일정 횟수 이상의 흔들기가 발생하였는지 확인하는 코드를 추가한다면, 좀 더 정교한 흔들기 인식을 구현할 수 있을 것이다.
-(void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration
{
  static NSInteger shakeCount = 0;
  static NSDate *shakeStart;

  NSDate *now = [[NSDate alloc] init];
  NSDate *checkDate = [[NSDate alloc] initWithTimeInterval:1.5f sinceDate:shakeStart];

  if([now compare:checkDate] == NSOrderedDescending || shakeCount == nil)
  {
    shakeCount = 0;
    [shakeStart release];
    shakeStart = [[NSDate alloc] init];
  }
  [now release];
  [checkDate release];

  if(fabsf(acceleration.x) > 2.0 || fabsf(acceleration.y) > 2.0 || fabsf(acceleration.z) > 2.0)
  {
    shakeCount++;
    if(shakeCount > 4)
    {
      shakeCount = 0;
      [shakeStart release];
      shakeStart = [[NSDate alloc] init];
    }
  }
}

* 가속도 센서를 통한 방향 제어
- 가속도 센서를 컨트롤러로 사용할 때 가장 어려운 점은, 델리게이트 메서드가 설정된 시간만큰 일정한 간격으로 호출되지 않는다는 점이다. 만일 가속도 센서가 델리게이트를 1초에 60회 업데이트하도록 설정하면, 업데이트가 1초에 60번 이하로 발생하는 것뿐이지, 반드시 1초 간격으로 한번씩 업데이트가 일어나는 것은 아니다.
- 안타깝게도 시뮬레이터에는 가속도 센서가 없어서 예제 애플리케이션을 테스트할 수 없다.

* 흔들어서 깨트리기
- 사실 아이폰의 화면을 실제로 깨트릴 것은 아니고, 흔들기를 인식하여 아이폰의 화면이 깨진 것과 같은 시각과 청각효과를 동시에 보여줄 것이다.

* 화면 깨뜨리기 코드
- Xcode에서 뷰 기반의 템플릿을 사용하여 새 프로젝트를 하나 생성하고, ShakeAndBreak라고 입력한다.
- 샘플 소스에서 home.png, homebroken.png. glass.wav파일을 찾아서 Resources폴더에 카피한다.
- Resources 폴더를 펼쳐 ShakeAndBreak-Info.plist파일을 클릭한다. 애플리케이션에서 상태 표시줄(status Bar)를 사용하지 않으려면, 프로퍼티 리스트에 새 프로퍼티를 추가해야 한다. Information Property list를 클릭하고 맨 끝에 보이는 버튼을 마우스로 클릭하여 하위 항목을 하나 추가한다. 새로 생선된 프로퍼티의 Key를 UIStatusBarHidden으로 변경하고 체크 박스를 체크한다.
- 내 환경은 sdk 4를 사용해서 그런지 위에 항목이 없다 그래서. Status bar is initially hidden를 넣어주고 체크 박스를 체크했다.
- Icon file 프로퍼티에 icon.png라고 입력한다.

#### ShakeAndBreakViewController.h ####
#import 
#import 

#define kAccelerationThreshold 2.2
#define kUpdateInterval (1.0f/10.0f)

@interface ShakeAndBreakViewController : UIViewController 
{
	// 화면에 나타낼 이미지를 변경할 수 있도록 이미지 뷰를 가리키는 아웃렛
	UIImageView *imageView;
	// 화면의 초기화 상태를 저장하기 위한 Boolean 변수
	BOOL brokenScreenShowing;
	// 사운드 파일을 참조하기 위한 사운드 ID
	SystemSoundID soundID;
	// 메모리에 로딩한 이미지를 가리키기위한 UIImage
	UIImage *fixed;
	UIImage *broken;
}

@property (nonatomic, retain) IBOutlet UIImageView *imageView;
@property (nonatomic, retain) UIImage *fixed;
@property (nonatomic, retain) UIImage *broken;

@end

#### ShakeAndBreakViewController.m ####
#import "ShakeAndBreakViewController.h"

@implementation ShakeAndBreakViewController
@synthesize imageView;
@synthesize fixed;
@synthesize broken;

-(void)viewDidLoad
{
	// 가속도 센서의 인스턴스를 생성
	UIAccelerometer *accel = [UIAccelerometer sharedAccelerometer];
	// 자기 자신을 가속도 센서의 델리게이트로 설정
	accel.delegate = self;
	// 업데이트 빈도를 설정
	accel.updateInterval = kUpdateInterval;
	
	// glass.wav 파일을 로딩하고, 할당된 식별자를 soundID 인스턴스 변수에 저장
	NSString *path = [[NSBundle mainBundle] pathForResource:@"glass" ofType:@"wav"];
	AudioServicesCreateSystemSoundID((CFURLRef)[NSURL fileURLWithPath:path],&soundID);
	
	// 이미지 파일을 로딩
	self.fixed = [UIImage imageNamed:@"home.png"];
	self.broken = [UIImage imageNamed:@"homebroken.png"];
	
	// 유리가 깨지지 않은 그림을 화면에 보여주기 위해 imageView를 설정하고
	// 화면을 재설정 하지 않기 위해 brokenScreenShowing에 NO를 할당
	imageView.image = fixed;
	brokenScreenShowing = NO;
	
	// 아래는 Motion Handling
	[self.view becomeFirstResponder];
}

// 아래는 Motion Handling
- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    [self becomeFirstResponder];
}

// 아래는 Motion Handling
- (BOOL) canBecomeFirstResponder {
    return YES;
}

#pragma mark -
-(void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration
{
	// brokenScreenShowing이 NO라면, 화면에는 이미 유리가 깨진 이미지가
	// 보여지고 있으므로 메서드는 아무 일도 하지 않는다.
	if(!brokenScreenShowing)
	{
		// 세 방향중 어느 한 방향이라도 조건에 만족하는 힘이 작용하였다면, 
		// 이미지 뷰에 유리가 깨진 이미지가 그려지고 효과음이 재생될 것이다.
		// 그러고 나서 화면이 초기화되기 전에 이미지와 사운드 파일이 한번 더 재생되는 것을 막기 위해 
		// brokenScreenShowing을 YES로 설정
		if(acceleration.x > kAccelerationThreshold || acceleration.y > kAccelerationThreshold
		   || acceleration.z > kAccelerationThreshold)
		{
			imageView.image  = broken;
			AudioServicesPlaySystemSound(soundID);
			brokenScreenShowing = YES;
		}
	}
}

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
	imageView.image = fixed;
	brokenScreenShowing = NO;
}

#pragma mark Motion Handling
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event {
    imageView.image = broken;
    AudioServicesPlaySystemSound (soundID);
    brokenScreenShowing = YES;    
    [super motionEnded:motion withEvent:event];
}

- (void)didReceiveMemoryWarning {
	[super didReceiveMemoryWarning];
}

- (void)viewDidUnload {
	self.imageView = nil;
	self.fixed = nil;
	self.broken = nil;
	[super viewDidUnload];
}

- (void)dealloc {
	[imageView dealloc];
	[fixed dealloc];
	[broken dealloc];
    [super dealloc];
}

@end

- 마지막으로 AudioToolbox.framework를 추가하여 사운드 파일을 재생할 수 있게 만든다.

* 구슬 굴리기 프로그램
- 애니메이션 처리를 위해서 쿼츠 2D를 사용할 것이다.
- Xcode에서 뷰 기반의 템플릿을 사용하여 새 프로젝트를 만들고 Ball이라고 입력한다. 예제 소스 폴더 안에 있는 15 Ball 폴더를 열면 ball.png라는 이미지 파일을 찾을 수 있다. 이 파일을 Resources폴더에 추가한다.
- Classes폴더를 클릭하고 New File로 Cocoa Touch Class카테고리의 Objective-C class와 Subclass of 팝업 메뉴의 UIView를 선택한다. 그런 다음 파일 이름을 BallView.m로 변경한다. BallView.m파일을 생성하면 헤더 파일도 자동으로 생성되므로, 확인한다.
- BallViewController.xib를 더블클릭하면, 인터페이스 빌더가 실행되고 파일이 열릴 것이다. 뷰 아이콘을 클릭하고 아이덴터티 인스펙터 창을 열어서, 뷰 클래스를 UIView에서 VallView로 변경한다. View항목의 Backgroup를 검정색으로 변경한다. 그 다음 File's Owner아이콘에서 Ball View으로 컨트롤-드래그하고, 컨트롤러와 뷰를 연결하기 위해 view아웃렛을 선택한다.

#### BallViewController.h ####
#import 
#define kUpdateInterVal (1.0f/60.0f)

@interface BallViewController : UIViewController 
{

}

@end

#### BallViewController.m ####
#import "BallViewController.h"
#import "BallView.h"

@implementation BallViewController

-(void)viewDidLoad
{
	UIAccelerometer *accelerometer = [UIAccelerometer sharedAccelerometer];
	accelerometer.delegate = self;
	accelerometer.updateInterval = kUpdateInterVal;
	[super viewDidLoad];	
}

#pragma mark -
-(void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration
{
	[(BallView *)self.view setAcceleration:acceleration];
	[(BallView *)self.view draw];
}


- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
}

- (void)viewDidUnload {
}

- (void)dealloc {
    [super dealloc];
}

@end

#### BallView.h ####
#import 

#define kVelocityMultiplier 500

@interface BallView : UIView 
{
	// 화면을 이동할 구슬 이미지를 참조하는데 사용
	UIImage *image;
	// 현재의 위치를 저장한다.
	CGPoint currentPoint;
	// 구슬의 이전 위치와 현재 위치를 둘러싸는 업데이트 영역을 정의하기 위해 마지막 위치를 저장한다.
	CGPoint previousPoint;
	// 컨트롤러 클래스를 통해 넘겨받은 인자를 이 프로퍼티에 저장한다.
	UIAcceleration *acceleration;
	// 구슬의 현재 속도를 저장히기 위해 변수 2개를 선언한다. 
	// 현재 속도 = 이전 속도 + 가속된 속도
	CGFloat ballXVelocity;
	CGFloat ballYVelocity;
}

@property (nonatomic, retain) UIImage *image;
@property CGPoint currentPoint;
@property CGPoint previousPoint;
@property (nonatomic, retain) UIAcceleration *acceleration;
@property CGFloat ballXVelocity;
@property CGFloat ballYVelocity;
-(void)draw;
@end

#### BallView.m ####
#import "BallView.h"

@implementation BallView
@synthesize image;
@synthesize currentPoint;
@synthesize previousPoint;
@synthesize acceleration;
@synthesize ballXVelocity;
@synthesize ballYVelocity;

// Nib로부터 뷰를 로딩할때, initWithCoder:메서드를 재 호출하였다.
// 이 클래스의 init이나 initWithFrame:메서드는 절대 호출되지 않는다.
// Nib파일들은 아카이브 객체들을 가지고 있기 때문에,Nib로부터 로딩되는 모든 객체는
// initWithCoder:메서드를 사용하여 초기활될 것이다.
-(id)initWithCoder:(NSCoder *)coder
{
	if(self = [super initWithCoder:coder])
	{
		//ball.png이미지를 로딩, 뷰의 중앙 좌료를 계산하여 그것을 구슬의 시작점으로 설정
		self.image = [UIImage imageNamed:@"ball.png"];
		self.currentPoint = CGPointMake((self.bounds.size.width / 2.0f) + (image.size.width / 2.0f), 
										(self.bounds.size.height / 2.0f) + (image.size.height /2.0f));
		
		// x,y축 방향에 대한 속도를 0으로 설정
		ballXVelocity = 0.0f;
		ballYVelocity = 0.0f;		
	}
	return self;	
}

#pragma mark -
-(CGPoint)currentPoint
{
	return currentPoint;
}

-(void)setCurrentPoint:(CGPoint)newPoint
{
	previousPoint = currentPoint;
	currentPoint = newPoint;
	
	// 화면의 경계선을 확인하는 작업
	// 만일 구슬의 x,y좌표 둘 중 어느 한쪽이라도 0보다 작어나 화면의 너비나 높이보다 크다면
	// (이미지의 너비와 높이를 계산에 포함시킨 상태에서), 가속도가 더 이상 작용하지 않는 상태로 처리하였다.
	if(currentPoint.x < 0)
	{
		currentPoint.x = 0;
		ballXVelocity = 0;
	}
	
	if(currentPoint.y < 0)
	{
		currentPoint.y = 0;
		ballYVelocity = 0;
	}
	
	if(currentPoint.x > self.bounds.size.width - image.size.width)
	{
		currentPoint.x = self.bounds.size.width - image.size.width;
		ballXVelocity = 0;
	}
	
	if(currentPoint.y > self.bounds.size.height - image.size.height)
	{
		currentPoint.y = self.bounds.size.height - image.size.height;
		ballYVelocity = 0;
	}
	
	// 2개의 CGRects 변수를 계산한다.
	// 하나의 직사각형은 새로운 이미지가 그려질 영역을 정의하고,
	// 다른 직사각형은 마지막으로 그려진 영역을 정의한다.
	// 2개의 직사각형 영역을 사용하여, 이전 영역에 그려진 구슬의 이미지를 지우고 새 영역에 구슬 이미지를 그릴것이다.
	CGRect currentImageRect = CGRectMake(currentPoint.x, currentPoint.y,
										 currentPoint.x + image.size.width,
										 currentPoint.y + image.size.height);
	
	CGRect previousImageRect = CGRectMake(previousPoint.x, previousPoint.y,
										  previousPoint.x + image.size.width,
										  previousPoint.y + image.size.height);
	
	// 2개의 직사각형을 결합한 새로운 직사각형을 만들었다.
	// 이 직사각형은 뷰에 다시 그려질 영역을 정의하기 위해 setNeedsDisplayInRect:의 인자로 사용된다.
	[self setNeedsDisplayInRect:CGRectUnion(currentImageRect, previousImageRect)];
		
}

// draw 메서드는 구슬의 새 위치를 정확히 계산하는데 사용된다.
// 이 메서드는 컨트롤러 클래스의 accelerometer메서드가 acceleration인자를 BallView 클래스에게
// 전달한 후에 accelerometer 메서드에게 호출된다.
-(void)draw
{
	// 먼저 NSDate 변수 하나를 static으로 선언한다.
	// 이변수는 마지막으로 draw메서드가 호출된 후부터 얼마나 지났는지를 체크하는데 사용된다.
	static NSDate *lastDrawTime;
	
	// draw메서드를 처음 호출할 때 lastDrawTime은 nil이며, lastDrawTime에 날짜가 
	// 할당되지 않아 메서드는 아무 일도 하지 않을 것이다. 
	// 업데이트는 1/60초의 주기로 발생하기 때문에, 처음 호출될 때 아무런 동작을 하지 않아도 알아채는 사람은 없을 것이다.
	if(lastDrawTime != nil)
	{
		// draw메서드가 호출될 때마다 매서드가 호출된 마지막 시간으로부터 현재 시간을 계산할 것이다.
		// lastDrawTime이 과거 시간이기 때문에 timeIntervalSineNow는  
		// lastDrawTime에서 현재 시간을 뺀 음수 값을 반활 할 것이다. 
		// 따라서 반환된 값에 마이너스(-)를 붙여서 secondsSineLastDraw에 저장하였다.		
		NSTimeInterval secondsSinceLastDraw = -([lastDrawTime timeIntervalSinceNow]);
		
		// 가속도를 현재 속도에 더하여 x,y방향의 속도를 계산하였다.
		// 가속도가 특정 시간 동안 지속되도록 가속도 값에 secondsSinceLastDraw를 곱하였다.
		ballYVelocity = ballYVelocity + -(acceleration.y * secondsSinceLastDraw);		
		ballXVelocity = ballXVelocity + acceleration.x * secondsSinceLastDraw;
	
		// draw메서드가 마지막으로 호출된 시점으로부터 지금까지의 속도 값을 사용하여 픽셀 단위의 
		// 위치 변화량을 계산하였다.
		// 구슬의 움직임을 자연스럽게 만들기 위해 속도 값과 경과 시간에 500을 곱하였다.
		// 만일 500을 곱하지 ㅇ낳는다면 구슬이 끈끈이에 달라붙은 것처럼 가속도가 엄청나게 느릴 것이다.
		CGFloat xAcceleration = secondsSinceLastDraw * ballXVelocity * 500;
		CGFloat yAcceleration = secondsSinceLastDraw * ballYVelocity * 500;
		
		self.currentPoint = CGPointMake(self.currentPoint.x + xAcceleration, self.currentPoint.y + yAcceleration);
	}
	
	// 현재 시간을 lastDrawTime를 업테이트 하였다.
	[lastDrawTime release];
	lastDrawTime = [[NSDate alloc] init];
		
}

// initWithCoder:메서드에서 로딩했던 구슬 이미지를 뷰에 그리는 기능만을 구현
- (void)drawRect:(CGRect)rect {
	[image drawAtPoint:currentPoint];
}

- (void)dealloc {
	[image release];
	[acceleration release];
    [super dealloc];
}

@end

:

[iPhone] - 시작하세요! 아이폰 3 프로그래밍 - Part 14. 여기가 어디지? 코어 로케이션을 이용한 길 찾기

iPhone/[위키북스]시작하세요! 아이폰3 프로그래밍 2010. 9. 14. 16:13

* 여기가 어디지? 코어 로케이션을 이용한 길 찾기
- 아이콘은 코어 로케이션 프레임워크를 사용하여 위치 정보를 제공한다. 코어 로케이션은 현재 위치를 결정하기 위해 3가지 기술을 사용한다. 그것은 바로 GPS, 휴대폰 기지국의 삼각 측량법, 와이파이 위치 확인 서비스(WPS)
- 세 기술 모두 많은 양의 전력을 필요로 하기 때문에, 코어 로케이션을 사용할 때는 배터리 소모에 주의해야 한다. 위치 정보가 정말 필요할 때 배터리가 부족하여 애플리케이션이 폴링하지 않을수 있기 때문이다.코어 로케이션을 사용하여 위치 정보를 수신할 때 정밀도를 설정할 수 있다. 정밀도를 조절하면 불 필요한 배터리 소모를 막을 수 있다.
- 코어 로케이션이 사용하는 기술들은 애플리케이션 레이어에게 가려져 있다. 그래서 애플리케이션이 코어 로케이션 라이브러리를 호출할 때 GPS, 삼각측량법, WPS중 어느 것을 사용하라고 지정할 수 없다.

* 로케이션 매니저
- 메인 클래스는 CLLocationManager이며, 로케이션 매니저를 생성하는데 사용한다. 다음과 같이 로케이션 매니저의 인스턴스를 만들어야 한다.
CLLocationManager *locationManager = [[CLLocationManager alloc] init];

- 이 코드는 로케이션 매니저의 인스턴스를 생성하지만, 폴링을 시작하고 위치 정보를 갱신하지 않는다. 폴링을 시작하려면 먼저 델리케이트 객체를 만들고, 델리케이트 객체를 로케이션 매니저에 할당해야 한다. 로케이션 매니저는 위치 정보를 수신하면 델리케이트 메서드를 호출한다.
- 델리케이트로 사용되는 객체는 반드시 CLLocationManagerDelegate 프로토콜을 따라야 한다.

* 정밀도 설정하기
- 델리케이트를 설정한 뒤에는 정밀도를 설정한다.
- 정밀도를 높게 설정하면 많은 전력이 소모된다는 것을 기억하자.
locationManager.delegate = self;
locationManager.desiredAccuracy = kCLLocationAccuracyBest;

- CLLocationAccuracy변수를 사용하여 정밀도를 설정하였다. CLLocationAccuacy는 double로 정의된 자료형이다.
- 정밀도는 미터 meter 단위를 사용하며, desiredAccuracy를 10으로 설정하면 10미터의 오차범위 안에서 현재 위치를 결정한다.
- kCLLocationAccuracyBest외에도 kCLLocationAccuracyNearestTenMeter, kCLLocationAccuracyHundredMeters, kCLLocationAccuracyKilometer, kCLLocationAccuracyThreeKilometers와 같은 값들을 사용할수 있다.

* 디스턴스 필터 설정하기
- 로케이션 매니저는 기본적으로 위치가 바뀌면 델리게이트에게 통보한다. 로케이션 매니저의 디스턴스 필터를 설정하면, 위치가 변경될 때 마다 델리게이트 메서드를 호출하는것이 아니라 필터에 명시된 거리 이상을 이동하였을 때만 호출한다. 또한 디스턴스 필터는 미터 단위를 사용한다.
0 디스턴스 필터 값이 1000으로 할당되어 있으면, 로케이션 매니저는 1000미터 이상 이동할 때까지 델리게이트에 통보하지 않는다.
locationManager.distanceFilter = 1000.0f;

-  로케이션 매니저의 필터를 해제하고 싶다면, 다음과 같이 kCLDistanceFilterNone상수를 사용할 수 있다.
locationManager.distanceFilter = kCLDistanceFilterNone;

* 로케이션 매니저 동작시키기
- 위치 정보를 폴링할 준비가 되었다면, 로케이션 매니저를 동작시킨다. 로케이션 매니저가 동작하면 위치 정보를 수신할 것이고, 위치 정보가 갱신될 때마다 델리게이트 메서드를 호출할 것이다.
[locationManager startUpdatingLocation];

* 효율적으로 로케이션 매니저 사용하기
- 위치 정보를 지속적으로 수신하지 않고, 단순히 현재 위치를 조회하고 싶다면 위치 정보를 얻은 후에 곧바로 로케이션 매니저를 중단시키면된다.
[locationManager stopUpdatingLocation];

* 로케이션 매니저 델리게이트
- 로케이션 매니저의 델리케이트는 반드시 CLLocationManagerDelegate 프로토콜을 따라야 한다.
- CLLocationManagerDelegate 프로토콜은 2개의 선택적 메서드를 정의하며, 이 메서드 중 하나는 현재의 위치가 결정되거나 위치가 변경되었을 때 로케이션 매니저에 의해 호출된다. 또 다른 메서드는 로케이션 매니저가 에러를 발생할 때 호출된다.

* 위치 정보 갱신하기
- 로케이션 매니저가 델리게이트에게 현재 위치를 제공할 때, 로케이션 매니저는 locationManager:didUpdateToLocation:fromLocation:메서드를 호출한다.
- 이 메서드의 인자는 세 개이며, 첫 번째 인자는 델리게이트 메서드를 호출한 로케이션 매니저이다. 두 번째와 세 번째 인자는 CLLoction 타입의 객체이며, 두번째 인자는 아이폰의 현재 위치를 가지고 있다. 세 번째 인자는 이전 위치를 가지고 있으며, 메서드가 처음 호출될때 nil 값을 가진다.

* CLLocation을 사용한 위도와 경도 구하기
- 위치 정보는 로케이션 매니저로부터 CLLocation인스턴스를 통해 전달된다. CLLocation 클래스에는 5개의 프로퍼티가 있다. 첫번째 프로퍼티는 coordinate이며, 위도와 경도를 가지고 있다.
CLLocationDegress latitude = theLocation.coordinate.latitude;
CLLovationDegress longitude = theLocation.coordinate.longitude;

- CLLocation 객체는 위도와 경도에 대한 정밀도를 제공한다. CLLocation의 horizontalAccuracy프로퍼티는 현재 위치를 묘사하는 원의 반지름으로 사용된다. horizontalAccuracy 값이 클수록 위치에 대한 정밀도는 떨어진다. 애플리케이션 안의 파란색 원은 위치 정보를 수신했을 때 나타나며, 반지름으로 horizontalAccuracy를 사용한다.
- horizontalAccuracy가 마이너스 값을 갖는다면, 특정한 이유로 인해 현재 위치를 파악할 수 없음을 의미한다.
- CLLocation 객체는 altitude라는 프로퍼티를 가지고 있으며, 이것은 해발고도상의 위치를 나타낸다.
CLLocationDistance altitude = theLocation.altitude;

- 각각의 CLLocation객체는 verticalAccuracy라는 이름의 프로퍼티를 가지고 있으며, 이 프로퍼티는 해발고도에 대한 정밀도를 나타낸다. 고도는 verticalAccuracy 만큼의 오차를 가지며 미터 단위를 사용한다. 만일 verticalAccuracy가 마이너스의 값이면, 아이폰이 고도 값을 구하지 못했음을 의미한다.
- CLLocation 객체는 timestamp라는 이름의 프로퍼티를 가지며, 이것은 로케이션 매니저가 현재 위치를 결정했을 때의 시간을 나타낸다.
- CLLocation은 getDistanceFrom:라는 유용한 인스턴스 메서드 하나를 가지고 있다. 이 인스턴스 메서드는 CLLocation 객체 사이의 거리를 반환하며 다음과 같이 사용한다.
CLLocationDistance distance = [fromLocation getDistanceFrom:toLocation];

- 위 코드는 CLLocation 객체인 fromLocation과 toLocation 사이의 거리를 반환한다. distance객체는 대권거리 값을 가지며, 이 거리는 altitude프로퍼티와 두 점 사이의 해발고도 값을 무시한 채 양쪽의 점이 해발 고도상에 같은 위치에 있다는 전제하에 계산된 것이다.

* 에러통보
- 로케이션 매니저가 현재 위치를 탐색하는 데 실패하면, 두번째 델리게이트 메서드인 locationManager:didFailWithError:를 호출한다.
- 에러가 발생하는 가장 흔한 원인 중 하나는 사용자 접근 불가이다. 로케이션 매니저는 사용자 인증을 필요로 하기 때문에, 애플리케이션이 위치 정보에 처음으로 접근할 때 사용자가 위치 정보에 접근해도 되는지를 확인하는 알림 창을 화면에 띄운다.
- 사용자가 Don't Allow 버튼을 클릭하면, 로케이션 매니저는 locationManager:didFailWithError:메서드를 호출하고 kCLErrorDenied라는 에러 코드와 함께 델리게이트에게 통보한다.
- locationManager:didFailWithError:는 kCLErrorLocationUnknown 라는 또 다른 에러 코드를 사용하며, 이것은 현재 위치를 결정할 수 없음을 나타낸다. 하지만 kCLErrorLocationUnknown 에러가 발생해도 로케이션 매니저가 지속적으로 현재 위치를 탐색한다. kCLErrorDenied 메러가 발생하면 애플리케이션은 현재 세션을 유지하는 동안에는 위치 정보에 접근할 수 없다.

* 코어 로케이션 사용하기
- Xcode에서 뷰 기반의 애플리케이션 템플릿을 사용하여 새 프로젝트를 생성한다. 파일명은 WhereAmI라고 입력한다.

#### WhereAmIViewController.h ####
#import 
#import 
// 코어 로케이션 프레임워크의 헤더 파일을 포함

// 로케이션 매니저로부터 위치 정보를 받기 위해서 CLLocationManagerDelegate프로토콜을 따르게
@interface WhereAmIViewController:UIViewController
{
	// 코어 로케이션 매니저의 인스턴스 선언
	CLLocationManager *locationManager;
	// 출발 지점을 저장할 포인터를 선언, 출발 지점의 좌표는 로케이션 매니저에 의해 업데이트 될것
	CLLocation *startingPoint;
	
	UILabel *latitudeLabel;
	UILabel *longitudeLabel;
	UILabel *horizontalAccuracyLabel;
	UILabel *altitudeLabel;
	UILabel *verticalAccuracyLabel;
	UILabel *distanceTraveledLabel;
}

@property (nonatomic, retain) CLLocationManager *locationManager;
@property (nonatomic, retain) CLLocation *startingPoint;
@property (nonatomic, retain) IBOutlet UILabel *latitudeLabel;
@property (nonatomic, retain) IBOutlet UILabel *longitudeLabel;
@property (nonatomic, retain) IBOutlet UILabel *horizontalAccuracyLabel;
@property (nonatomic, retain) IBOutlet UILabel *altitudeLabel;
@property (nonatomic, retain) IBOutlet UILabel *verticalAccuracyLabel;
@property (nonatomic, retain) IBOutlet UILabel *distanceTraveledLabel;

@end

- 인터페이스 빌더를 실행하기 위해 WhereAmIViewController.xib를 더블 클릭한다. 레이블 6개를 왼쪽에 배치하고 레이블 텍스트를 각각 Latitude:, Longitude:, Horizontal Accuracy:, Altitude:, Vertical Accuracy:, Distance Traveled:라고 입력한다.
- 오른쪽에 레이블 6개를 더 배치하고 각 아웃렛과 연결한다.

#### WhereAmIViewController.m #####
#import "WhereAmIViewController.h"

@implementation WhereAmIViewController
@synthesize locationManager;
@synthesize startingPoint;
@synthesize latitudeLabel;
@synthesize longitudeLabel;
@synthesize horizontalAccuracyLabel;
@synthesize altitudeLabel;
@synthesize verticalAccuracyLabel;
@synthesize distanceTraveledLabel;

#pragma mark -
-(void)viewDidLoad
{
	// CLLocationManager를 메모리에 할당하고 초기화
	self.locationManager = [[CLLocationManager alloc] init];
	// 컨트롤러 클래스를 델리케이트로 지정
	locationManager.delegate = self;
	// 정밀도를 최고로 설정
	locationManager.desiredAccuracy = kCLLocationAccuracyBest;
	// 로케이션 매니저가 위치 정보에 대한 업데이트를 시작할수 있게 호출
	[locationManager startUpdatingLocation];	
}

#pragma mark -
#pragma mark CLLocationManagerDelegate Methods
// 자기 자신을 로케이션 매니저의 델리게이트로 지정하였기 때문에 델리게이트 메서드인
// locationManager:didUpdateToLocation:fromLocation:을 여기에 구현
-(void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation
{
	// startingPoint가 nil값을 가지는지를 확인하고 nil이라면, 위치정보를 업데이트하고 현재 위치를 startingPoint에 할당
	if(startingPoint == nil)
		self.startingPoint = newLocation;
	
	NSString *latitudeString = [[NSString alloc] initWithFormat:@"%g•", newLocation.coordinate.latitude];
	latitudeLabel.text = latitudeString;
	[latitudeString release];
	
	NSString *longitudeString = [[NSString alloc] initWithFormat:@"%g•", newLocation.coordinate.longitude];
	longitudeLabel.text = longitudeString;
	[longitudeString release];
	
	NSString *horizontalAccuracyString = [[NSString alloc] initWithFormat:@"%gm", newLocation.horizontalAccuracy];
	horizontalAccuracyLabel.text = horizontalAccuracyString;
	[horizontalAccuracyString release];
	
	NSString *altitudeString = [[NSString alloc] initWithFormat:@"%gm", newLocation.altitude];
	altitudeLabel.text = altitudeString;
	[altitudeString release];
	
	NSString *verticalAccuracyString = [[NSString alloc] initWithFormat:@"%gm", newLocation.verticalAccuracy];
	verticalAccuracyLabel.text = verticalAccuracyString;
	[verticalAccuracyString release];
	
	CLLocationDistance newDistance = [newLocation distanceFromLocation:startingPoint];
	NSString *distanceString = [[NSString alloc] initWithFormat:@"%gm", newDistance];
	distanceTraveledLabel.text = distanceString;
	[distanceString release];
	
}

-(void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error
{
	NSString *errorType = (error.code == kCLErrorDenied) ? @"Access Denied" : @"Unknown Error";
	UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error gettting Location"
													message:errorType
												   delegate:nil
										  cancelButtonTitle:@"Okay"
										  otherButtonTitles:nil];
	[alert show];
	[alert release];
	
}


- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
}

- (void)viewDidUnload {
	self.locationManager = nil;
	self.latitudeLabel = nil;
	self.longitudeLabel = nil;
	self.horizontalAccuracyLabel = nil;
	self.altitudeLabel = nil;
	self.verticalAccuracyLabel = nil;
	self.distanceTraveledLabel = nil;

	[super viewDidUnload];
}


- (void)dealloc {
	[locationManager release];
	[latitudeLabel release];
	[longitudeLabel release];
	[horizontalAccuracyLabel release];
	[altitudeLabel release];
	[verticalAccuracyLabel release];
	[distanceTraveledLabel release];
    [super dealloc];
}

@end

- 프로그램을 컴파일하기 전에 CoreLocation.framework 를 추가한다.

- 나는 VMWare 상태라서 그런지 전부 Error만 리턴된다.. 쩝
- 또한 getDistanceFrom: 메서드 사용시 계속적인 경고메시지가 출력되어 메서드를 변경했다. 어디선가 찾은거 같은데
- [newLocation distanceFromLocation:startingPoint] 이렇게 하니까 경고 메시지가 나오지 않는다.
:

[iPhone] - 시작하세요! 아이폰 3 프로그래밍 - Part 13. 탭, 터치 그리고 제스처

iPhone/[위키북스]시작하세요! 아이폰3 프로그래밍 2010. 9. 5. 13:10

* 탭, 터치 그리고 체스처
- 멀티터치 스크린은 동시에 여러 손가락으로 터치한 상황을 감지할 수 있고, 각각 터치한 좌표를 독립적으로 추적할 수도 있다. 애플리케이션은 다양한 제스처를 인식할 수 있으며, 이러한 제스처는 인터페이스를 사용하지 않고 사용자의 명령을 애플리케이션에 전달할 수 있다.

* 멀티터치 관련 용어
- 제스처(gesture)는 화면을 1개 이상의 손가락으로 터치하기 시작하여 화면에서 손가락을 뗄 때까지 일어나는 연속적인 이벤트를 말한다.
- 제스처의 이벤트가 아무리 길어도, 손가락이 여전히 화면에 닿아 있으면 제스처는 아직 입력 중인 상태이다.
- 제스처는 이벤트 내부의 시스템을 통해 전달된다.
- 터치(touch)는 한마디로 하나의 손가락이 아이폰의 화면 위에 놓여진 상태를 말한다. 하나의 제스처에 관련된 터치의 개수는 화면상에 동시에 닿아 있는 손가락의 개수와 같다.
- 탭은 손가락 하나를 화면에 터치한 뒤 손가락을 움직이지 않은 채로 화면에서 즉시 들어올릴 때 발생한다. 아이폰이 기록하는 탭의 횟수에는 제한이 없다. 아이폰은 한 번의 탭과 두번의 탭을 구분하기 위해 타이밍을 계산하고 필요한 일을 처리한다.
- 탭은 한 소가락을 사용할 때만 유효하다. 2개 이상의 손가락으로 탭을 한다면 탭 카운트는 1회로 재설정된다.

* 리스폰더 체인
- 제스처는 이벤트 내부의 시스템을 통해 전달되며 이벤트들은 리스폰더 체인(responder chain)을 통과한다.
- 일반적으로 퍼스트 리스폰더는 사용자가 현재 상호작용하고 있는 객체를 말한다. 퍼스트 리스폰더는 리스폰더 체인의 시작 지점이다.
- 상위클래스로 UIResponder를 가지는 모든 클래스는 리스폰더이다. UIView는 UIResponder의 하위클래스이고, UIControl은 UIView의 하위클래스다. 따라서 모든 뷰와 컨트롤은 리스폰더가 될 수 있다.  마찬가지로 UIViewController도 UIResponder의 하위클래스며, UINavigationController와 UITabBarController의 하위클래스도 리스폰더이다. 리스폰더는 터치 이벤트와 같은 시스템 이벤트에 대해'응답'한다고 하여 '리스폰더'라는 이름을 붙혔다.
- 이벤트는 각각의 뷰를 거쳐 점차 뷰의 상위 계층으로 이동하고 뷰 컨트롤러들은 차례로 이벤트를 처리할 수 있는 기회를 가진다. 이벤트가 뷰의 최상위 층에 도착하면 이벤트는 애플리케이션의 윈도우에게 넘겨진다. 애플리케이션의 윈도우가 이벤트를 처리하지 않으면, 이벤트는 애플리케이션의 UIApplicataion객체 인스턴스에게 넘긴다. UIApplication 객체가 이벤트를 처리하지 않는다면 이벤트는 소멸된다.
- 스와이프 제스처가 뷰나 테이블 뷰 셀의 하위뷰 안에서 일어났다면, 뷰나 하위뷰는 제스처를 처리할 기회를 갖게 된다. 만일 하위뷰가 제스처를 처리하지 않으면 테이블 뷰 셀이 제스처를 처리할 기회를 갖게 된다. 테이블 뷰가 이벤트를 처리하지 않으면 이벤트는 처리될 때까지 체인 안의 남은 리스폰더에게 순서대로 전달되거나 마지막에는 체인의 끝에 도달하여 소멸하게 된다.

* 이벤트 전달하기 : 리스폰더 체인 유지하기
- 테이블 뷰 셀은 전달받은 이벤트가 스와이프 제스처인지 확인 하기 위해 터치 이벤트가 발생할 때 호출되는 메서드를 가지고 있다. 만일 이벤트가 스와이프 제스처라면 테이블 뷰 셀은 메일을 삭제하기 위해 액션 메서드를 등록하고, 이벤트는 더 이상 다른 객체로 넘어가지 않고 이동을 중단할 것이다.
- 만일 이벤트가 스와이프 제스처가 아니면, 테이블 뷰 셀은 이벤트를 리스폰더 체인상의 다음 객체에게 넘겨준다. 만약 섹이 이벤트를 넘겨주지 않으면 테이블을 포함한 리스폰더 체인상의 위쪽에 위치한 다른 객체들은 이벤트를 처리할 기회를 가질 수 없고 애플리케이션은 사용자가 기대한 것과는 다른 동작을 하게 될 것이다. 테이블 뷰 셀이 이벤트를 넘기지 않으면 다른 뷰는 제스처를 인식할 수 없다.
- 이벤트는 자동으로 다음 리스폰더 객체에게 넘어가지 않기 때문에, 이벤트를 넘기는 코드를 추가해야 한다. 객체가 처리되지 않는 이벤트를 받았다면, 같은 이름의 메서드를 호출하여 이벤트를 직접 전달할 수 있다.
-(void)respondToFictionalEvent:(UIEvent *)event
{
  if(comCondition)
  {
    [self handleEvent:event];
  } else {
    [self.nextResponder respondToFictionalEvent:event];
  }
}

* 멀티터치 아키텍처
- 제스처는 이벤트 안에 포함되어 리스폰더 체인을 따라 넘겨진다. 이것은 멀티터치 스크린을 사용하는 제스처 처리 코드가 리스폰더 체인에 연결된 객체 중 하나에 있어야 한다는 것을 의미한다. 또한 UIView의 하위클래스나 UIViewController 클래스 중 하나를 선택하여 클래스 내부에 제스처를 처리하는 코드를 추가할 수 있다는 것을 의미한다.
- 사용자가 화면을 터치 했을 때 화면에 보여지는 무언가를 처리해야 한다면 제스처를 처리하는 코드는 뷰 클래스 안에 포함되어야 한다.
- 반면에 눈에 보이지 않는 내부적인 처리를 할때는 제스처를 처리하는 코드는 부 컨트롤러 클래스 안에 있어야한다.

* 4개의 제스처 통보 메서드
- 리스폰더 객체는 4개의 메서드를 통해 터치와 제스처를 전달 받는다. 아이폰은 사용자가 화면을 터치할때 touchesBegan:withEvent: 메서드가 구현된 리스폰더 객체를 찾는다. 뷰나 뷰 컨트롤러 객체 안에 이 메서드를 구현하면 사용자가 제스처를 입력하거나 화면을 탭 했을 때 호출된다.
- touchesBegan:withEvent: 메서드와 터치에 관련된 메서드들은 touches라는 이름의 NSSet인스턴스 변수와 UIEvent 인스턴스 변수를 인자로 갖는다. touches안의 count 객체를 참조하여 현재 화면에 눌려진 손가락의 개수를 알수 있다.
- touches 객체가 가지고 있는 객체들은 UITouch객체로 부터 탭 회수를 가져올수 있다. 물론 touches안에 객체가 하나 이상이라면 탭 카운트는 무조건 1이다. 왜냐하면 앞에서 말한 것처럼 아이폰은 탭한 손가락이 하나일때만 탭 횟수를 기록하기 때문이다. 예제가 실행될 때 numTouches값이 2이면, 사용자는 더블탭을 한것이다.
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
  NSUInteger numTaps = [[touches anyObject] tapCount];
  NSUInteger numToches = [touches count];
}

- touches 변수 안의 객체들은 touchesBegan:withEvent:를 구현하고 있는 뷰나 뷰 컨트롤러와 연동되지 않는다. 예를 들어 설명하자면, 하나의 테이블 뷰 셀은 다른 테이블 뷰 셀에게 전달된 touches 객체나 내비게이션 바에 전달된 touches객체를 사용하지 않는다.
- 애플리케이션의 특정 뷰에서 발생하는 이벤트로부터 touches 객체 중 하나를 얻기 위해 아래와 같이 호출할 수 있다.
NSSet *touches = [event touchesForView:self.view];

- 각각의 UITouch 객체는 서로 다른 손가락을 나타낸다. 그리고 각각의 손가락은 화면상의 서로 다른 좌표에 위치한다. UITouch객체를 사용하면 특정 손가락의 좌표를 가져올 수 있다. 원한다면 손가락의 좌표를 뷰의 지역 좌표계로 변환하는 작업도 할 수 있다.
CGPoint point = [touch locationInView:self];

- 사용자가 손가락으로 화면 위를 움직이는 동안 이벤트를 받으려면 touchesMoved:withEvent:메서드를 구현해야 한다. 이 메서드는 손가락이 이동하는 동안 계속해서 호출되며, 호출될 때마다 touches인자와 event를 전달받는다. 덧붙여서 말하면 각 손가락의 현재 위치는 touches 인자를 사용하여 UITouch객체로부터 가져올 수 있고, 또한 터치의 예전 위치도 가져올 수 있다. 여기서 말하는 예전 위치는 touchesMoved:withEvent: 메서드나 touchesBegan:withEvent: 메서드가 마지막으로 호출되었을 때의 손가락 위치를 의미한다.
- 사용자가 화면에서 손가락을 떼면 touchesEnded:withEvent:메서드가 호출되나. 이 메서드가 호출되면 제스처 입력이 끝났다는것을 알 수 있다.
- 리스폰더 객체에 구현할 수 있는 마지막 메서드가 있다. touchesCancelled:withEvnet:메서드 이며, 전화가 오는것과 같은 인터럽트가 발생할 때 사용자가 제스처를 입력중이라면, touchesCancelled:withEvent:메서드가 호출된다. 이 메서드가 호출되면 touchesEneded:withEvent:메서드가 호출되지 않는다.

* 터치 익스플로러 애플리케이션
- Xcode에서 뷰 기반의 애플리케이션 템플릿을 사용하여 새 프로젝트를 하나 생성하고, 프로젝트 이름을 TouchExplorer라고 입력한다. 이 애플리케이션은 사용자가 화면에 터치 횟수와 탭한 회수 그리고 메서드가 호출된 시간을 나타내는 메시지를 출력할 것이다.
- 이 애플리케이션은 3개의 레이블을 필요로 한다. 첫번째 레이블의 텍스트는 각각의 메서드가 호출되었다는것을 나타내고, 두번째 레이블에는 사용자가 현재 탭을 한 횟수를 출력, 마지막 레이블에는 터치한 횟수를 출력한다.
#### TouchExplorerViewController.h ####
#import 

@interface TouchExplorerViewController : UIViewController 
{
	// 각각의 메서드가 호출되었다는 것을 나타낼 Label
	UILabel *messageLabel;
	// 사용자가 현재 탭을 한 회수를 나타내는 Label
	UILabel *tapsLabel;
	// 터치한 횟수를 나타내는 Label
	UILabel *touchesLabel;
}

@property (nonatomic, retain) IBOutlet UILabel *messageLabel;
@property (nonatomic, retain) IBOutlet UILabel *tapsLabel;
@property (nonatomic, retain) IBOutlet UILabel *touchesLabel;
-(void)updateLablesFromToTouches:(NSSet *)touches;
@end

- 인터페이스 빌더에서 TouchExplorerViewController.xib를 더블 클릭한다.
- 라이브러리 창에서 레이블 3개를 드래그하여 View윈도우에 올려 놓는다. 레이블의 너비가 뷰의 너비에 가득 차도록 레이블의 크기를 조절하면 텍스트를 뷰 중앙에 배치할수 있다.
- File's Owner 아이콘을 선택하고 컨트롤 키를 누른 상태에서 각각의 레이블 위로 드래그 한다. 맨위의 레이블은 messageLabel아웃렛으로 연결하고 다른 하나는 tapsLabel아웃렛으로, 그리고 마지막 하나는 touchesLabel아웃렛으로 연결한다.
- 마지막으로 View아이콘을 클릭하고 Func+1을 눌러서 속성 인스펙터 창이 나타나게 한다. 속성 인스펙처 창의 User Interaction Enabled와 Multiple Touch항목에 체크한다. Multiple Touch를 체크하지 않으면, 화면에 많은 속가락을 터치해도 컨트롤러 클래스의 터치 메서드는 오직 하나의 이벤트만을 받는다.

#### TouchExplorerViewController.m ####
#import "TouchExplorerViewController.h"

@implementation TouchExplorerViewController
@synthesize messageLabel;
@synthesize tapsLabel;
@synthesize touchesLabel;

// updateLabelsFromToTouches:메서드는 touches로부터 UITouch객체를 받는다.
// 그리고 UITouch객체의 tapCount를 참조하여 탭회수를 저장하고,
// touches의 count 메서드를 호출하여 터치한 횟수를 저장하였다.
-(void)updateLablesFromToTouches:(NSSet *)touches
{
	NSUInteger numTaps = [[touches anyObject] tapCount];
	NSString *tapsMessage = [[NSString alloc] initWithFormat:@"%d taps detected", numTaps];
	tapsLabel.text = tapsMessage;
	[tapsMessage release];
	
	NSUInteger numTouches = [touches count];
	NSString *touchMsg = [[NSString alloc] initWithFormat:@"%d touches detected.", numTouches];
	touchesLabel.text = touchMsg;
	[touchMsg release];
}

#pragma mark -
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
	messageLabel.text = @"Touches Began";
	[self updateLablesFromToTouches:touches];
}

-(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
	messageLabel.text = @"Toches Cancelled";
	[self updateLablesFromToTouches:touches];
}

-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
	messageLabel.text = @"Touches Detected";
	[self updateLablesFromToTouches:touches];
}

-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
	messageLabel.text = @"Touches Stopped";
	[self updateLablesFromToTouches:touches];
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];	
}

- (void)viewDidUnload {
	self.messageLabel = nil;
	self.tapsLabel = nil;
	self.touchesLabel = nil;
	[super viewDidUnload];
}

- (void)dealloc {
	[messageLabel release];
	[tapsLabel release];
	[touchesLabel release];
    [super dealloc];
}

@end

- 나 같은 경우는 VMware를 사용하기 때문에 멀티 터치 옵션을 사용하기 힘들다. 단순 맥에서 시뮬레이터를 이용할 경우는 드래그 상태에서 옵션키를 같이 누르면 2개의 터치를 사용하여 테스트할수 있다. 그러나 VMware에서는 드래그상태에서 Alt를 사용하면 같은 효과를 볼수 있다.

 * 스와이프 애플리케이션
- Xcode에서 뷰 기반의 애플리케이션 템플릿을 사용하여 새 프로젝트를 만들고, 이번에는 Swipes라고 입력한다.
- 이번에 만들 애플리케이션은 수평과 수직 방향의 스와이프 제스처를 인식하는 기능만 있다. 손가락으로 화면의 왼쪽에서 오른쪽,오른쪽에서 왼쪽, 위에서 아래로 혹은 아래에서 위로 스와으프 하면, 스와이프 애플리케이션은 화면 위쪽에 스와이프가 인식되었다는 것을 알려 주는 메시지를 잠시 동안 보여줄것이다.
- 스와이프를 인식하는 것은 비교적 쉬운 일이다. 우리는 최소 이동거리를 픽셀 단위로 저의할 것이다. 이 거리는 사용자의 제스처가 스와이프로 인식되기 위해서 손가락이 화면을 이동해야 하는 최소 거리이다. 또한 편차 값을 하나 정의할 것이고, 이것은 스와이프 제스처가 수평 방향인지 수직 방향인지 결정할 때 스와으프 한 직선이 얼마나 수직에 가까운지에 대한 오차 범위를 나타낸다.
- 사용자가 화면을 터치할 때 변수 하나에 처음 터치한 위치를 저장할 것이다. 그리고 나서, 사용자의 손가락이 스와이프 제스처라 판단할 수 있을 만큼 충분한 거리와 직선 방향으로 이동했는지 체크할 것이다.

#### SwipesViewController.h ####
#import 

// 스와이프로 제스처에 대한 최소 이동거리를 25픽셀로 정의
#define kMinimumGestureLength 25
// 편차는 5로 정의
#define kMaximumVariance 5

@interface SwipesViewController : UIViewController 
{
	// 이블을 가리키는 아웃
	UILabel *label;
	// 사용자가 처음 터치한 지점을 저장하기 위한 변수
	CGPoint gestureStartPoint;
}

@property (nonatomic, retain) IBOutlet UILabel *label;
@property CGPoint gestureStartPoint;
// 레이블의 텍스트를 삭제하기 위한 메서드
-(void)eraseText;
@end

#### SwipesViewController.m ####
#import "SwipesViewController.h"

@implementation SwipesViewController
@synthesize label;
@synthesize gestureStartPoint;

-(void)eraseText
{
	label.text = @"";
}

#pragma mark -
// touches인자로 부터 UITouch객체의 포인터를 가져왔다. 
// 스와이프 애플리케이션은 터치된 손가락이 하나라고 가정하고 동작하기 때문에 터치된 손가락의 갯수는 걱정하지 말자.
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
	UITouch *touch = [touches anyObject];
	gestureStartPoint = [touch locationInView:self.view];
}

// 실제 스와이프 애플리케이션 인식하는곳
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
	// 사용자가 터치한 위치의 좌표를 구한다.
	UITouch *touch = [touches anyObject];
	CGPoint currentPosition = [touch locationInView:self.view];
	
	// 터치한 위치를 기준으로 손가락이 수평과 수직 방향으로 각각 얼마만큼 이동했는지를 계산한다.
	// 표준 C math라이브러리인 fabsf()함수는 float변수의 절대 값을 반환한다.
	CGFloat deltaX = fabsf(gestureStartPoint.x - currentPosition.x);
	CGFloat deltaY = fabsf(gestureStartPoint.y - currentPosition.y);
	
	// 수평과 수직 방향의 이동거리를 계산하고 스와이프라고 판단할 수 있을 정도로 손가락이 한쪽 방향으로
	// 충분히 이동했는지 확인한다.
	if(deltaX >= kMinimumGestureLength && deltaY <= kMaximumVariance)
	{
		label.text = @"Horizontal swipe detected";
		// performSelector:withObject:afterDelay:를 사용하여 레이블의 텍스트가 2초 후에 화면에서
		// 사라지게 처리한다.
		[self performSelector:@selector(eraseText) withObject:nil afterDelay:2];
		
	} else if(deltaY >= kMinimumGestureLength && deltaX <= kMaximumVariance) {
		label.text = @"Vertical swipe detected";
		[self performSelector:@selector(eraseText) withObject:nil afterDelay:2];
	}
	
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];	
}

- (void)viewDidUnload 
{
	self.label = nil;
}

- (void)dealloc {
	[label release];
    [super dealloc];
}

@end




* 멀티 스와이프 구현하기
- 2개 이상의 손가락을 인식할 때는 앞에 예제는 사용하기 어렵다. 가장 큰 문제는 인자로 제공하는 touches변수의 자료형이 NSArray가 이닌 NSSet이라는 것이다. NSSet은 배열과는 달리 정렬되지 않는 자료형이기 때문에, 어떤 손가락을 참조하여 비교해야 하는지 결정할 수 없다.
- 더욱 힘든 점은 사용자가 2개 이상의 손가락으로 제스처를 입력했을때 각각의 손가락이 동시에 화면에 터치되지 않을 수 있다는 것이다. 어느 손가락으로 제스처를 입력했을 때 각각의 손가락이 동시에 화면에 터치되지 않을 수 있다는 것이다. 어느 손가락이든 하나가 먼저 화면에 닿게 되면 touchesBegan:withEvent:메서드가 호출되어 한 손가락 터치에 대한 이벤트 정보만이 전달된다.
- 이 문제를 해결하는 바업은 간단하다. 우선 touchesBegan:withEvent:가, 제스처가 터치의 시작을 알리는 이벤트를 받을 때 앞에서 했던 것처럼 한 소가락의 위치를 저장한다. 그중 하나만을 사용할 것이므로 모든 손가락의 위치를 저장할 필요는 없다.
- 손가락의 위치를 저장한 다음 스와이프 제스처인지 확인하기 위해 touchesMoved:withEvent:메서드 안에서 for 루프를 사용하여 touches객체에 있는 각각의 touch 객체의 좌표 값을 비교한다. 사용자가 여러 손가락으로 스와이프를 했다면, for 루프에서 좌표 값을 비교할 때 적어도 하나의 touch객체는 스와이프의 조건을 만족할 것이다.
- 수평/수직 스와이프라는 것이 확인되면 touches객체를 사용하는 for루프를 한번 더 실행하여 모든 손가락이 처음 터치한 좌표로부터 최소한의 기준거리를 이동했는지 확인한다.

#### SwipesViewController.h ####
#import 

// 스와이프로 제스처에 대한 최소 이동거리를 25픽셀로 정의
#define kMinimumGestureLength 25
// 편차는 5로 정의
#define kMaximumVariance 5

// enumeration변수는 체스처가 수평방향인지 수직 방향인지 구분하고 
// 스와이프 제스처의 인수여부를 나타내기 위해 사용할 것이다.
typedef enum {
	kNoSwipe = 0,
	kHorizontalSwipe, 
	kVerticalSwipe
} SwipeType;

@interface SwipesViewController : UIViewController 
{
	// 레이블을 가리키는 아웃
	UILabel *label;
	// 사용자가 처음 터치한 지점을 저장하기 위한 변수
	CGPoint gestureStartPoint;
}

@property (nonatomic, retain) IBOutlet UILabel *label;
@property CGPoint gestureStartPoint;
// 레이블의 텍스트를 삭제하기 위한 메서드
-(void)eraseText;
@end

#### SwipesViewController.m ####
#import "SwipesViewController.h"

@implementation SwipesViewController
@synthesize label;
@synthesize gestureStartPoint;

-(void)eraseText
{
	label.text = @"";
}

#pragma mark -
// touches인자로 부터 UITouch객체의 포인터를 가져왔다. 
// 스와이프 애플리케이션은 터치된 손가락이 하나라고 가정하고 동작하기 때문에 터치된 손가락의 갯수는 걱정하지 말자.
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
	UITouch *touch = [touches anyObject];
	gestureStartPoint = [touch locationInView:self.view];
}

// 실제 스와이프 애플리케이션 인식하는곳
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
	SwipeType swipeType = kNoSwipe;
	for(UITouch *touch in touches)
	{
		CGPoint currentPosition = [touch locationInView:self.view];
		
		CGFloat deltaX = fabsf(currentPosition.x - gestureStartPoint.x);
		CGFloat deltaY = fabsf(currentPosition.y - gestureStartPoint.y);
		
		if(deltaX >= kMinimumGestureLength && deltaY <= kMaximumVariance)
			swipeType = kHorizontalSwipe;
		else if(deltaY >= kMinimumGestureLength && deltaX <= kMaximumVariance)
			swipeType = kVerticalSwipe;
	}
	
	BOOL allFingersFarEnoughAway = YES;
	if(swipeType != kNoSwipe)
	{
		for(UITouch *touch in touches)
		{
			CGPoint currentPosition = [touch locationInView:self.view];
			
			CGFloat distance;
			
			if(swipeType == kHorizontalSwipe)
				distance = fabsf(currentPosition.x - gestureStartPoint.x);
			else
				distance = fabsf(currentPosition.y - gestureStartPoint.y);
			
			if(distance < kMinimumGestureLength)
				allFingersFarEnoughAway = NO;
			
		}
	}
	
	if(allFingersFarEnoughAway && swipeType != kNoSwipe)
	{
		NSString *swipeCountString = nil;
		if([touches count] == 2)
			swipeCountString = @"Double";
		else if([touches count] == 3)
			swipeCountString = @"Triple";
		else if([touches count] == 4)
			swipeCountString = @"Quadruple";
		else if([touches count] == 5)
			swipeCountString = @"Quintuple";
		else
			swipeCountString = @"";
		
		NSString *swipeTypeString = (swipeType == kHorizontalSwipe) ? @"Horizontal" : @"Vertical";
		
		NSString *message = [[NSString alloc] initWithFormat:@"%@%@ Swipe Detected.", swipeCountString, swipeTypeString];
		label.text = message;
		[message release];
		[self performSelector:@selector(eraseText) withObject:nil afterDelay:2];
		
	}
	
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];	
}

- (void)viewDidUnload 
{
	self.label = nil;
}

- (void)dealloc {
	[label release];
    [super dealloc];
}

@end

* 멀티탭 인식하기
- Xcode에서 뷰 기반 애플리케이션 템플릿으로 새 프로젝트를 생성한다. 프로젝트 이름을 TapTaps 라고 입력한다. 이 애플리케이션은 4개의 레이블을 가지며, 싱글탭, 멀티탭, 트리플탭, 쿼드러플탭이 인식되었을때 레이블의 텍스트를 변경하여 화면에 표시한다.
- 4개의 레이블에 각각 연결할 아웃렛이 필요하고 실제 애플리케이션에서 동작하는 것과 같이 시뮬레이션하기 위해 각각의 탭이 발생했을 때 호출할 4개의 메서드가 필요하다. 또 텍스트 필드를 지우는 메서드도 추가할 것이다.

#### TapTapsViewController.h ####
#import 

@interface TapTapsViewController : UIViewController 
{
	UILabel *singleLabel;
	UILabel *doubleLabel;
	UILabel *tripleLabel;
	UILabel *quadrupLabel;	
}

@property (nonatomic, retain) IBOutlet UILabel *singleLabel;
@property (nonatomic, retain) IBOutlet UILabel *doubleLabel;
@property (nonatomic, retain) IBOutlet UILabel *tripleLabel;
@property (nonatomic, retain) IBOutlet UILabel *quadrupLabel;

-(void)singleTap;
-(void)doubleTap;
-(void)tripleTap;
-(void)quadrupleTap;
-(void)eraseMe:(UITextField *)textField;

@end

- TapTapsViewController.xib를 더블클릭하여 인터페이스 빌더에서 파일이 열리도록 한다. 라이브러리 창에서 레이블 4개를 가져와 뷰를 구성한다. File's Owner 아이콘을 컨트롤-드래그하여 각각의 레이블 위에 놓고 각각 singleLabel, doubleLabel, tripleLabel, quadrupLabel로 연결한다.

#### TapTapsViewController.m ####
#import "TapTapsViewController.h"

@implementation TapTapsViewController
@synthesize singleLabel;
@synthesize doubleLabel;
@synthesize tripleLabel;
@synthesize quadrupLabel;

// 해당 텍스트를 변경하고 1.6초 후에 해당 Text를 삭제 한다.
-(void)singleTap
{
	singleLabel.text = @"Single Tap Detected";
	[self performSelector:@selector(eraseMe:) withObject:singleLabel afterDelay:1.6f];
}

-(void)doubleTap
{
	doubleLabel.text = @"Double Tap Detected";
	[self performSelector:@selector(eraseMe:) withObject:doubleLabel afterDelay:1.6f];
}

-(void)tripleTap
{
	tripleLabel.text = @"Triple Tap Detected";
	[self performSelector:@selector(eraseMe:) withObject:tripleLabel afterDelay:1.6f];
}

-(void)quadrupleTap
{
	quadrupLabel.text = @"Quadruple Tap Detected";
	[self performSelector:@selector(eraseMe:) withObject:quadrupLabel afterDelay:1.6f];
}

-(void)eraseMe:(UITextField *)textField
{
	textField.text = @"";
}

#pragma mark -
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
	UITouch *touch = [touches anyObject];
	NSUInteger tapCount = [touch tapCount];
	switch (tapCount)
	{
		case 1:
			[self performSelector:@selector(singleTap) withObject:nil afterDelay:0.4f];
			break;
		case 2:
			// performSelector:withObject:afterDelay:에 의한 호출을 취소하는 메소드가 있다.
			// 그것은 NSObject에 cancelPreviousPerformRequestsWithTarget:selector:object: 이다.
			// performSelector:withObject:afterDelay:를 사용하여 바로 호출하지 않고
			// tapCount에 맞게 이전 메서드를 취소시키고 새로운 메서드를 호출한다.
			[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(singleTap) object:nil];
			[self performSelector:@selector(doubleTap) withObject:nil afterDelay:0.4f];
			break;
		case 3:
			[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(doubleTap) object:nil];
			[self performSelector:@selector(tripleTap) withObject:nil afterDelay:0.4];
			break;
		case 4:
			[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(tripleTap) object:nil];
			[self quadrupleTap];
			break;
		default:
			break;
	}
}

- (void)didReceiveMemoryWarning {
	// Releases the view if it doesn't have a superview.
    [super didReceiveMemoryWarning];
	
	// Release any cached data, images, etc that aren't in use.
}

- (void)viewDidUnload {
	self.singleLabel = nil;
	self.doubleLabel = nil;
	self.tripleLabel = nil;
	self.quadrupLabel = nil;
}

- (void)dealloc {
	[singleLabel release];
	[doubleLabel release];
	[tripleLabel release];
	[quadrupLabel release];
    [super dealloc];
}

@end

* 핀치 인식하기
- 일반적으로 많이 사용하는 또 하나의 제스처는 두 손가락을 사용한 핀치이다. 핀치는 모바일 사파리, 메일, 사진과 같이 줌인과 줌 아웃 기능을 제공하는 다수의 애플리케이션에서 사용되고 있다.
- 핀치 인식은 매우 쉽다. 먼저, 제스처가 시작될 때 터치한 손가락이 2개인지 확인한다. 핀치는 두 손가락을 사용한 제스처이기 때문이다. 터치한 손가락이 2개라면 손가락 사이의 거리를 계산하다. 그리고 나서, 제스처가 진행되는 동안 계속해서 손가락 사이의 거리를 확인한다. 그래서 그 거리가 어느 정도 이상 증가하거나 감소하면 핀치로 인식한다.
- Xcode에서 새 프로젝트를 생성하고 뷰 기반의 애플리케이션 템플릿을 한 번 더 선택한다. 그리고 프로젝트 이름에 PinchMe라고 입력한다.
- 13 PinchMe 폴더 안의 CGPointUils.h와 CGPointUtils.c라는 이름의 파일을 찾는다. 두 파일을 드래그하여 방금 생성한 프로젝트의 Classes폴더로 옮긴다.
- PinchMe 애플리케이션은 레이블을 가리키는 아웃렛 변수와 손가락 사이의 거리를 저장할 인스턴스 변수 하나를 필요로 한다. 그리고 앞에서 만든 애플리케이션처럼 레이블의 텍스트를 지우는 메서드도 필요하다. 또, 핀치 제스처를 실행하는 두 손가락 사이의 거리의 최소값을 가지는 상수 하나를 선언할 것이다.

#### PinchMeViewController.h ####
#import 

#define kMinimumPinchDelta 100

@interface PinchMeViewController : UIViewController 
{
	UILabel *label;
	CGFloat initialDistance;	
}

@property (nonatomic, retain) IBOutlet UILabel *label;
@property CGFloat initialDistance;
-(void) eraseLabel;

@end


- Resources 폴더를 열고 PinchMeViewController.xib를 더블 클릭한다. 인터페이스 빌더에서 반드시 뷰를 멀티터치 지원하도록 설정한다. 레이블 하나 드래그하여 뷰 위에 놓는다. 설정을 마치면 레이블을 더블클릭하고 그 안에 포함된 텍스트를 삭제한다. 그런 다음 File's Owner아이콘을 컨트롤 드래그 하여 레이블 위에 놓고 label 아웃렛과 연결한다.

#### PinchMeViewController.m ####
#import "PinchMeViewController.h"
#import "CGPointUtils.h"

@implementation PinchMeViewController
@synthesize label;
@synthesize initialDistance;

-(void)eraseLabel
{
	label.text = @"";
}

#pragma mark -
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
	// UITouch 객체의 수가 2라면 CGPointUtils.c에서 제공하는 distanceBetweenPoints메서드를
	// 사용하여 두 손가락이 터치한 좌표 사이의 거릴 계산하고 initialDistance안에 저장한다.
	if([touches count] == 2)
	{
		NSArray *twoTouches = [touches allObjects];
		UITouch *first = [twoTouches objectAtIndex:0];
		UITouch *second = [twoTouches objectAtIndex:1];
		initialDistance = distanceBetweenPoints([first locationInView:self.view], [second locationInView:self.view]);
	}	
}

-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
	// 터치한 객체의 수가 2개인지를 확인한다.
	if([touches count] == 2)
	{
		NSArray *twoTouches = [touches allObjects];
		UITouch *first = [twoTouches objectAtIndex:0];
		UITouch *second = [twoTouches objectAtIndex:1];		
		CGFloat currentDistance = distanceBetweenPoints([first locationInView:self.view], [second locationInView:self.view]);
		
		// 먼저 initialDistance 값이 0인지 확인한다. initialDistance값을 확인하는 이유는
		// 사용자 손가락이 화면에 동시에 터치되었는지 확인하기 위해서이다.
		// 두 손가락이 동시에 터치되지 않았다면 touchesBegan:withEvent:메서드가 호출되었을 때
		// 터치된 손가락은 하나일 것이다.
		// initialDistance가 0이면, touchesMoved:withEvent:메서드가 호출된 시점이 두 손가락이 처음으로
		// 화면에 닿은 것이기 때문에 initialDistance에  currentDistance를 저장한다.
		if(initialDistance == 0) {
			initialDistance = currentDistance;			
		// initialDistance가 0이 아니면, 입력된 제스처가 핀치인지 확인하기 위해 현재 손가락 사이의
		// 거리에서 처음 손가락 사이의 거리 값을 뺀 값이 상수로 정의했던 최소 거리보다 큰 값인지를 확인한다.
		// 거리의 차가 최소거리보다 크다면 바깥쪽(outside)핀치이다.
		// 왜냐하면 바깥쪽 핀치는 현재 손가락 사이의 거리가 처음 손가락 사이의 거리보다 더 크기 때문이다.
		} else if(currentDistance - initialDistance > kMinimumPinchDelta) {
			label.text = @"Outward Pinch";
			[self performSelector:@selector(eraseLabel) withObject:nil afterDelay:1.6f];
		// initialDistance가 0아니고 바깥쪽 핀치도 아니라면, 안쪽(inward)핀치인지 확인하기 위해서
		// 처음 손가락 사이의 거리에서 현재 손가락 사이의 거리를 뺀 값이 최소 거리보다 큰 값인지 확인한다.
		} else if(initialDistance - currentDistance > kMinimumPinchDelta) {
			label.text = @"Inward Pinch";
			[self performSelector:@selector(eraseLabel) withObject:nil afterDelay:1.6f];
		}
			
	}
}

-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
	initialDistance = 0;
}

- (void)didReceiveMemoryWarning {
	[super didReceiveMemoryWarning];
}

- (void)viewDidUnload {
	self.label = nil;
}

- (void)dealloc {
	[label release];
    [super dealloc];
}

@end



* 커스텀 제스처 정의하기
- 제스처를 정의할 때 가장 중요하게 처리해야 할 부분은 유연성이다.
- 예를 들면, 체크 모양과 같은 제스처를 정의한다고 가정하자. 체크마크 제스처를 구성하는 특징들은 무엇이 있을까? 한가지 조건은 두 선 사이의 뾰족한 각의 크기이다. 또한 사용자의 손가락이 두선 사이의 각을 지나기 전에 충분히 직선을 그렸는지도 체크할 수도 있다.
- Xcode에서 새 프로젝트를 생성하고, 프로젝트 이름을 CheckPlease라고 한다. CGPointUtils.h와 CGPointUtils.c 파일을 Classes에 추가한다.

#### CheckPleaseViewController.h #####
#import 

// 체스처로 인식할 최소 각을 50도로 정의
#define kMinimumCheckMarkAngle 50
// 체스처로 인식할 최대 각을 135도로 정의
#define kMaximumCheckMarkAngle 135
// 손가락이 최소한 움직여야 하는 거리
#define kMinimumCheckMarkLength 10

@interface CheckPleaseViewController : UIViewController 
{
	// 체크 모양의 제스처를 인식했을 때 사용자에게 메시지를 표시할 레이블
	UILabel *label;
	// 터치가 발생할 때마다 현재 터치한 좌표와 그 전에 터치했던 좌표 값을 전달할 변수
	// 이 두 좌표는 하나의 선을 정의한다. 
	// 모든 UITouch 객체는 이전에 터치했던 좌표와 현재 터치한 좌표를 제공한다.
	CGPoint lastPreviousPoint;
	CGPoint lastCurrentPoint;
	CGFloat lineLengthSoFar;	
}

@property (nonatomic, retain) IBOutlet UILabel *label;

-(void)eraseLabel;

@end

- 터치가 발생할 때마다 현재 터치한 좌표와 그 전에 터치했던 좌표 값이 전달된다. 이 두 좌표는 하나의 선을 정의한다. 그 후에 발생하는 터치 역시 터치가 일어난 순간의 좌표와 그 전에 터치한 좌표 값을 전달할 것이며, 이 좌표 값들은 또 다른 선을 저의 할 것이다.
- 모든 UITouch 객체는 이전에 터치했던 좌표와 현재 터치한 좌표를 제공한다는 것을 기억하자. touchesMoved:메서드 안에서 각의 크기를 확인하기 위해 이전의 두 좌표가 이루는 선을 참조할 필요가 있다. 그래서 touchesMoved:메서드가 또 다시 호출되기 전에 현재 사용자가 그린 선의 두 좌표를 저장해야 한다. 우리는 touchesMoved:메서드가 호출될 때 마다 lastPreviousPoint와 lastCurrentPoint 변수에 그렸던 선의 두 좌표를 저장할 것이므로 현재 그린 선과 이전에 그린 선을 비교하고 선이 이루는 각이 몇 도인지를 확인할 수 있게 한다.
- Resources폴더를 펼치고 CheckPleaseViewController.xib 파일을 더블클릭하여 인터페이스 빌더를 실행한다. 이것은 한 손가락 제스처이므로, 뷰가 멀티터치를 지원하도록 설정할 필요가 없다. 레이블을 하나 추가 하고 File's Owner아이콘으로부터 컨트롤 드래그하여 레이블의 아웃렛과 연결한다.

#### CheckPleaseViewController.m ####
#import "CheckPleaseViewController.h"
#import "CGPointUtils.h"

@implementation CheckPleaseViewController
@synthesize label;

-(void)eraseLabel
{
	label.text = @"";
}

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
	// 현재 체크한 좌표 값을 lastPreviousPoint와 lastCurrentPoint에 저장
	UITouch *touch = [touches anyObject];
	CGPoint point = [touch locationInView:self.view];
	lastPreviousPoint = point;
	lastCurrentPoint = point;
	lineLengthSoFar = 0.0f;
}

-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
	UITouch *touch = [touches anyObject];
	CGPoint previousPoint = [touch previousLocationInView:self.view];
	CGPoint currentPoint = [touch locationInView:self.view];
	CGFloat angle = angleBetweenLines(lastPreviousPoint, lastCurrentPoint, previousPoint, currentPoint);
	
	if(angle >= kMinimumCheckMarkAngle && angle <= kMaximumCheckMarkAngle && lineLengthSoFar > kMinimumCheckMarkLength)
	{
		label.text = @"Chekmark";
		[self performSelector:@selector(eraseLabel) withObject:nil afterDelay:1.6];
	}
	
	// 두 좌표 값에 차이를 누적해서 저장한다.
	lineLengthSoFar += distanceBetweenPoints(previousPoint, currentPoint);
	lastPreviousPoint = previousPoint;
	lastCurrentPoint = currentPoint;
	
}

- (void)didReceiveMemoryWarning {
	[super didReceiveMemoryWarning];
}

- (void)viewDidUnload {
	self.label = nil;
	[super viewDidUnload];
}

- (void)dealloc {
	[label release];
    [super dealloc];
}

@end

:

[iPhone] - 시작하세요! 아이폰 3 프로그래밍 - Part 12. 쿼츠와 OpenGL을 이용한 그리기

iPhone/[위키북스]시작하세요! 아이폰3 프로그래밍 2010. 8. 23. 16:04

* 쿼츠와 OpenGL을 이용한 그리기
- 다른 라이브러리를 사용하지 않고, UIKit 프레임워크에서 제공하는 라이브러리들만으로도 다양한 애플리케이션 인터페이스를 만들 수 있다. 그러나 일부 애플케이션들은 또 다른 라이브러리를 사용하지 않으면 구현할 수 없는것들이 있다. 예를 들면 드로잉(Drawing) 애플리케이션을 만들 경우다. 다행히 드로잉을 구현할 때 도움을 받을 수 있는 두 개의 독립적인 라이브러리가 있다.
- 하나는 쿼츠(Quartz)2D라는 코어 그래픽스 프레임워크에 속하는 라이브러리이다.
- 다른 하나는 OpenGL ES이라는 크로스 플랫폼 그래픽스 라이브러리 이다. OpenGL ES는 OpenGL이라 불리는 또 다른 크로스 플랫폼 라이브러리의 경량화 버전이다. 'ES'라는 글자는 Embedded System에서 따온 것이다.

* 그래픽 세계의 두 뷰
- 쿼츠 라이브러리는 메모리상의 뷰나 이미지에 직접 그릴 수 있도록 설계된 함수, 자료형, 객체로 구성되어 있다.
- 쿼츠 라이브러리는 뷰나 이미지를 가상 캔버스 위에 그리며, 페인터 모델이라는 드로잉 방식을 사용한다. 페인터 모델은 물감을 가지고 종이에 그림을 그리는 것과 같은 과정으로 드로잉 명령을 가장 캔버스에 적용시키는 방식이다. 드로잉이 발생하면 새로 그려질 뷰나 이미지는 이전 드로잉 때 그려진 뷰나 이미지 위에 겹쳐져 그려지게 된다.
- OpenGL ES는 상태 기계의 구조로 되어 있다. OpenGL ES는 사용자가 어떠한 행동을 하였을 때 뷰나 윈도우 혹은 이미지에 직접적으로 변화를 주는 것이 아니라 3차원의 가상 세계를 통해서 사용자의 행동을 반영한다. 이 가상 세계에 새로운 객체를 추가하면, OpenGL은 가상 세계 안에 있는 모든 객체의 정보를 기록한다. OpenGL은 가상 캔버스 대신에 가상 세계를 보여주는 가상 윈도우를 제공한다. 추가된 객체의 정보를 기록하고 나면 OpenGL은 초기화할 때 설정한 값과 객체들의 상대적인 위치 값을 기반으로 가상 윈도우에 가상 세계를 그린다.
- 쿼츠는 OpenGL에 비해 사용하기 쉬우면, 다양한 선, 도형, 이미지를 그리는 함수를 제공한다. 쿼츠 2D라이브러리가 사용하기 쉬워도 2차원 드로잉만 지원한다는 한계가 있다. 그리고 드로잉을 할 때마다 가속된다는 보장은 없다.
- OpenGL은 쿼츠에 비해 상당히 복잡하고 더 어려운 개념으로 구성되었지만, 2차원과 3차원 드로잉이 모두 가능하며 하드웨어 가속의 이점을 충분히 사용할 수 있도록 설계되었다.

* 드로잉 애플리케이션
- 예제 애플리케이션의 모습을 보면 위쪽과 아래쪽에 세그먼트 컨트롤을 가진 바로 구성되어 있다. 위쪽 바에 배치된 컨트롤들은 그리기를 할때 사용할 색을 변경하는 데 사용한다. 아래쪽 바에 있는 컨트롤들은 드로잉을 할 도형을 선택할 때 사용한다. 색과 도형을 선택하고 뷰위에서 손가락으로 드래깅하면, 선택한 도형은 선택한 색으로 그려질것이다.

* 드로잉을 위한 쿼츠 사용법
- 드로잉을 위해 쿼츠 라이브러리를 사용할 때는 일반적으로 드로잉 코드를 뷰 클래스 안에 추가한다. UIView의 하위클래스를 하나 만들고 UIView클래스의 drawRect:메서드에 쿼츠 라이브러리를 호출하는 코드를 추가하였다고 가정하자. drawRect:메서드는 UIView클래스에 정의된 멤버 함수이며, 뷰가 스스로를 화면에 그릴 때마다 호출된다. 만일 drawRect:함수 안에 쿼츠 라이브러리를 사용하는 코드를 넣었다면 쿼츠 라이브러리 함수를 호출한 후에 UIView의 뷰를 그릴것이다.

* 쿼츠 2D 그래픽스 컨텍스트
- 쿼츠 2D는 코어 그래픽스 프레임워크에서 제공하며, 쿼츠 2D를 사용할 때 드로잉은 보통 컨텍스트(Context)라고 일컫는 그래픽스 컨텍스트 안에서 일어난다. 모든 뷰는 컨텍스트와 관련이 있다. 뷰 클래스 안에서 드로잉을 할 때는 현재 컨텍스트의 값을 가져온 뒤에 컨텍스트에 드로잉을 한다. 그리고이 컨텍스트 변수는 쿼츠 라이브러리들을 호출할 때 호출할 함수의 인자로 사용하며, 뷰에 드로잉을 할때 컨텍스트가 렌더링 처리를 할 수 있게 해준다.
- 아래는 현재 컨텍스트를 가져온다.
CGContextRef context = UIGraphicsGetCurrentContext();

- 그래픽스 컨텍스트 변수를 정의하였다면, 코어 그래픽스 함수에 컨텍스트 변수를 인자로 넘겨서 드로잉을 할수 있다. 아래의 순서대로 코드가 실행되면 컨텍스트에는 2픽셀의 넓은 선이 그려진다.
// 2픽셀의 선을 생성하라고 정의한다.
CGContextSetLineWidth(context, 2.0);
// 테두리를 빨간색으로 칠하도록 함수를 호출한다.
// 코어 그래픽스 라이브러리로 드로잉을 할 때 고려해야 할 색은 2가지가 있다.
// 그것은 테두리 색과 채우기 색이다.
// 테두리 색은 선을 그릴 때와 도형의 외곽을 그릴 때 사용된다.
// 채우기 색은 도형의 내부를 채울때 사용된다.
CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
// CGContextMoveToPoint()를 호출하면 아무것도 그리지 않고 새로운 위치로 보이지 않는 펜을 이동한다. 
//이 함수를 사용하여 (100,100)에서 그리기를 시작하라고 지정한다.
CGContextMoveToPoint(context, 100.0f, 100.0f);
// CGContextAddLineToPoint()는 실제로 현재 펜의 위치에서 인자에 지정된 위치로 선을 그린다.
// 아직은 실제로 보이는 선을 그리는것은 아니다. 보이지 않는 선을 그린다.
CGContextAddLineToPoint(context, 200.0f, 200.0f);
// CGContextStrokePath()함수를 사용하여 쿼츠 라이브러리에게 선을 실제 보이게 긋도록 알려준다.
CGContextStrokePath(context);

* 좌표계
 - CGContextMoveToPoint()와 CGContextAddLineToPoint()함수에 한쌍의 부동 소수점을 인자로 넘겨 준다. 이 부동소수점들은 코어 그래픽스 좌표계상의 위치를 나타낸다. 좌표계상의 위치는 우리가 흔히 표현하는 (x,y)처럼 X좌표와 Y좌표로 사용된다. (0,0)은 컨텍스트의 왼쪽 상단 모서리의 좌표이다. 화면상의 커서를 아래로 내리면 y값이 증가하며, 커서를 오른쪽으로 움직이면 x값이 증가한다.
- 좌표계는 쿼츠를 사용하여 드로잉할 때 나타나는 직관적이지 않은 특성 중 하나이다. 왜냐하면 쿼츠의 좌표계는 많은 그래픽스 라이브러리가 사용하는 것이나 기하학에서 일반적으로 표현하는 것과 다른게 뒤집혀 있기 때문이다. 예를 들면 OpenGL ES에서 (0,0)은 왼쪽 하단 모서리이고, Y좌표가 증가하면 아래 그림 오른쪽과 같이 컨텍스트나 뷰의 상단으로 이동한다. OpenGL 라이브러리를 사용할 때는 뷰 좌표계 기준에서 OpenGL의 좌표계 기준으로 좌표 값을 변환해야 한다.

- 좌표계에서 하나의 점을 만들기 위해 일부 쿼츠 라이브러리 함수들은 부동 소수점 한쌍을 인자로 취한다.
- 반면에 다른 쿼츠 라이브러리 함수들은 CGPoint 구조체 값을 인자로 취한다. CGPoint 구조체는 부동 소수점 값인 x와 y를 멤버 변수로 가지고 있다.
- 쿼츠 라이브러리는 뷰나 그 외의 객체들에 대한 크기를 표현하기 위해 CGSize 구조체를 사용하며, CGSize구조체는 width와 height 두개의 부동소수점 값을 멤버 변수로 가지고 있다. 또한 쿼츠 라이브러리는 좌표계에서 직사각형을 정의하는데 자주 사용되는 CGRect라는 이름의 자료형을 사용한다.
- CGRect는 2개의 멤버 변수를 가지고 있는데, 하나는 직사각형의 왼쪽 상단의 좌표를 나타내는 origin이라는 이름의 CGPoint 구조체 변수이고, 다른 하나는 직사각형의 width와 height를 나타내는 size라는 이름의 CGSize 구조체 변수이다.

* 색의 정의하기
- UIColor는 UIKit이 제공하는 오브젝티브C클래스 중 하나이다.
- 코어 그래픽스 라이브러리 함수를 사용할 때 UIColor 객체를 인자로 직접 사용하는 일은 없다. 하지만 UIColor는 CGColor를 포함하고 있으므로 UIColor 인스턴스의 프로퍼티인 CGColor를 통해 CGColor 페러런스 값을 가질 수 있다.
- redColor라는 이름의 컨비니언스 메서드(오토릴리즈 풀을 사용할 수 있는 메서드)를 사용하여 UIColor 인스턴스를 하나 생성했다. 그러고 나서 CGColor 프로퍼티 값을 받아서 함수의 인자로 넘겨 주었다.
CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);

* 아이폰의 디스플레이를 위한 색상 이론 맛보기
- 현대 컴퓨터 그래픽스에서는 색을 표현하는 일반적인 방법으로 빨강(Red), 녹색(Green), 파랑(Blue) 그리고 알파(Alpha) 4가지 요소를 사용한다. 쿼츠 2D에서 이변수들은 CGFloat 타입이고 0.0과 1.0사이의 값을 가진다.

* 더 많은 색이 눈과 만나다
- 알파는 한 색이 다른 색 위에 그려질 때 최종적으로 화면에 나타낼 색을 결정하는 데 사용된다. 알파 값이 1.0이면 칠하는 색이 100퍼센트 불투명한 상태이고, 이전에 칠해진 색은 새로 칠하는 색에 의해 모두 가려진다. 알파가 사용될 때 색 모델은 가끔씩 RGBA 색상 모델을 참조한다.
- UIColor 객체에서 CGColor 값을 가져와 인자로 넘겨주기만 하면, 코어 그래픽스 라이브러리가 필요한 변환 작업을 처리 해준다.
-  OpenGL ES 라이브러리를 사용하여 작업을 할 때는 OpenGL ES가 쿼츠 라이브러리와 다른 색상 모델들을 지원하는 점을 명심하자. 왜냐하면 OpenGL ES 라이브러리는 RGBA 색상 모델에서 정의하는 색을 필요로 하기 때문이다.
- UIColor 클래스는 특정한 하나의 색으로 초기화 된 UIColor 색체를 반환하는 수많은 컨비언스 메서들을 가지고 있다. 컨비니언스 메서드는 RGBA 색상 모델을 사용하고, 컨비니언스 메서드를 통해 UIColor 인스턴스를 생성할 수 있다는 것이다.
- 만일 색상의 이름을 사용하는 컨비니언스 메서드를 사용하지 않고 좀 더 정교하게 색을 만들고 싶다면 아래와 같이 4개의 인자를 사용하여 하나의 색을 만들 수 있다.
return [UIColor colorWithRed:1.0f green:0.0f blue:0.0f alpha:1.0f];

* 컨텍스트에 이미지 그리기
- 쿼츠 2D는 컨텍스트에 직접 이미지를 그린다. 여기서는 코어 그래픽스 데이터 구조체인 CGImage를 사용하지 않고 대안으로 오브젝티브 C 클래스인 UIImage를 사용할 것이다.
- UIImage클래스는 이미지를 그려서 현재 컨텍스트에 나타내는 메서드들을 가지고 있다. 이미지가 컨텍스트의 어느 곳에 그려질것인지 지정하는 작업이 필요로 하므로, 이미지가 그려질 위치를 나타내기 위해 CGPointer를 사용하여 이미지의 왼쪽 상단의 꼭지점 좌표를 지정하거나 CGRect를 사용하여 이미지가 그려질 직사각형 틀을 지정할 수 있다.
- 사각형 틀이 이미지 크기보다 작으면, 이미지의 크기가 직사각형 틀에 맞게 바뀐다.
CGPoint drawPoint = CGPointMake(100.0f, 100.0f);
[image drawAtPoint:drawPoint];

* 도형 그리기 : 다각형,선,커브
- 직사각형이나 다각형을 그리기 위해 도형의 각을 계산하거나 선을 그리거나 수학적인 계산을 할 필요는 전혀 없다. 단지 쿼츠  라이브러리 함수만 호출하면 도형을 그리는것이 가능하다. 예를 들어 타원을 그리기 위해 타원의 크기를 표현하는 직사각형을 하나 정의하고 인자로 넘기면 코어 그래픽스 라이브러리가 직사각형 틀에 꼭 맞는 타원을 그릴 것이다.
CGRect theRect = CGMakeRect(0, 0, 100, 100);
CGContextAddEllipseInRect(context, theRect);
CGContextDrawPath(context, kCGPathFillStroke);

* 쿼츠 2D 툴 샘플러 : 패턴, 그레디언트, 대시 패턴
- 쿼츠 2D는 하나의 색이 아닌 그레디언트로 다각형을 채우는 기능을 지원한다. 단순한 실선이 아닌 대시 패턴의 선을 지원한다.

* QuartzFun 애플리케이션 만들기
- Xcode에서 뷰 기반의 애플리케이션 템플릿을 사용하여 새로운 프로젝트를 생성한다. 파일명은 QuartzFun으로 하자
- Classes폴더에 클래스를 추가 하자. 뷰 안에서 무언가를 그릴려면 UIView의 하위클래스를 하나 만들어야 한다. 그리고 이 하위클래스의 drawRect:메서드를 오버라이드하여 메서드 안에 드로잉 코드를 추가할 것이다.
- Cocoa Touch Class에서 Objective-C class를 선택하고 subclass of에서 UIView를 선택한다. 파일이름은 QuartzFunView.m이라고 하고 헤더 파일도 같이 생성한다.
- 오직 상수 정의만을 위한 헤더 파일을 만들어야 하므로, 파일 생성 창에서 Other 메뉴의 Empty File 템플릿을 선택하고 파일 이름은 Constants.h로 변경한다.
- UIColor 클래스는 랜덤 색을 반환하는 메서드를 가지고 있지 않기 때문에 랜덤 색을 제공하는 메서드를 하나 만들어야 한다. Empty File 템플릿을 사용하여 2개의 파일을 더 생성한다. 하나는 UIColor-Random.h와 UIColor-Random.m파일이다.

* 랜덤 색 만들기

#### UIColor-Random.h ####
#import 

@interface UIColor(Random)
+(UIColor *)randomColor;
@end

#### UIColor-Random.m ####
#import "UIColor-Random.h"

@implementation UIColor(Random)

+(UIColor *)randomColor
{
	// 메서드가 처음 실행되는지 구별하기 위해서 static 변수를 하나 선언
	// 애플리케이션이 실행되는 동안에 위의 메서드가 처음 실행되면 메서드는 랜덤 번호 생성기를 만들것이다.
	static BOOL seeded = NO;
	if(!seeded)
	{
		seeded = YES;
		srandom(time(NULL));
	}
	
	CGFloat red = (CGFloat)random()/(CGFloat)RAND_MAX;
	CGFloat blue = (CGFloat)random()/(CGFloat)RAND_MAX;
	CGFloat green = (CGFloat)random()/(CGFloat)RAND_MAX;
	return [UIColor colorWithRed:red green:green blue:blue alpha:1.0f];
}
@end

* 애플리케이션 상수 정의하기
#### Constants.h ####
typedef enum {
	kLineShape = 0,
	kRectShape,
	kEllipseShape,
	kImageShape
} ShapeType;

typedef enum {
	kRedColorTab = 0,
	kBlueColorTab,
	kYellowColorTab,
	kGreenColorTab,
	kRandomColorTab
} ColorTabIndex;

#define degreesToRadian(x) (M_PI * (x) / 180.0)

* QuartzFunView 뼈대 구현하기
#### QuartzFunView.h ####
#import 
#import "Constants.h"

@interface QuartzFunView : UIView 
{
	// 화면을 드래그할 때 사용된 손가락의 위치를 추적하는데 사용된다.
	// firstTouch 변수에는 사용자가 화면상에 처음 터치한 위치를 저장
	CGPoint firstTouch;
	// lastTouch 변수에는 손가락이 드래그하며 움직이는 위치를 지속적으로 저장하고 있다가 드래그가
	// 끝 났을 때 마지막으로 터치한 위치를 저장
	CGPoint lastTouch;
	// 사용자가 선택한 색을 저장하기 위해 UIColor의 포인터를 선언
	UIColor *currentColor;
	// 사용자가 그리려고 하는 도형을 저장하기 위해 ShapeType 변수를 하나 선언
	ShapeType shapeType;
	// 아래쪽 툴바의 맨 오른쪽 메뉴를 선택하였을 때 화면에 그릴 이미지를 저장하기 위해 UIImage프로퍼티를 선언
	UIImage *drawImage;
	// 랜덤 색을 선택하였는지 판단하기 위해서.
	BOOL useRandomColor;
}

@property CGPoint firstTouch;
@property CGPoint lastTouch;
@property (nonatomic, retain) UIColor *currentColor;
@property ShapeType shapeType;
@property (nonatomic, retain) UIImage *drawImage;
@property BOOL useRandomColor;

@end

#### QuartzFunView.m ####
#import "QuartzFunView.h"
#import "UIColor-Random.h"

@implementation QuartzFunView
@synthesize firstTouch;
@synthesize lastTouch;
@synthesize currentColor;
@synthesize shapeType;
@synthesize drawImage;
@synthesize useRandomColor;

// 뷰가 nib 파일로부터 로딩되기 때문에 먼저 initWithCoder:메서드를 구현해야 한다.
// nib 파일 안의 객체 인스턴스는 아카이브 객체의 형태로 저장된다.
// 결국 객체 인스턴스가 하나의 nib 파일로부터 로딩될때, init:메서드나 initWithFrame:메서드가 호출되는것이 아니라
// initWithCoder:메서드가 호출된다. 그래서 우리는 initWithCoder:메서드 안에 애플리케이션의 초기화 코드를 추가해야 한다.
-(id)initWithCoder:(NSCoder *)coder
{
	if(( self = [super initWithCoder:coder] ))
	{
		// 색을 빨간색으로 초기화하기 위해 useRandomColor값을 NO로 설정
		// 화면에 그릴 이미지 파일을 로딩해 두었다.
		self.currentColor = [UIColor redColor];
		self.useRandomColor = NO;
		if(drawImage == nil)
			self.drawImage = [UIImage imageNamed:@"iphone.png"];		
	}	
	return self;	
}

- (id)initWithFrame:(CGRect)frame {
    if ((self = [super initWithFrame:frame])) {
        // Initialization code
    }
    return self;
}

// drawRect:메서드 안에서 애플리케이션이 실제로 하는 일을 구현하게 될것이다.
- (void)drawRect:(CGRect)rect {
    // Drawing code
}

// UIView로부터 상속 받았으며 오버라이드 될 수 있는 아래 3개의 메서드들은 사용자가 화면을
// 터치한 위치를 찾기 위해 오버라이드하여 사용하는 메서드 들이다.
// touchesBegan:withEvent 메서드는 사용자의 손가락이 화면을 처음 터치 할때 호출된다.
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
	// 사용자가 랜덤 색을 선택하였을 때 UIColor 클래스에 추가했던 randomColor 메서드를 
	// 호출하여 색을 랜덤 색으로 설정할 수 있게 하였다.
	if(useRandomColor)
		self.currentColor = [UIColor randomColor];
	
	// 처음 터치한 위치를 참조하기 위해 현재 위치를 저장..
	UITouch *touch = [touches anyObject];
	firstTouch = [touch locationInView:self];
	lastTouch = [touch locationInView:self];
	// setNeedsDisplay를 호출하여 화면을 다시 그리게 설정
	[self setNeedsDisplay];
}

// 사용자가 화면에 손가락을 드래그하는 동안에 지속적으로 호출하는 메서드
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
	UITouch *touch = [touches anyObject];
	lastTouch = [touch locationInView:self];	
	[self setNeedsDisplay];
}

// 사용자가 화면에서 손가락을 떼는 순간 호출된다. 
// 이 메서드는 손가락의 마지막 위치를 lastTouch에 저장하고 뷰를 다시 그리게 요청한다.
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
	UITouch *touch = [touches anyObject];
	lastTouch = [touch locationInView:self];
	[self setNeedsDisplay];
}

- (void)dealloc {
	[currentColor release];
	[drawImage release];
    [super dealloc];
}

@end

* 뷰 컨트롤러에 아웃렛과 액션 추가하기
- 애플리케이션의 인터페이스가 2개의 세그먼트 컨트롤러로 이루어졌다는 것을 기억하자.
- 하나는 화면 위쪽에 위치하며 다른 하나는 화면 아래쪽에 위치한다.
- 위쪽의 컨트롤러는 사용자가 색을 선택할 때 사용된다. 아래쪽의 컨트롤러는 도형 선택을 할 때 사용한다.
- 위쪽의 세그먼트 컨트롤러를 가리킬 아웃렛을 만들어야 하며, 사용하지 않을 때는 위쪽의 컨트롤러 메뉴를 숨길수 있어야 한다.
- 2개의 메서드를 추가로 구현할 것인데, 하나는 색을 선택했을 때 호출될 메서드이고 다른 하나는 도형이 선택되었을 때 호출될 메서드 인다.

#### QuartzFunViewController.h ####
#import 

@interface QuartzFunViewController : UIViewController 
{
	UISegmentedControl *colorControl;	
}

@property (nonatomic, retain) IBOutlet UISegmentedControl *colorControl;

-(IBAction)changeColor:(id)sender;
-(IBAction)changeShape:(id)sender;

@end

#### QuartzFunViewController.m ####
#import "QuartzFunViewController.h"
#import "QuartzFunView.h"
#import "Constants.h"

@implementation QuartzFunViewController
@synthesize colorControl;

// 사용자가 선택한 색을 확인하고 선택한 색을 객체로 생성
-(IBAction)changeColor:(id)sender
{
	UISegmentedControl *control = sender;
	NSInteger index = [control selectedSegmentIndex];
	
	// 현재 뷰를 QuartzFunView 타입으로 캐스팅
	QuartzFunView *quartzView = (QuartzFunView *)self.view;
	
	switch(index)
	{
		case kRedColorTab :
			quartzView.currentColor = [UIColor redColor];
			quartzView.useRandomColor = NO;
			break;
			
		case kBlueColorTab :
			quartzView.currentColor = [UIColor blueColor];
			quartzView.useRandomColor = NO;
			break;
			
		case kYellowColorTab :
			quartzView.currentColor = [UIColor yellowColor];
			quartzView.useRandomColor = NO;
			break;
			
		case kGreenColorTab :
			quartzView.currentColor = [UIColor greenColor];
			quartzView.useRandomColor = NO;
			break;
			
		case kRandomColorTab :
			quartzView.useRandomColor = YES;
			break;
			
		default :
			break;
			
	}
	
}

-(IBAction)changeShape:(id)sender
{
	UISegmentedControl *control = sender;
	
	[(QuartzFunView *)self.view setShapeType:[control selectedSegmentIndex]];
	
	if([control selectedSegmentIndex] == kImageShape)
		colorControl.hidden = YES;
	else
		colorControl.hidden = NO;
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
}

- (void)viewDidUnload {
	self.colorControl = nil;
	[super viewDidUnload];
}

- (void)dealloc {
	[colorControl release];
    [super dealloc];
}

@end

* QuartzFunViewController.xib 업데이트 하기
- QuartzFunViewController.xib 파일을 더블클릭하여 인터페이스 빌더를 실행한다.
- 맨 먼저 해야 할 일은 뷰의 클래스를 변경하는 일이다. QuartzFunViewController.xib라는 제목의 창 안의 View 아이콘을 클릭하고 Func+4를 눌러 아이텐터티 인스펙터 창을 열게 한다. 그리고 나서 클래스를 UIView에서 QuartzFunView로 변경한다.
- 라이브러리 창에서 Navigation Bar를 찾아서 상단에 추가 한다. Navigation Controller가 아니다.
- 다음으로 라이브러리 창에서 Segmented Control을 찾아서 네비게이션 바의 바로 위에 드래그하여 배치한다.컨트롤을 네비게이션 바의 왼쪽이나 오른쪽이 아닌 중앙에 배치한다. 세그먼트 컨트롤의 양쪽에 붙어 있는 크기 조절 점중에 하나를 잡아서 네비게이션 바의 너비와 같아지도록 늘려준다. 세그먼트 컨트롤이 선택된 상태에서 Func+1를 눌러서 속성 인스펙터 창이 나타나게 하고 세그먼트 수를 2에서 5로 늘린다. 레이블은 순서대로 Red, Blue, Yellow, Green, Random으로 변경한다.
- File's Owner 아이콘 위에서 컨트롤-드래그 하여 세그먼트 컨트롤에 끌어 놓은 뒤, colorControl 아웃렛과 연결한다. Func+2를 누르면 커넥션 인스펙터창이 나타나는데 Value Changed 이벤트를 드래그하여 File's Owner 아이콘위에 글어 놓고 ChangeColor: 액션을 선택한다.
- 라이브러리 창에서 Toolbar를 찾아서, 뷰 윈도우의 하단에 끌어다 놓는다. 툴바는 피요 없는 버튼이 있기 때문에 버튼을 삭제 한다. 또 다른 세그먼트 컨트롤 하나를 드래그 하여, 툴바 위에 붙여 올려놓는다. 세그먼트 컨트롤을 클릭하고 툴바의 양쪽과의 공간이 조금 남아 있도록 크기를 변경한다.
- 분할 컨트롤을 선택한 채로 Func+1을 눌러 속성 인스펙처 창을 띄우고, 분할 컨트롤의 수를 2에서 4로 변경한다 레이블은 차례로 Line, Rect, Ellipse, Image로 변경한다. 작업 찾을 커넥션 인스펙터로 바꾸고, Value Changed이벤트를 File's Owner의 changeShape: 액션에 연결한다.

* 선 그리기
- xcode에서 QuartzFunView.m에서 drawRect:메서드를 아래와 같이 변경한다.
#### QuartzFunView.m ####
// drawRect:메서드 안에서 애플리케이션이 실제로 하는 일을 구현하게 될것이다.
- (void)drawRect:(CGRect)rect 
{
	// 선을 그릴 위치를 정하기 위해 현재 컨텍스트의 레퍼런스를 가져온다.
    CGContextRef context = UIGraphicsGetCurrentContext();	
	// 선의 너비를 2 픽셀로 설정한다.
	CGContextSetLineWidth(context, 2.0f);
	// 선을 그릴때 사용할 색을 설정한다. CGColor 프로퍼티 인자를 필요로 한다.	
	CGContextSetStrokeColorWithColor(context, currentColor.CGColor);
	
	switch (shapeType)
	{
		case kLineShape :
			// 사용자가 처음 터치한 부분으로 보이지 않는 펜을 옮긴다.
			CGContextMoveToPoint(context, firstTouch.x, firstTouch.y);
			// 사용자가 마지막으로 터치한 위치로 선을 그린다. 아직은 보이지 않는 펜으로
			CGContextAddLineToPoint(context, lastTouch.x, lastTouch.y);
			// 눈에 보이는 선으로 그린다.
			CGContextStrokePath(context);
			break;
		case kRectShape :
			break;
		case kEllipseShape :
			break;
		case kImageShape :
			break;
		default :
			break;
	}	
}


* 직사각형과 타원 그리기
- draw:메서드를 다시 수정
#### QuartzFunView.m ####

// drawRect:메서드 안에서 애플리케이션이 실제로 하는 일을 구현하게 될것이다.
- (void)drawRect:(CGRect)rect 
{
	// 선을 그릴 위치를 정하기 위해 현재 컨텍스트의 레퍼런스를 가져온다.
    CGContextRef context = UIGraphicsGetCurrentContext();	
	// 선의 너비를 2 픽셀로 설정한다.
	CGContextSetLineWidth(context, 2.0f);
	// 선을 그릴때 사용할 색을 설정한다. CGColor 프로퍼티 인자를 필요로 한다.	
	CGContextSetStrokeColorWithColor(context, currentColor.CGColor);
	
	// 타원과 직사각형에 불투명 색을 채워야 하므로 currentColor를 사용하여 색을 채우는 함수를 추가
	CGContextSetFillColorWithColor(context, currentColor.CGColor);
	// 사용자의 드래그를 기준으로 직사각형의 정보를 저장하기 위해 currentRect변수를 선언
	// CGRect는 origin과 size라는 두 개의 멤버 변수를 가지고 있다는것을 기억.
	// x,y 좌표와 너비, 높이 값을 인자로 사용하여 CGRectMake()를 호출하면 CGRect를 생성할 수 있고
	// CGRect를 사용하여 직사각형을 그릴수 있다.
	// 사용자가 특정 방향으로 드래그 하면, origin값은 드래그 방향에 따라서 달라질것이다. 
	// origin값을 결정하기 위해 2개의 좌표의 x와 y를 각각 비교하여 작은 쪽의 값을 사용하였다.
	// x좌표 사이의 절대값과 y좌료 사이의 절대값을 구하여 size 값을 결정..
	CGRect currentRect = CGRectMake(
									(firstTouch.x > lastTouch.x) ? lastTouch.x : firstTouch.x,
									(firstTouch.y > lastTouch.y) ? lastTouch.y : firstTouch.y,
									fabsf(firstTouch.x - lastTouch.y),
									fabsf(firstTouch.y - lastTouch.y));
	
	switch (shapeType)
	{
		case kLineShape :
			// 사용자가 처음 터치한 부분으로 보이지 않는 펜을 옮긴다.
			CGContextMoveToPoint(context, firstTouch.x, firstTouch.y);
			// 사용자가 마지막으로 터치한 위치로 선을 그린다. 아직은 보이지 않는 펜으로
			CGContextAddLineToPoint(context, lastTouch.x, lastTouch.y);
			// 눈에 보이는 선으로 그린다.
			CGContextStrokePath(context);
			break;
		case kRectShape :
			// 직사각형을 추가
			CGContextAddRect(context, currentRect);
			// 색을 칠한다.
			CGContextDrawPath(context, kCGPathFillStroke);
			break;
		case kEllipseShape :
			// 타원을 추가
			CGContextAddEllipseInRect(context, currentRect);
			// 색을 칠한다.
			CGContextDrawPath(context, kCGPathFillStroke);
			break;
		case kImageShape :
			break;
		default :
			break;
	}	
}



* 이미지 그리기
#### QuartzFunView.m ####

// drawRect:메서드 안에서 애플리케이션이 실제로 하는 일을 구현하게 될것이다.
- (void)drawRect:(CGRect)rect 
{
	// 선을 그릴 위치를 정하기 위해 현재 컨텍스트의 레퍼런스를 가져온다.
    CGContextRef context = UIGraphicsGetCurrentContext();	
	// 선의 너비를 2 픽셀로 설정한다.
	CGContextSetLineWidth(context, 2.0f);
	// 선을 그릴때 사용할 색을 설정한다. CGColor 프로퍼티 인자를 필요로 한다.	
	CGContextSetStrokeColorWithColor(context, currentColor.CGColor);
	
	// 타원과 직사각형에 불투명 색을 채워야 하므로 currentColor를 사용하여 색을 채우는 함수를 추가
	CGContextSetFillColorWithColor(context, currentColor.CGColor);
	// 사용자의 드래그를 기준으로 직사각형의 정보를 저장하기 위해 currentRect변수를 선언
	// CGRect는 origin과 size라는 두 개의 멤버 변수를 가지고 있다는것을 기억.
	// x,y 좌표와 너비, 높이 값을 인자로 사용하여 CGRectMake()를 호출하면 CGRect를 생성할 수 있고
	// CGRect를 사용하여 직사각형을 그릴수 있다.
	// 사용자가 특정 방향으로 드래그 하면, origin값은 드래그 방향에 따라서 달라질것이다. 
	// origin값을 결정하기 위해 2개의 좌표의 x와 y를 각각 비교하여 작은 쪽의 값을 사용하였다.
	// x좌표 사이의 절대값과 y좌료 사이의 절대값을 구하여 size 값을 결정..
	CGRect currentRect = CGRectMake(
									(firstTouch.x > lastTouch.x) ? lastTouch.x : firstTouch.x,
									(firstTouch.y > lastTouch.y) ? lastTouch.y : firstTouch.y,
									fabsf(firstTouch.x - lastTouch.y),
									fabsf(firstTouch.y - lastTouch.y));
	
	switch (shapeType)
	{
		case kLineShape :
			// 사용자가 처음 터치한 부분으로 보이지 않는 펜을 옮긴다.
			CGContextMoveToPoint(context, firstTouch.x, firstTouch.y);
			// 사용자가 마지막으로 터치한 위치로 선을 그린다. 아직은 보이지 않는 펜으로
			CGContextAddLineToPoint(context, lastTouch.x, lastTouch.y);
			// 눈에 보이는 선으로 그린다.
			CGContextStrokePath(context);
			break;
		case kRectShape :
			// 직사각형을 추가
			CGContextAddRect(context, currentRect);
			// 색을 칠한다.
			CGContextDrawPath(context, kCGPathFillStroke);
			break;
		case kEllipseShape :
			// 타원을 추가
			CGContextAddEllipseInRect(context, currentRect);
			// 색을 칠한다.
			CGContextDrawPath(context, kCGPathFillStroke);
			break;
		case kImageShape : {
			// 위에 보면 중괄호가 있다. GCC는 case문의 다음에 변수를 선언하면 컴파일 에러를 발생한다.
			// 이 중괄호는 GCC에게 불평하지 말라고 알려주는 표시 이다.
			// 이미지의 중심 좌표를 계산하였다.
			CGFloat horizontalOffSet = drawImage.size.width / 2;
			CGFloat verticalOffSet = drawImage.size.height / 2;
			CGPoint drawPoint = CGPointMake(lastTouch.x - horizontalOffSet, lastTouch.y - verticalOffSet);
			// 이미지를 화면에 그리게 한다.
			[drawImage drawAtPoint:drawPoint];
			break;
		}
		default :
			break;
	}	
}


* QuartzFun 애플리케이션 최적화하기
- 애플리케이션이 좀 더 복잡해지고 상대적으로 느린 프로세서상에서 실행된다면 애플리케이션이 느리다고 느낄 것이다. 이문제는 QuartzFunView.m파일의 touchesMoved:메서드와 touchesEnded:메서드 안에서 발생한다.
[self setNeedsDisplay];

- 분명 위 코드는 뷰의 내용이 변경되었으니 뷰를 다시 그리도록 호출하는 것이다. 동작하는 데 문제는 없지만 화면상에 매우 작은 변화만 있어도 뷰의 전 영역을 지우고 다시 그린다.
- 드래그하는 동안 계속해서 뷰의 전체를 지우고 다시 그리는 대신 setNeedsDisplayInRect:메서드를 사용하여 필요한 부분만을 지우고 다시 그릴수 있다. setNeedsDisplayInRect:메서드는 NSView 클래스의 멤버이고, 이 메서드는 지우고 다시 그리려는 영역을 직사각형으로 표시한다. 이 메서드를 사용하여 현재 드로잉 동작에 의해 다시 그려져야만 하는 뷰의 영역을 표시해 두는 것으로 좀 더 효율적으로 드로잉을 처리할 수 있다.
- setNeedsDisplayInRect:메서드를 사용하여 단순히 firstTouch와 lastTouch 사이의 직사각형 영역만을 다시 그리는 것이 아니라 계속되는 드래그로 사용자가 그리려는 도형의 영역에 포함되었다가 제외되서 흰 배경으로 보여야 하는 영역들도 다시 그려져야 한다. 사용자가 화면을 터치하고 이리저리 드래그 할때 firstTouch와 lastTouch 사이의 부분만을 다시 그린다면, 우리가 제공하려는 기능과는 달리 흰 배경으로 다시 초기화되어야 하는 화면들이 다시 그려지지 않은 채 그대로 남게 될 것이다.
- CGRect 인스턴스 변수 하나에 새롭게 도형이 그려질 영역과 흰색으로 다시 그려져야 할 영역 모두를 저장하는 것이다. tochesBegan:메서드 안에서 인스턴스 변수를 사용자가 터치한 위치로 재 설정한다. 그런다음 tochesMoved:메서드와 touchesEnded:메서드에서 CGRectUnion:메서드를 호출하여 현재 사용자의 드래그로 인해 그려진 직사각형 영역과 이미 그려진 직사각형의 영역을 합한 새로운 직사각형의 영역을 저장한다. 이 새 직사각형 영역을 setNeedsDisplayInRect:에게 넘겨 주어 새로 그려질 뷰의 영역을 지정할수 있다.

#### QuartzFunView.h ####
#import 
#import "Constants.h"

@interface QuartzFunView : UIView 
{
	// 화면을 드래그할 때 사용된 손가락의 위치를 추적하는데 사용된다.
	// firstTouch 변수에는 사용자가 화면상에 처음 터치한 위치를 저장한다.
	CGPoint firstTouch;
	// lastTouch 변수에는 손가락이 드래그하며 움직이는 위치를 지속적으로 저장하고 있다가 드래그가
	// 끝 났을 때 마지막으로 터치한 위치를 저장한다.
	CGPoint lastTouch;
	// 사용자가 선택한 색을 저장하기 위해 UIColor의 포인터를 선언
	UIColor *currentColor;
	// 사용자가 그리려고 하는 도형을 저장하기 위해 ShapeType 변수를 하나 선언
	ShapeType shapeType;
	// 아래쪽 툴바의 맨 오른쪽 메뉴를 선택하였을 때 화면에 그릴 이미지를 저장하기 
	// 위해 UIImage 프로퍼티를 선언
	UIImage *drawImage;
	// 랜덤 색을 선택하였는지 판단하기 위해서..
	BOOL useRandomColor;
	// 다시 그려질 영역을 저장하는데 사용..
	CGRect redrawRect;
}

@property CGPoint firstTouch;
@property CGPoint lastTouch;
@property (nonatomic, retain) UIColor *currentColor;
@property ShapeType shapeType;
@property (nonatomic, retain) UIImage *drawImage;
@property BOOL useRandomColor;
@property (readonly) CGRect currentRect;
@property CGRect redrawRect;

@end

#### QuartzFunView.m ####
#import "QuartzFunView.h"
#import "UIColor-Random.h"

@implementation QuartzFunView
@synthesize firstTouch;
@synthesize lastTouch;
@synthesize currentColor;
@synthesize shapeType;
@synthesize drawImage;
@synthesize useRandomColor;
@synthesize redrawRect;
@synthesize currentRect;

-(CGRect)currentRect
{
	return CGRectMake(
					  (firstTouch.x > lastTouch.x) ? lastTouch.x : firstTouch.x,
					  (firstTouch.y > lastTouch.y) ? lastTouch.y : firstTouch.y,
					  fabsf(firstTouch.x - lastTouch.x),
					  fabsf(firstTouch.y - lastTouch.y));
}

// 뷰가 nib 파일로부터 로딩되기 때문에 먼저 initWithCoder:메서드를 구현해야 한다.
// nib 파일 안의 객체 인스턴스는 아카이브 객체의 형태로 저장된다.
// 결국 객체 인스턴스가 하나의 nib 파일로부터 로딩될때, init:메서드나 initWithFrame:메서드가 호출되는것이 아니라
// initWithCoder:메서드가 호출된다. 그래서 우리는 initWithCoder:메서드 안에 애플리케이션의 초기화 코드를 추가해야 한다.
-(id)initWithCoder:(NSCoder *)coder
{
	if(( self = [super initWithCoder:coder] ))
	{
		// 색을 빨간색으로 초기화하기 위해 useRandomColor값을 NO로 설정
		// 화면에 그릴 이미지 파일을 로딩해 두었다.
		self.currentColor = [UIColor redColor];
		self.useRandomColor = NO;
		if(drawImage == nil)
			self.drawImage = [UIImage imageNamed:@"iphone.png"];		
	}	
	return self;	
}

- (id)initWithFrame:(CGRect)frame {
    if ((self = [super initWithFrame:frame])) {
        // Initialization code
    }
    return self;
}

// drawRect:메서드 안에서 애플리케이션이 실제로 하는 일을 구현하게 될것이다.
- (void)drawRect:(CGRect)rect 
{
	// 선을 그릴 위치를 정하기 위해 현재 컨텍스트의 레퍼런스를 가져온다.
    CGContextRef context = UIGraphicsGetCurrentContext();	
	// 선의 너비를 2 픽셀로 설정한다.
	CGContextSetLineWidth(context, 2.0f);
	// 선을 그릴때 사용할 색을 설정한다. CGColor 프로퍼티 인자를 필요로 한다.	
	CGContextSetStrokeColorWithColor(context, currentColor.CGColor);
	
	// 타원과 직사각형에 불투명 색을 채워야 하므로 currentColor를 사용하여 색을 채우는 함수를 추가
	CGContextSetFillColorWithColor(context, currentColor.CGColor);
	// 사용자의 드래그를 기준으로 직사각형의 정보를 저장하기 위해 currentRect변수를 선언
	// CGRect는 origin과 size라는 두 개의 멤버 변수를 가지고 있다는것을 기억.
	// x,y 좌표와 너비, 높이 값을 인자로 사용하여 CGRectMake()를 호출하면 CGRect를 생성할 수 있고
	// CGRect를 사용하여 직사각형을 그릴수 있다.
	// 사용자가 특정 방향으로 드래그 하면, origin값은 드래그 방향에 따라서 달라질것이다. 
	// origin값을 결정하기 위해 2개의 좌표의 x와 y를 각각 비교하여 작은 쪽의 값을 사용하였다.
	// x좌표 사이의 절대값과 y좌료 사이의 절대값을 구하여 size 값을 결정..
	//CGRect currentRect = CGRectMake(
	//								(firstTouch.x > lastTouch.x) ? lastTouch.x : firstTouch.x,
	//								(firstTouch.y > lastTouch.y) ? lastTouch.y : firstTouch.y,
	//								fabsf(firstTouch.x - lastTouch.y),
	//								fabsf(firstTouch.y - lastTouch.y));
	
	switch (shapeType)
	{
		case kLineShape :
			// 사용자가 처음 터치한 부분으로 보이지 않는 펜을 옮긴다.
			CGContextMoveToPoint(context, firstTouch.x, firstTouch.y);
			// 사용자가 마지막으로 터치한 위치로 선을 그린다. 아직은 보이지 않는 펜으로
			CGContextAddLineToPoint(context, lastTouch.x, lastTouch.y);
			// 눈에 보이는 선으로 그린다.
			CGContextStrokePath(context);
			break;
		case kRectShape :
			// 직사각형을 추가
			//CGContextAddRect(context, currentRect);
			CGContextAddRect(context, self.currentRect);
			// 색을 칠한다.
			CGContextDrawPath(context, kCGPathFillStroke);
			break;
		case kEllipseShape :
			// 타원을 추가
			//CGContextAddEllipseInRect(context, currentRect);			
			CGContextAddEllipseInRect(context, self.currentRect);
			// 색을 칠한다.
			CGContextDrawPath(context, kCGPathFillStroke);
			break;
		case kImageShape : {
			// 위에 보면 중괄호가 있다. GCC는 case문의 다음에 변수를 선언하면 컴파일 에러를 발생한다.
			// 이 중괄호는 GCC에게 불평하지 말라고 알려주는 표시 이다.
			// 이미지의 중심 좌표를 계산하였다.
			CGFloat horizontalOffSet = drawImage.size.width / 2;
			CGFloat verticalOffSet = drawImage.size.height / 2;
			CGPoint drawPoint = CGPointMake(lastTouch.x - horizontalOffSet, lastTouch.y - verticalOffSet);
			// 이미지를 화면에 그리게 한다.
			[drawImage drawAtPoint:drawPoint];
			break;
		}
		default :
			break;
	}
	
	
}

// UIView로부터 상속 받았으며 오버라이드 될 수 있는 아래 3개의 메서드들은 사용자가 화면을
// 터치한 위치를 찾기 위해 오버라이드하여 사용하는 메서드 들이다.
// touchesBegan:withEvent 메서드는 사용자의 손가락이 화면을 처음 터치 할때 호출된다.
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
	// 사용자가 랜덤 색을 선택하였을 때 UIColor 클래스에 추가했던 randomColor 메서드를 
	// 호출하여 색을 랜덤 색으로 설정할 수 있게 하였다.
	if(useRandomColor)
		self.currentColor = [UIColor randomColor];
	
	// 처음 터치한 위치를 참조하기 위해 현재 위치를 저장..
	UITouch *touch = [touches anyObject];
	firstTouch = [touch locationInView:self];
	lastTouch = [touch locationInView:self];
	// setNeedsDisplay를 호출하여 화면을 다시 그리게 설정
	[self setNeedsDisplay];
}

// 사용자가 화면에 손가락을 드래그하는 동안에 지속적으로 호출하는 메서드
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
	UITouch *touch = [touches anyObject];
	lastTouch = [touch locationInView:self];	
	//[self setNeedsDisplay];
	
	if(shapeType == kImageShape)
	{
		CGFloat horizontalOffset = drawImage.size.width / 2;
		CGFloat verticalOffset = drawImage.size.height / 2;
		redrawRect = CGRectUnion(redrawRect,
								 CGRectMake(lastTouch.x - horizontalOffset, lastTouch.y - verticalOffset,
											drawImage.size.width, drawImage.size.height));
	}
	// 그려져 있는 영역과 사용자가 드래그 한 영역을 계속 누적해서 저장해야 한다.
	redrawRect = CGRectUnion(redrawRect, self.currentRect);
	// rect영역 부분만을 지우고 다시 그린다.
	[self setNeedsDisplayInRect:redrawRect];
}

// 사용자가 화면에서 손가락을 떼는 순간 호출된다. 
// 이 메서드는 손가락의 마지막 위치를 lastTouch에 저장하고 뷰를 다시 그리게 요청한다.
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
	UITouch *touch = [touches anyObject];
	lastTouch = [touch locationInView:self];
	//[self setNeedsDisplay];
	
	if(shapeType == kImageShape)
	{
		CGFloat horizontalOffset = drawImage.size.width / 2;
		CGFloat verticalOffset = drawImage.size.height / 2;
		redrawRect = CGRectUnion(redrawRect, 
								 CGRectMake(lastTouch.x - horizontalOffset, lastTouch.y - verticalOffset,
														drawImage.size.width, drawImage.size.height));
	} else {
		redrawRect = CGRectUnion(redrawRect, self.currentRect);
	}
	// 그려져 있는 영역과 사용자가 드래그 한 영역을 계속 누적해서 저장해야 한다.
	redrawRect = CGRectInset(redrawRect, -2.0f, -2.0f);
	
	[self setNeedsDisplayInRect:redrawRect];
	
}

- (void)dealloc {
	[currentColor release];
	[drawImage release];
    [super dealloc];
}
@end


- 그려져 있는 영역과 사용자가 드래그한 영역을 계속해서 누적하지 않으면 아래와 같은 결과가 나온다.


* OpenGL의 기본
- OpenGL ES와 쿼츠 2D는 드로잉에 있어서 근본적으로 다른 방법으로 접근한다.
- http://www.khronos.org/opengles를 둘러보는것도 좋다.
- 아니면 다음 URL에서 "tutorial"이라는 단어로 검색하는 것이 더 나을 수 있따. http://khronos.org/developers/resources/opengles/
- 아니면 제프 라마키의 아이폰 블로그의 OpenGL 튜토리얼도 참고 . http://iphonedevelopment.blogspot.com/

* GLFun 애플리케이션 만들기

- Xcode에서 뷰 기반의 새 애플리케이션을 하나 만들고, GLFun라고 부르자. QuartzFun프로젝트로부터 Constants.h, UIColor-Random.h, UIColor-Random.m, iPhone.png 파일을 새 프로젝트로 복사한다.

#### GLFunViewController.h ####
#import 

@interface GLFunViewController : UIViewController 
{
	UISegmentedControl *colorControl;
}

@property (nonatomic, retain) IBOutlet UISegmentedControl *colorControl;
-(IBAction)changeColor:(id)sender;
-(IBAction)changeShape:(id)sender;
@end

#### GLFunViewController.m ####
#import "GLFunViewController.h"
#import "Constants.h"
#import "GLFunView.h"
#import "UIColor-Random.h"

@implementation GLFunViewController
@synthesize colorControl;

-(IBAction)changeColor:(id)sender
{
	UISegmentedControl *control = sender;
	NSInteger index = [control selectedSegmentIndex];
	
	GLFunView *glView = (GLFunView *)self.view;
	
	switch (index)
	{
		case kRedColorTab:
			glView.currentColor = [UIColor redColor];
			glView.useRandomColor = NO;
			break;
		
		case kBlueColorTab:
			glView.currentColor = [UIColor blueColor];
			glView.useRandomColor = NO;
			break;
			
		case kYellowColorTab:
			glView.currentColor = [UIColor yellowColor];
			glView.useRandomColor = NO;
			break;
			
		case kGreenColorTab:
			glView.currentColor = [UIColor greenColor];
			glView.useRandomColor = NO;
			break;
			
		case kRandomColorTab:
			glView.useRandomColor = YES;
			break;
			
		default :
			break;
	}
}

-(IBAction)changeShape:(id)sender
{
	UISegmentedControl *control = sender;
	[(GLFunView *)self.view setShapeType:[control selectedSegmentIndex]];
	if([control selectedSegmentIndex] == kImageShape)
		[colorControl setHidden:YES];
	else
		[colorControl setHidden:NO];
}

- (void)didReceiveMemoryWarning {
	// Releases the view if it doesn't have a superview.
    [super didReceiveMemoryWarning];
}

- (void)viewDidUnload {
	self.colorControl = nil;
	[super viewDidUnload];
}


- (void)dealloc {
	[colorControl release];
    [super dealloc];
}

@end

- 시작하기에 앞서 프로젝트에 몇 개의 파일을 더 추가하자. 12 GLFun 폴더 안을 보면, Texture2D.h, Texture2D.m, OpenGLES2DView.h, OpenGLES2DView.m라는 이름의 파일 4개를 프로젝트에 추가 한다.
- Texture2D.h와 Texture2D.m에 들어 있는 코드들은 애플에서 작성한 것이며, OpenGL ES를 사용하여 쉽게 이미지를 그릴 수 있게 해주는 코드들이 들어 있다.
- 두번째 파일은 애플에서 기본적으로 제공하는 샘플코드를 토대로 만들어진 파일이다. 이 파일에는 OpenGL을 사용하여 2차워 드로잉을 하는데 필요한 설정에 관련된 코드들이 들어 있다. OpenGL을 설정하는 일은 매우 복잡한 작업이므로 이미 필요한 설정이 구현 된 코드를 제공하는 것이다.
- OpenGL ES는 본질적으로 스프라이트나 이미지를 그리는 메서드를 가지고 있지 않다. 그것은 이미지의 한 종류인 텍스처만을 가지고 있으며 텍스처는 반드시 도형이나 객체 위에 그려진다. OpenGL ES상에서 이미지를 그리는 방법은 사각형을 그리는 것이고, 사각형을 그리고 나면 텍스처를 사각형과 매핑하여 폴리곤의 크기와 정확히 일치하도록 만들어야 한다. Texture2D는 이와 같은 비교적 복잡한 과정을 캡슐화하여 사용자에게 제공한다.
- OpenGLES2DView는 UIView의 하위클래스이고 드로잉을 하기 위해 OpenGL을 사용한다. OpenGLES의 좌표계와 뷰의 좌표계를 1:1로 매핑하기 위해 뷰를 설정할 것이다. OpenGL ES는 3차원 시스템이다. OpenGLES2DView는 OpenGL의 3D 세계를 2D 뷰의 픽셀로 매핑한다. 뷰와 OpenGL의 컨텍스트를 1:1로 매핑하였다고 해도, y좌표는 여전히 뒤집혀서 보이므로 뷰 좌표계로부터 y좌표를 변환해야 한다. 뷰 좌표계에서는 y값이 증가하면 아래로 이동하고, OpenGL 좌표계에서는 y값이 증가하면 위로 이동한다.
- OpenGLES2DView를 사용하기 위해, OpenGLES2DView의 하위클래스로 변경한 뒤 조금 후에 보게 될 실제로 드로잉할 draw 메서드를 구현할 것이다.

#### GLFunView.h ####
#import 
#import "Constants.h"
#import "OpenGLES2DView.h"

@class Texture2D;

@interface GLFunView:OpenGLES2DView
{
	CGPoint firstTouch;
	CGPoint lastTouch;
	UIColor *currentColor;
	BOOL useRandomColor;
	ShapeType shapeType;
	Texture2D *sprite;
}

@property CGPoint firstTouch;
@property CGPoint lastTouch;
@property (nonatomic, retain) UIColor *currentColor;
@property BOOL useRandomColor;
@property ShapeType shapeType;
@property (nonatomic, retain) Texture2D *sprite;

@end

#### GLFunView.m ####
#import "GLFunView.h"
#import "UIColor-Random.h"
#import "Texture2D.h"

@implementation GLFunView
@synthesize firstTouch;
@synthesize lastTouch;
@synthesize currentColor;
@synthesize useRandomColor;
@synthesize shapeType;
@synthesize sprite;

-(id)initWithCoder:(NSCoder *)coder
{
	if(self = [super initWithCoder:coder])
	{
		self.currentColor = [UIColor redColor];
		self.useRandomColor = NO;
		self.sprite = [[Texture2D alloc] initWithImage:[UIImage imageNamed:@"iphone.png"]];
		
		// Texture 2D 객체 생성
		glBindTexture(GL_TEXTURE_2D, sprite.name);
	}
	
	return self;
}

-(void)draw
{
	// OpenGL은 드로잉 과정을 3단계로 구성하고 있다.
	// 첫번째 컨텍스트에 드로잉한다.
	// 둘째 드로잉을 마치면, 컨텍스트를 렌더링하여 버퍼에 담는다.
	// 세번째 실제로 픽셀들이 화면에 그려질 때 렌더링 버퍼의 내용을 화면에 보여준다.
	
	// 가상세계를 초기화
	glLoadIdentity();
	// 배경색을 흰색으로 초기화 한다.
	glClearColor(0.78f, 0.78f, 0.78f, 1.0f);
	glClear(GL_COLOR_BUFFER_BIT);
	
	// UIColor 객체로부터 각각의 RGB값을 가져와서 glColor4f함수를 호출하여 OpenGL의 드로잉 색을 설정
	// 각각의 색을 가져올때 편리하게 사용할수 있는 UIColor객체를 사용하면된다.
	CGColorRef color = currentColor.CGColor;
	const CGFloat *components = CGColorGetComponents(color);
	CGFloat red = components[0];
	CGFloat green = components[1];
	CGFloat blue = components[2];	
	glColor4f(red, green, blue, 1.0f);
	
	switch (shapeType)
	{
		case kLineShape : {
			// OpenGL ES의 맵 텍스처 기능을 제거 한다.
			glDisable(GL_TEXTURE_2D);
			
			// 선을 그리려면 2개의 점 vertices가 필요하다. 
			// 2개의 점의 좌표를 저장하려면 길이가 4인 배열을 하나 만들어야 한다.
			// OpenGL에서 좌표는 CGPoint와 같은 구조체에 들어있지 않다. 
			// 대신에 화면에 그릴 도형을 구성할 모든 점의 좌표를 배열로 만들어야 한다.
			// OpenGL을 사용하여 (100,150)에서 (200,250)으로 선을 그릴려면 다음과 같은 버텍스 배열이 필요하다.
			// vertices[0] = 100;
			// vertices[1] = 150;
			// vertices[2] = 200;
			// vertices[3] = 250;
			GLfloat vertices[4];
			
			vertices[0] = firstTouch.x;
			vertices[1] = self.frame.size.height - firstTouch.y;
			vertices[2] = lastTouch.x;
			vertices[3] = self.frame.size.height - lastTouch.y;
			
			// 선의 너비를 2.0으로 설정
			glLineWidth(2.0);			
			// 버텍스 배열을 OpenGL ES에 넘겨준다.
			glVertexPointer(2, GL_FLOAT, 0, vertices);
			// OpenGL ES가 배열을 사용하여 선을 그리게 한다.
			glDrawArrays(GL_LINES, 0, 2);
			
			break;
		}
		case kRectShape : {
			glDisable(GL_TEXTURE_2D);
			
			GLfloat vertices[8];
			
			GLfloat minX = (firstTouch.x > lastTouch.x) ? lastTouch.x : firstTouch.x;
			GLfloat minY = (self.frame.size.height - firstTouch.y > self.frame.size.height - lastTouch.y) ? 
			self.frame.size.height - lastTouch.y : self.frame.size.height - firstTouch.y;
			
			GLfloat maxX = (firstTouch.x > lastTouch.x) ? firstTouch.x : lastTouch.x;
			GLfloat maxY = (self.frame.size.height - firstTouch.y > self.frame.size.height - lastTouch.y) ?
			self.frame.size.height - firstTouch.y : self.frame.size.height - lastTouch.y;
			
			vertices[0] = maxX;
			vertices[1] = maxY;
			vertices[2] = minX;
			vertices[3] = maxY;
			vertices[4] = minX;
			vertices[5] = minY;
			vertices[6] = maxX;
			vertices[7] = minY;
			
			glVertexPointer(2, GL_FLOAT, 0, vertices);
			glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
			break;
		}
		case kEllipseShape : {
			// OpenGL ES는 타원을 그리는 라이브러리를 제공하지 않고 있다.
			glDisable(GL_TEXTURE_2D);
			
			// 좌표를 구성하기 위해서 720개의 GLfloats 변수를 저장하기 위해 버텍스 배열을 하나 정의한다.
			// 360개의 좌표를 구성하기 위해서는 720개의 변수가 필요.. 2개가 한쌍.
			GLfloat vertices[720];
			
			// firstTouch와 lastTouch 변수안에 저장된 2개의 좌표 값으로 타원의 수평 반지름과 수직 반지름을 계산한다.
			GLfloat xradius = (firstTouch.x > lastTouch.x) ? (firstTouch.x - lastTouch.x)/2 : (lastTouch.x - firstTouch.x)/2;
			GLfloat yradius = (self.frame.size.height - firstTouch.y > self.frame.size.height - lastTouch.y) ?
			((self.frame.size.height - firstTouch.y) - (self.frame.size.height - lastTouch.y))/2 :
			((self.frame.size.height - lastTouch.y) - (self.frame.size.height - firstTouch.y))/2;
			
			// for문을 사용하여 원의 둘레를 구성하는 점들의 좌표 값을 계산한다.
			for(int i = 0; i < 720; i+= 2)
			{
				GLfloat xOffset = (firstTouch.x > lastTouch.x) ? lastTouch.x + xradius : firstTouch.x + xradius;
				GLfloat yOffset = (self.frame.size.height - firstTouch.y > self.frame.size.height - lastTouch.y) ?
				self.frame.size.height - lastTouch.y + yradius : self.frame.size.height - firstTouch.y + yradius;
				
				vertices[i] = (cos(degreesToRadian(i/2))*xradius) + xOffset;
				vertices[i+1] = (sin(degreesToRadian(i/2))*yradius) + yOffset;				
			}
			
			glVertexPointer(2, GL_FLOAT, 0, vertices);
			glDrawArrays(GL_TRIANGLE_FAN, 0, 360);
			break;
		}
		case kImageShape :
			glEnable(GL_TEXTURE_2D);
			[sprite drawAtPoint:CGPointMake(lastTouch.x, self.frame.size.height - lastTouch.y)];
			break;
		default :
			break;
	}
	
	// OpenGL ES를 사용하여 드로잉을 할 때는 언제나 버퍼의 데이터를 렌더링해야 한다.
	// 그리고 뷰의 컨텍스트에 새롭게 렌더링된 내용을 표시해야 한다.
	glBindRenderbufferOES(GL_RENDERBUFFER_OES, viewRenderbuffer);
	[context presentRenderbuffer:GL_RENDERBUFFER_OES];	
}

-(void)dealloc 
{
	[currentColor release];
	[sprite release];
	[super dealloc];
}

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
	if(useRandomColor)
		self.currentColor = [UIColor randomColor];
	
	UITouch *touch = [[event touchesForView:self] anyObject];
	firstTouch = [touch locationInView:self];
	lastTouch = [touch locationInView:self];
	[self draw];
}

-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
	UITouch *touch = [touches anyObject];
	lastTouch = [touch locationInView:self];
	
	[self draw];
}

-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
	UITouch *touch = [touches anyObject];
	lastTouch = [touch locationInView:self];
	
	[self draw];
}

@end

*  Nib를 설계하고 프레임워크을 추가한 뒤에 애플리케이션 실행하기
- QuartzFunViewController.xib업데이트 하기와 동일하게 진행하는데 QuartzFunView을 GLFunView클래스로 바꾸어 사용한다.
- 컴파일 하기 전에 프로젝트에 2개의 프레임워크를 링크해야만 한다. OpenGLES.framework와 QuartzCore.framework를 추가한다.
- Xcode에서 Frameworks에서 마우스 우클릭 후 Add > Existing Frameworks를 선택한다.
:

[iPhone] - 시작하세요! 아이폰 3 프로그래밍 - Part 11. 기본적인 데이터 저장 방법

iPhone/[위키북스]시작하세요! 아이폰3 프로그래밍 2010. 8. 12. 17:58

* 애플리케이션 샌드박스
- 아이폰에서는 4가지 데이터 자장 메커니즘(프로퍼티 리스트, 객체 아카이브, SQLite3, 코어 데이터)이 공유하는 중요한 구성 요소가 있다. 그것은 애플리케이션의 /Documents폴더이다. 모든 애플리케이션은 자신만의 /Documents폴더를 갖는다. 그리고 애플리케이션은 /Documents 디렉토리 안의 파일만 읽고 쓸 수 있는 제약이 있다.
- Finder 윈도우를 열고 홈 디렉토리 안의 Library/ApplicationSupport/iPhone Simulator/User/폴더로 이동한다.
- 내 개발환경은 iPhone SDK 4 이기 때문에 디렉토리 구성이 조금 다른다. Library/Application Support/iPhone Simulator/4.0/Applications 안에 있다.
- 아이폰은 애플리케이션을 Applications 폴더 안에 저장한다.
- 가끔은 애플리케이션의 여러 디렉토리들 사이에서 .sb 파일을 보게 될 것이다. .sb파일은 시뮬레이터가 파일 이름과 동일한 이름의 프로그램을 실행할 때 사용하는 설정값들을 보관하고 있다. 폴더 안에는 여러분이 만든 예제 애플리케이션의 하나와 애플리케이션이 사용하는 Documents, Library, tmp 폴더가 들어 있을 것이다. NSUserDefaults를 통해 저장한 환경설정 데이터를 제외하고, 애플리케이션의 모든 데이터는 Docuemtns폴더에 저장된다.
- NSUserDefaults로 저장한 환경설정 데이터는 Library/Preferences폴더에 저장된다. tmp 디렉토리는 애플리케이션이 임시로 사용하는 파일을 저장하는 곳이다.

* Documents 디렉토리 경로 가져 오기
- 어떻게 Docuemtns 디렉토리의 전체 경로를 알아내어 파일을 읽고 쓰는데 사용할 수 있을까? 사실 이것은 매우 간단하다. NSSearchPathForDirectoriesInDomain라는 C함수가 있고 홈 디렉토리의 경로를 제공한다.
- 하지만 NSSearchPathForDirectoriesInDomain 함수가 제공하는 많은 기능들은 Mac OS X를 위해 설계되었기 때문에, 아이폰에서 사용될 때는 반환 값을 가지지 않는다. 왜냐하면 NSSearchPathForDirectoriesInDomain가 반활할 수 있는 디렉토리들은 아이폰상에서는 실제로 존재하지 않거나, 샌드박스 메커니즘 때문에 접근할 수 없기 때문이다.
NSString *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];

- NSDocumentDirectory는 Documents 디렉토리의 경로를 가져올 때 인사로 사용하는 상수이다. 다음으로 NSUserDomainMask는 디렉토리를 찾을 때 검색 범위를 애플리케이션의 샌드 박스 안으로 제한한다. Mac OS X에서 NSUserDomainMask 상수를 사용하는 것은 디렉토리의 검색 범위를 사용자 홈 디렉토리 안으로 제한한다는 것을 나타낸다.
- NSSearchPathForDirectoriesInDomains 함수가 검색한 경로를 배열 값으로 반환해도 우리는 배열의 0번째 요소가 Documents 디렉토리 경로라는 것을 알 수 있다. 왜 그럴까? 각각의 애플리케이션은 오직 하나의 Documents 디렉토리를 가지고 있기 때문에 반환받은 배열에는 오직 하나의 경로만이 들어 있기 때문이다.
- NSString을 반환하는 stringByAppendingPathComponent: 메서드를 사용하면 파일 이름을 쉽게 생성할 수 있다.
NSString *filename = [documentsDirectory stringByAppendingPathComponent:@"theFile.txt"];

* tmp 디렉토리 경로 가져오기
- Foundation 함수인 NSTemporaryDirectory()는 애플리케이션 tmp 디렉토리의 전체 경로를 담고 있는 문자열 포인터를 반환한다.
NSString *tempPath = NSTemporaryDirectory();

* 파일 저장 기법
- SQLite3를 사용할 경우 데이터베이스 파일을 하나 생성하면 SQLite3가 데이터를 저장하고 불러오는 일을 처리한다.
- 파일 저장을 단순하게 처리하고 싶다면 코어 데이터를 이용하여 파일 시스템을 관리할 수 있다.
- 프로퍼티 리스트와 아카이브를 사용할 때는 하나의 파일에 데이터를 저장할 것인지 여러 파일에 데이터를 저장할 것인지를 고려해야 한다.

* 하나의 파일에 데이터 저장하기
- 가장 간단한 방법은 하나의 파일에 데이터를 저장하는 것이며 대부분의 애플리케이션에서 사용되고 있다. 데이터를 저장하려면 먼저 루트 객체를 생성해야 하지만, 보통은 아카이빙을 사용할 때 NSArray나 NSDictionary와 같이 NSObject를 상속하는 클래스들을 사용한다. 그리고 나서 루트 객체에 애플리케이션의 데이터를 저장한다. 애플리케이션은 데이터를 저장할 때마다 루트 객체 안의 모든 데이터를 파일에 저장할 것이다. 애플리케이션이 실행되면 파일의 모든 내용을 읽어서 메모리에 적재하고, 종료될 때는 모든 내용을 파일에 저장한다.
- 하나의 파일을 사용할 때의 단점은 파일 안의 모든 데이터를 메모리에 적재해야 한다는 것이다. 심지어 매우 작은 데이터만 수정해도 데이터 전체를 파일에 출력해야 한다.

* 여러 파일에 데이터 저장하기
- 데이터를 여러 파일에 나눠서 저장하면 사용자가 데이터를 요청했을 때 애플리케이션은 요청한 데이터를 가지고 있는 파일 만을 로딩하며, 사용자가 데이터를 수정하면 변경된 내용을 보관할 파일 만을 저장한다. 또한 여러 파일을 사용하면 사용자가 현재 참조하지 않는 메모리상의 데이터를 파일에 출력하고 필요한 시기에 다시 불러올 수 있기 때문에, 메모리가 부족하다는 메시지를 받았을 때 메모리를 확보할 수 있게 된다. 여러 파일에 데이터를 나눠서 저장하는 방법의 단점은 애플리케이션의 내부 구조가 다소 복잡해진다는 것이다.

* 애플리케이션 데이터 저장하기
* 프로퍼티 리스트 직렬화
- 프로퍼티 리스트에 저장하려면 인스턴스는 직렬화 객체만을 가지고 있어야 한다. 직렬화 객체는 바이트 스트림(byte stream)으로 변환되기 때문에 파일에 저장하거나 네트워크를 통해서 전송될 수 있다. 물론 모든 직력화 객체가 콜렉션 클래스(collection class)에 포함될 수 있는 것은 아닌다. NSDictionary나 NSArray와 같은 특정 객체들만이 콜렉션 클래스에 포함될 수 있다.
  • NSArray
  • NSMutableArray
  • NSDictionary
  • NSMutableDictionary
  • NSData
  • NSMutableData
  • NSString
  • NSMutableString
  • NSNumber
  • NSData

- 만일 위의 클래스들을 사용하여 데이터 모델을 만드면, 프로퍼티 리스트에 데이터를 저장하고 불러올수 있다.
- 프로퍼티 리스트를 사용할 때 NSArray나 NSDictionary를 사용하여 데이터를 보관할 것이다. NSArray나 NSDictionary에 보관하는 객체가 직렬화 객체라면 writeToFile:atomically:메서드를 호출하여 다음과 같이 프로퍼티 리스트를 만들수 있다.

[myArray writeToFile:@"/some/file/location/output.list" atomically:YES];

- atomically 파라미터를 사용하여 writeToFile:메서드를 호출하면 인자에 지정된 파일에 데이터를 출력 하기 전에 임시 파일을 생성한다. wirteToFile:메서드는 임시 파일을 만든 뒤에 인자에 지정된 위치로 임시 파일을 복사한다. 이렇게 하면 안전하게 파일을 저장할 수 있다. 왜냐하면 애플리케이션이 파일을 저장하는 도중에 갑자기 종료되면, 임시 파일에만 영향이 있을 뿐 이전에 저장된 파일은 안전하기 때문이다.
- 프로포티 리스트를 사용할 때의 단점은 사용자 객체는 직렬화하여 프로퍼티 리스트에 저장할 수 없다는 것이다. 또한 코코아 터치의 객체들을 상속받았다고 해도, 앞에서 언급한 목록에 포함된 객체만이 프로퍼티 리스트를 사용할 수 있다. 이것은 NSURL, UIImage, UIColor와 같은 클래스에 저장된 데이터들은 프로퍼티 리스트에 직접 저장할 수 없음을 의미 한다.
- 이러한 객체들을 직렬화할 수 없다는 것은 다른 객체로부터 상속받은 객체나 객체 간의 계산 결과를 가지고 있는 객체는 프로퍼티로 사용할 수 없으며, 모델 클래스에 구현된 일부 코드를 컨트롤러 클래스로 옮겨야 함을 의미한다.
- 애플리케이션에 데이터를 포함하는 멋진 방법이 하나 있다. 프로퍼티 리스트 파일을 하나 만들고 프로젝트의 Resources폴더에 프로퍼티 리스트 파일을 저장하는 것이다. 그러면 애플리케이션이 컴파일될 때 프로퍼티 리스트 파일은 애플리케이션에 포함되어 컴파일될 것이다.

* 퍼시스턴스 애플리케이션
- 여기서는 4개의 텍스트 필드에 데이터를 입력할 수 있는 애플리케이션을 하나 만들것이다.
- 애플리케이션이 종료될 때 필드 안의 데이터들은 프로퍼티 리스트 파일로 저장되고, 애플리케이션이 실행되면 프로퍼티 리스트로부터 데이터를 읽어와 다시 텍스트 필드로 로딩할 것이다.

* 퍼시스턴스 프로젝트 생성하기
- Xcode에서 뷰 기반의 애플리케이션 템플릿을 사용하여 새 프로젝트를 하나 생성하고, 프로젝트 이름에 Persistence라고 입력한다.

#### PersistenceViewController.h ####
#import 

// 파일 이름을 위한 상수
#define kFilename @"data.plist"

@interface PersistenceViewController : UIViewController {
	UITextField *field1;
	UITextField *field2;
	UITextField *field3;
	UITextField *field4;	
}

@property (nonatomic, retain) IBOutlet UITextField *field1;
@property (nonatomic, retain) IBOutlet UITextField *field2;
@property (nonatomic, retain) IBOutlet UITextField *field3;
@property (nonatomic, retain) IBOutlet UITextField *field4;

-(NSString *)dataFilePath;
// applicationWillTerminate: 메서드는 애플리케이션이 종료될 때 호출될 것이고, 프로퍼티 파일로 데이터를 저장할 것이다.
-(void)applicationWillTerminate:(NSNotification *)notification;

@end

* 퍼시스턴스 애플리케이션 뷰 설계하기
- Resources 폴더를 펼치고 PresistenceViewController.xib를 더블 클릭한다.
- 라이브러리 창으로 부터 Text Field 하나를 드래그 하여 오른쪽과 위쪽의 파란 안내선에 맞춰서 배치한다. 그다음 Text Field를 왼쪽을 잡고 윈도우 2/3 정도 되는 위치까지 늘린다. Func+1 눌러서 속성 인스펙터 창이 나타나게 한다. Clear When Editing Begins라는 항목의 체크를 해지한다. 총 4개의 Text Field를 배치한다.
- 이제 4개의 레이블을 드래그하여 뷰에 붙힌다.
- 4개의 텍스트 필드와 레이블들을 배치한 뒤, File's Owner에서 각각의 텍스트 필드로 컨트롤 드래그하여 연결한다.

* 퍼시스턴스 클래스 편집한기.
#### PersistenceViewController.m ####
#import "PersistenceViewController.h"

@implementation PersistenceViewController
@synthesize field1;
@synthesize field2;
@synthesize field3;
@synthesize field4;

// 데이터 파일의 전체 경로를 반환한다. 
-(NSString *)dataFilePath
{
	// Documents 디렉토리의 경로를 paths에 저장한 다음 kFilename을 paths에 붙여서 전체 경로를 나타내는 문자열을 만든다.
	NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
	NSString *documentsDirectory = [paths objectAtIndex:0];
	return [documentsDirectory stringByAppendingPathComponent:kFilename];
}

// 메서드가 NSNotification의 포인터를 인자로 사용한다는 것을 기억하자.
// applicationWillTerminate: 메서드는 노티피케이션 notification 메서드이고 
// 모든 노티피케이션들은 하나의 NSNotification 인스턴스를 인자로 사용한다.
// 노티피케이션은 객체 간에 서로 커뮤니케이션을 할 수 있도록 만들어진 간단한 메커니즘이다.
// 모든 객체는 1개 이상의 노티피케이션을 정의할 수 있으며 그것은 애플리케이션의 노티피케이션 센터(notification center)에서 발급된다. 
// 노티피케이션 센터란 객체들 사이에 노티피케이션을 넘겨주기 위하여 존재하는 싱글턴 객체이다. 
// 일반적으로 노티피케이션은 이벤트와 노티피케이션을 발급한 객체가 전달되었다는 것을 알려주는 표시이다.
// 애플리케이션은 종료 전에 데이터를 저장해야 하므로 UIApplicationWillTerminateNotification라는 노티피케이션을 사용해야 한다.
-(void)applicationWillTerminate:(NSNotification *)notification
{
	NSMutableArray *array = [[NSMutableArray alloc] init];
	[array addObject:field1.text];
	[array addObject:field2.text];
	[array addObject:field3.text];
	[array addObject:field4.text];
	[array writeToFile:[self dataFilePath] atomically:YES];
	[array release];
}

#pragma mark -
-(void)viewDidLoad
{
	// 같은 이름의 데이터 파일이 존재하는지 확인하고, 존재 하지 않는다면 데이터 파일을 로딩하지 않는다.
	// 데이터 파일이 존재하면 파일의 내용을 배열에 복사하고, 배열을 사용해서 4개의 텍스트 필드로 문자열을 복사한다.
	// 배열의 데이터들은 정렬되어 있으므로 데이터를 저장했던 순서대로 텍스트 필드에 할당하면, 
	// 데이터는 원래 입력되었던 텍스트 필드에 정확히 로딩될것이다.
	NSString *filePath = [self dataFilePath];
	if([[NSFileManager defaultManager] fileExistsAtPath:filePath])
	{
		NSArray *array = [[NSArray alloc] initWithContentsOfFile:filePath];
		field1.text = [array objectAtIndex:0];
		field2.text = [array objectAtIndex:1];
		field3.text = [array objectAtIndex:2];
		field4.text = [array objectAtIndex:3];
		[array release];
	}
	
	// 애플리케이션 인스턴스 포인터를 만들었다.
	// NSNotificationCenter의 기본 인스턴스 객체의 addObserver:selector:name:object:메서드를 호출하여
	// UIApplicationWillTerminateNotification 노티피케이션 구독하였다.
	UIApplication *app = [UIApplication sharedApplication];
	
	// 우리는 PersistenceViewController를 옵저버(Observer)객체로 지정하였다. 
	// 이것은 PresistenceViewController 클래스가 노티피케이션을 받겠다는 것을 의미한다.
	// applicationWillTerminate:를 셀렉터로 지정하여 노티피케이션이 발급되면 노티피케이션 센터가 
	// applicationWillTerminate:를 호출할 수 있게 하였다.
	// 세번째 파라미터인 name:은 구독하려는 노티피케이션의 이름이며 마지막 인자인 object:는 노티피케이션을 발급하는 객체이다.
	// 만일 마지막 인자를 nil로 지정하면 UIApplicationWillTerminateNotification을 발급하는 모든 노티피케이션을 구독한다.
	[[NSNotificationCenter defaultCenter] addObserver:self 
											 selector:@selector(applicationWillTerminate:)
												 name:UIApplicationWillTerminateNotification
											   object:app];
	
	[super viewDidLoad];
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
}

- (void)viewDidUnload {
	self.field1 = nil;
	self.field2 = nil;
	self.field3 = nil;
	self.field4 = nil;
	[super viewDidUnload];
}

- (void)dealloc {
	[field1 release];
	[field2 release];
	[field3 release];
	[field4 release];
    [super dealloc];
}

@end


- 총 정리 : 메인 뷰는 로딩을 마치면 프로퍼티 리스트파일을 찾는다. 파일이 존재하면 파일로부터 데이터를 복사해서 텍스트 필드에 넣는다. 다음으로 애플리케이션이 종료할 때 통보받기 위해 노티피케이션을 구독하였다. 애플리케이션은 종료할 때 4개의 텍스트 필드의 값을 가변 배열에 추가하고, 배열을 프로퍼티 리스트 파일에 저장한다.

* 아카이빙 모델 객체
- 코코아 프로그래밍에서 아카이빙(archiving)이라는 개념은 일종의 직렬화를 의미하지만 프로퍼티 리스트와는 달리 모든 객체에 적용할 수 있다. 모델 객체를 아카이브로 만들면 복잡한 객체를 파일로 저장하고 로딩할 수 있다. 어떤 클래스의 프로퍼티가 int, float 혹은 NSCoding 프로토콜을 따르는 클래스의 인스턴스와 같은 양을 나타내는 자료형으로만 구성되었다면 그 클래스는 완벽하게 아카이브로 만들 수 있다. 데이터를 저장하는 Foundation 클래스와 코코아 터치 클래스들은 NSCoding을 따르기 때문에(물론 UIImage처럼 예외에 해당하는 클래스도 있다.) 대부분의 클래스를 아카이브로 만드는 것은 비교적 쉬운 일이다.
- 데이터를 아카이브로 만들 때 NSCoding프로토콜과 함께 구현해야 하는 또 다른 크로토콜이 있다. 그것은 바로 NSCopying 프로토콜이며, 이 프로토콜을 따르는 객체는 복사 될수 있다. 객체가 복사될 수 있으면 데이터 모델 객체를 사용할 때 더 많은 유연성을 가진다.

* NSCoding 프로토콜 구현하기
- NSCoding 프로토콜은 2개의 메서드를 선언하고 있다.
- 하나는 객체를 인코딩하여 아카이브에 저장하는 메서드 이고, 다른 하나는 아카이브를 디코딩하여 객체를 만드는 메서드이다. 두 메서드 모두 NSCoder인스턴스를 인자로 사용한다.
- 객체를 인코딩하는 메서드는 아래와 같다. 객체를 아카이브로 만들려면 각각의 인스턴스 변수들을 적절한 메서드로 인코딩해서 encoder객체에 저장해야 한다. 따라서 NSCoder의 객체를 초기화하는 메서드를 구현할 것이다.
-(void)encodeWithCoder:(NSCoder *)encoder
{
	[encoder encodeObject:foo forKey:kFooKey];
	[encoder encodeObject:bar forKey:kBarKey];
	[encoder encodeInt:someInt forKey:kSomeIntKey];
	[encoder encodeFloat:someFloat forKey:kSomeFloatKey];
}

- 만일 NSCoding를 따르는 하위클래스를 만든다면 상위 클래스의 encodeWithCoder:를 호출하는지 확인해야 한다.
-(void)encodeWithCoder:(NSCoder *)encoder
{
	[encoder encodeObject:for forKey:kFooKey];
	[encoder encodeObject:bar forKey:kBarKey];
	[encoder encodeInt:someInt forKey:kSomeIntKey];
	[encoder encodeFloat:someFloat forKey:kSomeFloatKey];
	[super encodeWithCoder:encoder];
}

- initWithCoder:메서드를 구현하는 것은 encodeWithEncoder:를 구현하는 것보다 조금 복잡하다. 만일 클래스가 NSObject의 하위클래스이거나 NSCoding 프로토콜을 따르지 않는면, 디코더를 아래와 같이 구현할 수 있다. 메서드는 [super init]를 사용하여 객체 인스턴스를 초기화하고, 초기화에 성공하면 NSCoder인스턴스로 넘겨받은 인자를 디코딩하여 프로퍼티에 복사한다.
-(id)initWithCoder:(NSCoder *)decoder
{
	if(self = [super init])
	{
		self.foo = [decoder decodeObjectForKey:kFooKey];
		self.bar = [decoder decodeObjectForKey:kBarKey];
		self.someInt = [decoder decodeIntForKey:kSomeIntKey];
		self.someFloat = [decoder decodeFloatForKey:kAgeKey];
	}

	return self;
}

- 클래스의 상위 클래스가 NSCoding 프로토콜을 따르고, 자신도 NSCoding 프로토콜을 따르면 initWithCoder:메서드는 아래와 같은 다른 방법으로 구현해야 한다. 상위 클래스의 init 메서드를 호출하는 대신에 아래와 같은 initWithCoder:메서드를 호출해야 한다.
-(id)initWithCoder:(NSCoder *)decoder
{
	if(self = [super initWithCoder:decoder])
	{
		self.foo = [decoder decodeObjectForKey:kFooKey];
		self.bar = [decoder decodeObjectForKey:kBarKey];
		self.someInt = [decoder decodeIntForKey:kSomeIntKey];
		self.someFloat = [decoder decodeFloatForKey:kAgeKey];
	}

	return self;
}

- 이와 같이 객체의 프로퍼티를 인코딩하고 디코딩할수 있는 2개의 메서드가 구현되어 있다면 객체를 아카이브로 만들 수 있고 아카이브로부터 객체를 읽고 쓰는 것이 가능하다.

* NSCopying 프로토콜 구현하기
- 데이터 모델 클래스가 NSCopying프로토콜를 따르는 것은 매우 좋은 방법이다. NSCopying는 copyWithZone:라는 메서드를 가지고 있다. 그리고 이 메서드는 객체를 복사하는 기능을 가지고 있다. NSCopying 프로토콜의 copyWithZone:메서드를 구현한 코드는 initWithCoder:메서드를 구현한 코드와 유사하다. 원본 클래스의 인스턴스 변수를 하나 만들고 인스턴스 변수의 프로퍼티에 원본 클래스의 프로퍼티를 복사하면된다.
-(id)copyWithZone:(NSZone *)zone
{
	MyClass *copy = [[[self class] allocWithZone:zone] init];
	copy.foo = [self.foo copyWithZone:zone];
	copy.bar = [self.bar copyWithZone:zone];
	copy.someInt = self.someInt;
	copy.someFloat = self.someFloat;
	return copy;
}

- 새로 생성한 객체를 릴리즈하거나 오토릴리즈 하지 않은 점에 주목하자. 복사된 객체는 리테인 속성이 내재되어 있으며, copy나 copyWithZone:을 호출할 때 복사되는 객체는 릴리즈나 오토릴리즈 속성을 호팜함하고 있다.
- NSZone 인자에 대해 신경 쓸 필요는 없다. 이 인자는 메모리를 관리하기 위해 시스템에 의해 사용되는 구조체를 가리키는 포인터이다.

* 데이터 객체를 아카이브로 만들기
- NSCoding 프로토콜을 따르는 객체를 아카이브로 만드는 것은 어렵지 않다.
// 먼저 인코딩된 데이터를 저장하기 위해 NSMutableData의 인스턴스 변수 하나를 생성한다.
// 그리고 나서 NSMutableData인스턴스 변수에 아카이브 객체를 저장하기 위해 NSKeyArchiver 인스턴스를 생성한다.
NSMutableData *data = [[NSMutableData alloc] init;
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];

// 키-밸류 코딩을 사용하여 아래와 같이 아카이브로 만들려는 객체를 인코딩할 것이다.
[archiver encodeObject:myObject forKey:@"keyValueString"];

// archiver인스턴스의 finishEncoding을 호출한다.
// 그리고 NSMutableData인스턴스를 파일로 저장하고 객체를 메모리에서 제거한다.
// 파일을 쓰는 동안에 문제가 발생하면, success 값은 NO일것이다. 
// success 값이 YES이면 데이터는 성공적으로 지정된 파일에 쓰여진 것이다.
[archiver finishEncoding];
BOOL success = [data writeToFile:@"/path/to/archiver" atomically:YES];
[archiver release];
[data release];

* 아카이브로부터 데이터 객체 가져오기
- 아카이브로부터 객체를 만들려면 저장할 때와 유사한 방법을 사용해야 한다.
// 아카이브 파일을 읽어서 NSData 인스턴스를 생성하고, 
// 데이터를 디코딩하기 위해 NSKeyedUnarchiver 인스턴스를 생성한다.
NSData *data = [[NSData alloc] initWithContentsOfFile:path];
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];

// 객체를 아카이브로 만들 때 사용했던 키(key)를 사용하여 unarchiver로 부터 객체를 가져온다.
self.object = [unarchiver decodeObjectForKey:@"keyValueString"];

[unarchiver finishDecoding];
[unarchiver release];
[data release];

* 아카이빙 애플리케이션
* FourLines 클래스 구현하기
- 퍼시스턴스 프로젝트를 그대로 사용하겠당. Classes Folder에 새로운 파일을 추가 하자 Cocoa Touch Class를 선택하고 파일명은 FourLines.m로 설정한다. 헤더 파일도 같이 생성한다.
- 이 파일은 데이터 모델 클래스로 사용할 것이고, 프로퍼티 리스트 애플리케이션의 딕셔너리에 보관할 데이터를 저장할 것이다.

#### FourLines.h ####
#import 
#define kField1Key @"Field1"
#define kField2Key @"Field2"
#define kField3Key @"Field3"
#define kField4Key @"Field4"

@interface FourLines : NSObject 
{
	NSString *field1;
	NSString *field2;
	NSString *field3;
	NSString *field4;
}

@property (nonatomic, retain) NSString *field1;
@property (nonatomic, retain) NSString *field2;
@property (nonatomic, retain) NSString *field3;
@property (nonatomic, retain) NSString *field4;

@end

#### FourLines.m ####
#import "FourLines.h"

@implementation FourLines
@synthesize field1;
@synthesize field2;
@synthesize field3;
@synthesize field4;

#pragma mark NSCoding
// encodeWithCoder: 메서드는 4개의 프로퍼티를 각각 인코딩한다.
-(void)encodeWithCoder:(NSCoder *)encoder
{
	[encoder encodeObject:field1 forKey:kField1Key];
	[encoder encodeObject:field2 forKey:kField2Key];
	[encoder encodeObject:field3 forKey:kField3Key];
	[encoder encodeObject:field4 forKey:kField4Key];	
}

// initWithCoder: 메서드는 인코딩할 때 사용했던 키를 사용하여 각각의 데이터를 디코딩하였다.
-(id)initWithCoder:(NSCoder *)decoder
{
	if(self = [super init])
	{
		self.field1 = [decoder decodeObjectForKey:kField1Key];
		self.field2 = [decoder decodeObjectForKey:kField2Key];
		self.field3 = [decoder decodeObjectForKey:kField3Key];
		self.field4 = [decoder decodeObjectForKey:kField4Key];
	}	
	return self;
}

#pragma mark -
#pragma mark NSCopying
// copyWithZone: 메서드는 FourLines 객체의 인스턴스를 생성하고 4개의 문자열을 FourLines객체의 프로퍼트로 복사 하였다.
-(id)copyWithZone:(NSZone *)zone
{	
	FourLines *copy = [[[self class] allocWithZone:zone] init];
	copy.field1 = [[self.field1 copyWithZone:zone] autorelease];
	copy.field2 = [[self.field2 copyWithZone:zone] autorelease];
	copy.field3 = [[self.field3 copyWithZone:zone] autorelease];
	copy.field4 = [[self.field4 copyWithZone:zone] autorelease];
	return copy;
}


@end

* PersistenceViewController 클래스 구현하기
- 기존 프로젝트 파일에서 파일 몇개 변경한다.

#### PersistenceViewController.h ####
#import 

#define kFilename @"archive"
#define kDataKey @"Data"

@interface PersistenceViewController : UIViewController {
	UITextField *field1;
	UITextField *field2;
	UITextField *field3;
	UITextField *field4;	
}

@property (nonatomic, retain) IBOutlet UITextField *field1;
@property (nonatomic, retain) IBOutlet UITextField *field2;
@property (nonatomic, retain) IBOutlet UITextField *field3;
@property (nonatomic, retain) IBOutlet UITextField *field4;

-(NSString *)dataFilePath;
// applicationWillTerminate: 메서드는 애플리케이션이 종료될 때 호출될 것이고, 프로퍼티 파일로 데이터를 저장할 것이다.
-(void)applicationWillTerminate:(NSNotification *)notification;

@end

#### PersistenceViewController.m ####
#import "PersistenceViewController.h"
#import "FourLines.h"

@implementation PersistenceViewController
@synthesize field1;
@synthesize field2;
@synthesize field3;
@synthesize field4;

// 데이터 파일의 전체 경로를 반환한다. 
-(NSString *)dataFilePath
{
	// Documents 디렉토리의 경로를 paths에 저장한 다음 kFilename을 paths에 붙여서 전체 경로를 나타내는 문자열을 만든다.
	NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
	NSString *documentsDirectory = [paths objectAtIndex:0];
	return [documentsDirectory stringByAppendingPathComponent:kFilename];
}

// 메서드가 NSNotification의 포인터를 인자로 사용한다는 것을 기억하자.
// applicationWillTerminate: 메서드는 노티피케이션 notification 메서드이고 
// 모든 노티피케이션들은 하나의 NSNotification 인스턴스를 인자로 사용한다.
// 노티피케이션은 객체 간에 서로 커뮤니케이션을 할 수 있도록 만들어진 간단한 메커니즘이다.
// 모든 객체는 1개 이상의 노티피케이션을 정의할 수 있으며 그것은 애플리케이션의 노티피케이션 센터(notification center)에서 발급된다. 
// 노티피케이션 센터란 객체들 사이에 노티피케이션을 넘겨주기 위하여 존재하는 싱글턴 객체이다. 
// 일반적으로 노티피케이션은 이벤트와 노티피케이션을 발급한 객체가 전달되었다는 것을 알려주는 표시이다.
// 애플리케이션은 종료 전에 데이터를 저장해야 하므로 UIApplicationWillTerminateNotification라는 노티피케이션을 사용해야 한다.
-(void)applicationWillTerminate:(NSNotification *)notification
{	
	FourLines *fourLines = [[FourLines alloc] init];
	fourLines.field1 = field1.text;
	fourLines.field2 = field2.text;
	fourLines.field3 = field3.text;
	fourLines.field4 = field4.text;
	
	NSMutableData *data = [[NSMutableData alloc] init];
	NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
	[archiver encodeObject:fourLines forKey:kDataKey];
	[archiver finishEncoding];
	[data writeToFile:[self dataFilePath] atomically:YES];
	[fourLines release];
	[archiver release];
	[data release];	
}

#pragma mark -
-(void)viewDidLoad
{
	// 같은 이름의 데이터 파일이 존재하는지 확인하고, 존재 하지 않는다면 데이터 파일을 로딩하지 않는다.
	// 데이터 파일이 존재하면 파일의 내용을 배열에 복사하고, 배열을 사용해서 4개의 텍스트 필드로 문자열을 복사한다.
	// 배열의 데이터들은 정렬되어 있으므로 데이터를 저장했던 순서대로 텍스트 필드에 할당하면, 
	// 데이터는 원래 입력되었던 텍스트 필드에 정확히 로딩될것이다.
	NSString *filePath = [self dataFilePath];
	if([[NSFileManager defaultManager] fileExistsAtPath:filePath])
	{
		
		NSData *data = [[NSMutableData alloc] initWithContentsOfFile:filePath];
		NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
		
		FourLines *fourLines = [unarchiver decodeObjectForKey:kDataKey];
		field1.text = fourLines.field1;
		field2.text = fourLines.field2;
		field3.text = fourLines.field3;
		field4.text = fourLines.field4;
		
		[unarchiver release];
		[data release];
		
	}
	
	// 애플리케이션 인스턴스 포인터를 만들었다.
	// NSNotificationCenter의 기본 인스턴스 객체의 addObserver:selector:name:object:메서드를 호출하여
	// UIApplicationWillTerminateNotification 노티피케이션 구독하였다.
	UIApplication *app = [UIApplication sharedApplication];
	
	// 우리는 PersistenceViewController를 옵저버(Observer)객체로 지정하였다. 
	// 이것은 PresistenceViewController 클래스가 노티피케이션을 받겠다는 것을 의미한다.
	// applicationWillTerminate:를 셀렉터로 지정하여 노티피케이션이 발급되면 노티피케이션 센터가 
	// applicationWillTerminate:를 호출할 수 있게 하였다.
	// 세번째 파라미터인 name:은 구독하려는 노티피케이션의 이름이며 마지막 인자인 object:는 노티피케이션을 발급하는 객체이다.
	// 만일 마지막 인자를 nil로 지정하면 UIApplicationWillTerminateNotification을 발급하는 모든 노티피케이션을 구독한다.
	[[NSNotificationCenter defaultCenter] addObserver:self 
											 selector:@selector(applicationWillTerminate:)
												 name:UIApplicationWillTerminateNotification
											   object:app];
	
	[super viewDidLoad];
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
}

- (void)viewDidUnload {
	self.field1 = nil;
	self.field2 = nil;
	self.field3 = nil;
	self.field4 = nil;
	[super viewDidUnload];
}

- (void)dealloc {
	[field1 release];
	[field2 release];
	[field3 release];
	[field4 release];
    [super dealloc];
}

@end

* 아이폰에 내장된 SQLite3 사용하기
- SQLite3라는 아이폰에 내장된 SQL 데이터베이스를 사용하는 것이다. SQLite3는 목잡한 데이터를 처리할 수 있는 능력이 있으며, 객체를 사용해 처리하는 것보다 훨씬 빠르다. SQLite3를 사용하면 모든 객체를 메모리에 로딩하지 않고, 처리할 수 있다. SQLite3로부터 계산된 결과를 가져오는 것은 모든 객체를 메모리에 로딩하고 객체의 합을 계산하는 것보다 훨씬 빠르다.
- SQLite3는 구조적 질의언어(SQL)를 사용한다.
- 이절에서는 SQLite3 데이터베이스를 초기화하는 방법과 애플리케이션과 상호작용하는 방법에 대해서 설명할 것이다.

* 데이터베이스 생성하고 열기
- SQLite3를 사용하면 먼저 데이터베이스를 열어야(OPEN)한다. 데이터베이스를 열기 위한 함수인 sqlite3_open()는 기존에 만들어진 데이터베이스 하나를 열거나 명시된 데이터베이스가 존재하지 않는다면 새로운 데이터베이스를 만드는 기능을 제공한다.
- result값이 SQLITE_OK라면 데이터베이스가 성공적으로 열린 것이다. 여기서 주의할 점은 데이터베이스 파일의 경로를 인자로 넘길 때는 표준 C문자열을 사용해야 한다는 것이다. SQLite3는 오브젝티브C가 아닌 일발적인 C로 작성되었고 NSString를 사용할수 없다.
sqlite3 *database;
int result = sqlite3_open("/path/to/database/file", &database);

- 다행히 NSString에는 인스턴스를 C문자열을 변환해주는 멤버 메서드가 있다.
char *stringPath = [pathString UTF8String];

- SQLite3 데이터 베이스와 관련된 작업을 모두 마쳤다면 아래아 같이 호출하여 데이터베이스를 닫는다.
sqlite3_close(database);

- 데이터베이스는 모든 데이터를 테이블에 저장한다. SQL의 CREATE문을 실행하는 것으로 새로운 데이블을 만들 수 있고, sqlite3_exec함수를 사용하여 아래와 같이 열려있는 데이터베이스에서 CREATE문을 실행할 수 있다.
- 명령이 성공적으로 실행됐는지 확인하기 위해 결과 값이 SQLITE_OK인지 확인할 필요가 있다. 만일 아니라면 errorMsg에 발생한 문제에 대한 설명이 포함되어 있을 것이다.
char *errorMsg;
const char *createSQL = "CREATE TABLE IF NOT EXITS PEOPLE (ID INTEGER PRIMARY KEY AUTOINCREMENT, FILED_DATA TEXT");
int result = sqlite3_exec(database, createSQL, NULL, NULL, &errorMsg);
- sqlite3_exec함수는 SQLite3명령어를 실행하는 데 사용되며 반환 값을 가지지 않는다. 이 함수는 update, insert, delete문에 사용된다.
NSString *query = @"SELECT ID, FIELD_DATA FROM FIELDS ORDER BY ROW";
sqlite3_stmt *statement;
int result = sqlite3_prepare_v2(database, [query UTF8String], -1, &statement, nil);

- result 값이 SQLITE_OK이면 SQL문이 성공적으로 준비된(prepared)것이고, 결과셋(result set)을 가지고 계속 진행할 수 있다. 아래는 데이터베이스로부터 결과셋을 만들고 int값과 NSString을 가져오는 예제이다.
while(sqlite3_step(statement) == SQLITE_ROW)
{
	int rowNum = sqlite3_column_int(statement, 0);
	char *rowData = (char *)sqlite3_column_text(statement, 1);
	NSString *fieldValue = [[NSString alloc] initWithUTF8String:rowData];
	// DO Someting with the data here
	[fieldValue release];
}

sqlite3_finalize(statement);

* 바인딩 변수들
- 데이터베이스에 값을 입력하기 위해 SQL문장에 자체에 값을 포함시켜 사용하는 것이 가능하지만, 데이터 베이스에 입력할 때는 바인딩 변수라는 것을 사용하는 연습이 필요하다. SQL문장에 부적절한 문자나 따옴표가 포함되지 않도록 확인하는 일은 상당히 귀찮은 일이다. 바인딩 변수를 사용하면 이러한 일들을 쉽게 처리할수 있다.
- 바인딩 변수를 사용하여 테이블에 레코드를 삽입하는 것은 일반적으로 SQL문장을 만드는것과 동일하지만 물음표를 포함시켜야만 한다. 각각의 물음표는 문장이 실행되기 전에 실제 값으로 치환될 하나의 변수를 나타낸다. 그리고 나서 SQL문에 전처리 과정을 거치면 바인딩 변수들은 각각의 값들로 치환될 것이다.
char *sql = "insert into foo values (?,?);";
sqlite3_stmt *stmt;
if(sqlite3_prepare_v2(database, sql, -1, &stmt, nil) == SQLITE_OK)
{
	sqlite3_bind_int(stmt, 1, 235);
	sqlite3_bind_text(stmt, 2, "Bar", -1, NULL);
}
if(sqlite3_step(stmt) != SQLITE_DONE)
	NSLog(@"This should bt real error checking!");
sqlite3_finalize(stmt);

- 사용하기 원하는 데이터 타입에 의존하여 다중 바인딩 문을 사용하는것도 가능하다.
- 첫번째 인자는 바인딩될 데이터 타입과는 무관하며 sqlite3_prepare_v2()를 호출하기 전에 선언된 sqlite3_stmt의 포인터이다.
- 두번째 인자는 바인딩하려는 변수의 인덱스 번호이다. SQL문의 첫 번째 물음표는 인덱스 값으로 1을 사용할 것이고 SQL문 안에서 각각의 물음표는 왼쪽부터 순서대로 1씩 증가된 인덱스 값을 가진다.
- 세번째 인자는 물음표에 치환된 실제 값이다.
- 대부분의 바인딩 함수들은 3개의 인자만을 사용하지만 텍스트나 바이너리 데이터를 인자로 바인딩할 때는 2개의 인자를 추가로 사용한다.
- 네번째 인자는 세 번째 인자의 데이터 길이를 나타내는 인자이다. C문자열을 인자로 넘겨줄 때는 문자열의 길이 대신 -1을 사용하는 것이 가능하며 -1을 사용하면 함수는 문자열 전체를 사용하게 될 것이다. 문자열이 아닌 경우에는 정확한 데이터의 길이를 int값으로 넘겨줘야만한다.
- 다섯번째 인자는 콜백 함수의 포인터로써 선택적으로 사용할 수 있으며 SQL문이 호출된 후에 메모리 해제와 같은 특별한 일을 수행하고 싶다면 해당 기능을 가진 콜백 함수를 등록할 수 있다. 전통적으로 다섯 번째 인자로 등록되는 콜백 함수는 malloc()으로 할당된 메모리를 해제하는 함수를 등록하는 데 사용되었다.
- 만일 SQL문자열이 update문보다는 SQL쿼리문을 가지고 있다면 SQL_DONE을 반활 할때 까지 sqllist3_step()을 호출해야만 한다. 만일 update문이라면 sqlite3_step()는 한번만 호출하면 된다.

* SQLite3 사용을 위한 프로젝트 설정하기
- C함수의 형태로 제공되는 수많은 절차적 API들을 사용해서 SQLite3에 접근할 수 있다. 이 API를 사용하기 위해서는 Mac OS X와 아이폰의 /usr/lib에 위치한 libsqlite3.dylib라는 이름의 동적 라이브러리 파일을 애플리케이션에 링크시켜야 한다.
- Xcode로 이동한 뒤 Persistence 프로젝트에서 Groups & Files의 Frameworks을 선택하고 Project 메뉴의 [Add to Project ..]를 클릭한다. 그리고 나서 /Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulatorX.Y.sdk/usr/lib으로 이동하여 libsqlite3.dylib라는 이름의 파일을 찾아서 추가한다.
- 메시지 창이 뜨면 Copy items into destination group's folder (if needed)항목이 체크 해제되었는지 확인한다. 그리고 Reference Type을 Relative to Current SDK로 변경하고 추가 add버튼을 누른다.
- 만일 Reference Type을 Absolute Path로 선택하였다면,/usr/lib/libsqlite3.dylib와 같이 절대 경로를 사용하여 링크하게 될 것이다.

#### PersistenceViewController.h ####
#import "PersistenceViewController.h"
#import "FourLines.h"

@implementation PersistenceViewController
@synthesize field1;
@synthesize field2;
@synthesize field3;
@synthesize field4;

// 데이터 파일의 전체 경로를 반환한다. 
-(NSString *)dataFilePath
{
	// Documents 디렉토리의 경로를 paths에 저장한 다음 kFilename을 paths에 붙여서 전체 경로를 나타내는 문자열을 만든다.
	NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
	NSString *documentsDirectory = [paths objectAtIndex:0];
	return [documentsDirectory stringByAppendingPathComponent:kFilename];
}

// 메서드가 NSNotification의 포인터를 인자로 사용한다는 것을 기억하자.
// applicationWillTerminate: 메서드는 노티피케이션 notification 메서드이고 
// 모든 노티피케이션들은 하나의 NSNotification 인스턴스를 인자로 사용한다.
// 노티피케이션은 객체 간에 서로 커뮤니케이션을 할 수 있도록 만들어진 간단한 메커니즘이다.
// 모든 객체는 1개 이상의 노티피케이션을 정의할 수 있으며 그것은 애플리케이션의 노티피케이션 센터(notification center)에서 발급된다. 
// 노티피케이션 센터란 객체들 사이에 노티피케이션을 넘겨주기 위하여 존재하는 싱글턴 객체이다. 
// 일반적으로 노티피케이션은 이벤트와 노티피케이션을 발급한 객체가 전달되었다는 것을 알려주는 표시이다.
// 애플리케이션은 종료 전에 데이터를 저장해야 하므로 UIApplicationWillTerminateNotification라는 노티피케이션을 사용해야 한다.
-(void)applicationWillTerminate:(NSNotification *)notification
{	
	// 데이터를 저장하기 위해, for 루프를 사용하여 각각의 행을 업데이트하는 UPDATE 문을 생성
	for( int i = 1; i <= 4; i++)
	{
		// 필드 이름을 생성하고 필드 이름을 인자로 사용하여 텍스트 필드 아웃렛을 가져왔다.
		// valueForKey: 메서드가 이름을 키(key) 값으로 사용하여, 프로퍼티를 반환했다는 점이다.
		NSString *fieldName = [[NSString alloc] initWithFormat:@"field%d", i];
		UITextField *field = [self valueForKey:fieldName];
		[fieldName release];
		
		// 에러가 발생했을때 에러 메시지를 출력할 수 있도록 포인터 선언
		char *errorMsg;
		// 표준 SQL문인 INSERT문 대신에 INSERT OR REPLACE를 사용함으로써 이미 같은 행번호를 가지는 레코드가
		// 테이블에 저장되어 있어도 신경 쓸 필요가 없다.
		char *update = "INSERT OR REPLACE INTO FIELDS (ROW, FIELD_DATA) VALUES (?, ?);";

		sqlite3_stmt *stmt;
		
		// 바인딩 변수를 사용한 쿼리 명령문을 준비하고 바인딩 변수를 실제 값으로 치환.
		if(sqlite3_prepare_v2(database, update, -1, &stmt, nil) == SQLITE_OK)
		{
			sqlite3_bind_int(stmt, 1, i);
			sqlite3_bind_text(stmt, 2, [field.text UTF8String], -1, NULL);
		}
		
		// 쿼리 명령문을 실행하기 위해 sqlite3_step함수를 호출하였으며, 쿼리 명령문이 실행되었는지 확인하고
		// 명령문을 소멸시키고 데이터베이스를 닫는다.
		
		// 에러가 발생했을 때 assertion문을 사용한 점을 주목하자. 이러한 종류의 에러는 사용자가 애플리케이션을 사용하면서
		// 발생하는 것이 아니라 개발자의 실수로 발생하기 때문에 예외처리나 직접적인 에러 확인보다는 assertion을 사용하는것이 좋다.
		// assertion 매크로를 사용하는 것은 우리가 작성한 코드를 디버깅하는 데 사용될 수 있으며 최종적으로 만들어지는 
		// 애플리케이션에는 assertion 매크로는 제외돌 것이다.
		if(sqlite3_step(stmt) != SQLITE_DONE)
			NSAssert1(0, @"Error updating table: %s", errorMsg);
		
		sqlite3_finalize(stmt);		  	   
	}
	sqlite3_close(database);
}

#pragma mark -
-(void)viewDidLoad
{
	// 먼저 데이터베이스를 열었다. 만일 데이터 베이스를 여는데 문제가 있다면, 데이터베이스는 닫힐 것이고 assertion이 일어날것이다.
	if(sqlite3_open([[self dataFilePath] UTF8String], &database) != SQLITE_OK)
	{
		sqlite3_close(database);
		NSAssert1(0, @"Failed to open database", "");
	}
	
	// 데이터를 저장할 테이블을 생성하였다.
	// CREATE TABLE SQL문을 사용하면 테이블을 생성할수 있다. 
	// IF NOT EXISTS을 사용하여 데이터베이스가 기존에 존재하는 데이터를 덮어써버리는 것을 방지 할수 있다.
	// 만일 같은 이름에 테이블이 존재한다면, 쿼리문은 아무 일도 하지 않고 조용히 종료될 것이다.
	// 그래서 테이블의 존재를 확인하지 않고도 애플리케이션이 실행될 때마다 매번 호출할 수 있다.
	char *errorMsg;
	NSString *createSQL = @"CREATE TABLE IF NOT EXISTS FIELDS (ROW INTEGER PRIMARY KEY, FIELD_DATA TEXT);";
	
	if(sqlite3_exec(database, [createSQL UTF8String], NULL, NULL, &errorMsg) != SQLITE_OK)
	{
		sqlite3_close(database);
		NSAssert1(0, @"Error creating table : %s", errorMsg);
	}
	
	// 데이터를 가져오는 작업
	NSString *query = @"SELECT ROW, FIELD_DATA FROM FIELDS ORDER BY ROW";
	sqlite3_stmt *statement;
	
	if(sqlite3_prepare_v2(database, [query UTF8String], -1, &statement, nil) == SQLITE_OK)
	{	
		// while문을 사용하여 반환받은 각각의 레코드를 순환하였다.
		while(sqlite3_step(statement) == SQLITE_OK)
		{
			// 행 번호를 가져와서 int변수에 저장하고, 필드 데이터 문자열의 포인터를 C문자열 포인터에 변수에 저장한다.
			int row = sqlite3_column_int(statement, 0);
			char *rowData = (char *)sqlite3_column_text(statement, 1);
			
			// 행 번호를 참조하여 필드 이름을 만들고, C문자열을 NSString으로 변환하였다.
			NSString *fieldName = [[NSString alloc] initWithFormat:@"field%d", row];
			NSString *fieldValue = [[NSString alloc] initWithUTF8String:rowData];
			
			// 텍스트 필드에 데이터베이스로부터 가져온 값을 설정하기 위해 필드 이름과 NSString 포인터를 사용하였다.
			UITextField *field = [self valueForKey:fieldName];
			field.text = fieldValue;
			
			[fieldName release];
			[fieldValue release];		
		}
		sqlite3_finalize(statement);
	}
	
	
	// 애플리케이션 인스턴스 포인터를 만들었다.
	// NSNotificationCenter의 기본 인스턴스 객체의 addObserver:selector:name:object:메서드를 호출하여
	// UIApplicationWillTerminateNotification 노티피케이션 구독하였다.
	UIApplication *app = [UIApplication sharedApplication];
	
	// 우리는 PersistenceViewController를 옵저버(Observer)객체로 지정하였다. 
	// 이것은 PresistenceViewController 클래스가 노티피케이션을 받겠다는 것을 의미한다.
	// applicationWillTerminate:를 셀렉터로 지정하여 노티피케이션이 발급되면 노티피케이션 센터가 
	// applicationWillTerminate:를 호출할 수 있게 하였다.
	// 세번째 파라미터인 name:은 구독하려는 노티피케이션의 이름이며 마지막 인자인 object:는 노티피케이션을 발급하는 객체이다.
	// 만일 마지막 인자를 nil로 지정하면 UIApplicationWillTerminateNotification을 발급하는 모든 노티피케이션을 구독한다.
	[[NSNotificationCenter defaultCenter] addObserver:self 
											 selector:@selector(applicationWillTerminate:)
												 name:UIApplicationWillTerminateNotification
											   object:app];
	
	[super viewDidLoad];
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
}

- (void)viewDidUnload {
	self.field1 = nil;
	self.field2 = nil;
	self.field3 = nil;
	self.field4 = nil;
	[super viewDidUnload];
}

- (void)dealloc {
	[field1 release];
	[field2 release];
	[field3 release];
	[field4 release];
    [super dealloc];
}

@end

* 코어 데이터 사용하기
- 아이폰 SDK에서 지원하지 않는 기능 중 가장 주목할 만한 것은 데이터 ㅁ모델을 시각적으로 설계할 수 있는 애플의 도구인 코어데이터이다. 애플은 아이폰 SDK3부터 코어 데이터를 지우너하기 시작했으며, 코어 데이터릐 지원은 아이폰 개발자들에게는 큰 축복이었다.
- Xcode에서 새 프로젝트를 생성한다. 이번에는 Window-based Application 템플릿을 사용하되 Choose버튼을 누르기 전에 오른쪽 하단에 보면 Use Core Data for storage라는 체크박스를 체크한다. 파일명은 Core_Data_Persistence라고 입력한다.

* 엔티티와 관리객체
- Resources 폴더에는 Core_Data_Persistence.xcdatamodel이라는 이름의 처음보는 파일이 들어 있을것이다. 이 파일은 데이터 모델 파일이다. 코어 데이터는 코드 작성 없이도 애플리케이션의 데이터 모델을 시각적으로 설계할 수 있도록 해준다.
- '엔티티'와 '관리객체' 모두가 데이터 모델 객체를 참조하기 때문에 이 두 용어가 조금은 혼동스러울 수 있다. '관리객체'는 애플리케이션이 실행될 때 생성된 '엔티티'의 실제 인스턴스를 참조하는데, 사용되며 '엔티티'라는 용어는 '관리객체'가 인스턴스를 참조하는 동안 객체의 설명을 참조하는데 사용된다.
- 엔티티는 총 4개의 프로퍼틸로 구성된다. 그것은 바로 속성, 관계, 페치드(fetched) 프로퍼티, 페치 리퀘스트(tetch request)이다.
- 속성은 오브젝티브 C 클래스의 인스턴스 변수에 해당하는 코어 데이터 엔티티의 함수이다.
- 관계(relationship)는 엔티티 사이의 관계를 정의한다. 관계는 하나 혹은 그 이상일 수 있다.
- 페치드 프로퍼티는 관계와는 동시에 사용될 수 없는 특성이다. 즉,  둘 중 하나를 선택해 사용해야 한다. 관계와의 차이점은 로딩 방식이 다르다는 것이다. 예를 들어, 사람과 집주소가 관계로 이어져 있다면 사람이 로딩될 때 집주소도 로딩될 것이다. 반대로 사람이 집주소를 페치드 프로퍼티로 참조하고 있다면 사람이 로딩되더라도 집주소는 로딩되지 않는다. 적어도 집주소에 접근하기 전까지는 말이다. 일존의 '지연로딩'
- 페치 리퀘스트는 미리 정의된 쿼리이다.

* 키-밸류 코딩
- 관리객체를 사용할 때 프로퍼티의 값을 가져오거나 저장하기 위해 속성의 이름을 키로 사용한다. 관리객체로부터 name이라는 속성에 저장된 값을 가져오기 위해 다음과 같이 호출 할 수 있다.
NSString *name = [myManagedObject valueForKey:@"name"];

- 비슷한 방법으로 새로운 값을 관리객체의 프로퍼티에 저장하려면 다음과 같이 사용한다.
[myManagedObject setValue:@"Martha Steware" forKey:@"name"];

* 컨텍스트에 모든 것을 밀어 넣기
- 그렇다면 이 관리객체들은 어디에 존재하는 것일까? 그들은 퍼시스턴스 스토어(persistent store)라는 곳에 있으며 퍼시스턴스 스토어는 이따금씩 백킹 스토어(backing store, 일종의 보조 기억장치)의 형태로 참조되기도 한다.
- 퍼시스턴스 스토어는 몇 가지 서로 다른 형태를 가진다. 기본적으로 Core Data 애플리케이션은 애플리케이션의 디렉토리 안에 있는 SQLite 데이터베이스와 같은 백킹 스토어를 만든다. 심지어 데이터가 SQLite통해 저장되더라도, 코어 데이터 프레임워크 안의 클래스들이 데이터의 저장과 로딩에 관련된 모든 일을 처리한다. 만일 코어 데이터를 사용한다면 SQL문을 직접 사용하지 않아도 된다. 오직 객체를 다루는 것만으로 코어 데이터가 필요한 SQL문들을 처리해주기 때문이다. 백킹스토어는 SQLite뿐만 아니라 가벼운 파일과 같은 형태로 사용될 수 있다.
- 대부분의 애플리케이션들이 오직 하나의 저장소만을 사용하지만 하나의 애플리케이션에서 다수의 저장소를 사용하는 것도 가능하다.
- 보통 저장소를(저장소는 애플리케이션의 델리케이트 안에서 치리된다.) 직접 생성하는 것보다 컨텍스트(Context)를 참조하는 관리객체 컨텍스트(managed object context)를 호출하여 생성한다. 이 컨텍스트 중개자는 저장소에 접근하여 객체가 마지막으로 저장되었을 때부터 지금까지 프로퍼티의 변경된 정보들을 가지고 있다. 또한 컨텍스트는 취소 관리자(undo manager)를 통해 모든 변경 사항을 기록하여 언제든지 실행한 작업을 취소하거나 마지막으로 데이터라 저장됐을 때로 롤백 (rollback)할 수 있다.
- 대부분의 코어 데이터의 함수들을 호출할 때는 NSManagedObjectContext를 인자로 필요로 하거나 컨텍스트에 실행될 수 있어야 한다. 복잡한 멀티스레드 아이폰 애플리케이션을 제외한 대부분의 경우에 애플리케이션의 델리케이트로부터 managedObjectContext 프로퍼티를 가져와 사용할 수 있다.

* 관리객체 생성하기
- 관리객체를 생성할때는 alloc이나 inint대신에 NSEntityDescription클래스의 팩톨리 메서드를 사용한다. 이 클래스의 인스턴스들은 메모리 안에서 단일 엔티티를 나타낸다. 엔티티는 클래스와 유사하다는 것을 기억하자. 엔티티는 객체의 정보를 가지고 있으며 특정 엔티티가 가지는 프로퍼티들을 정의하고 있다.
- 새로운 객체를 생성하는 코드는 다음과 같다.
theLine = [NSEntityDescription insertNewObjectForEntityForName:@"EntityName" inManagedObjectContext:context];

- insertNewObjectForEntityForName:inManagedObjectContext: 메소드는 객체를 생성하는 일 외에도 생성된 객체를 컨텍스트에 저장하고 오토릴리즈된 객체를 반환하는 일을 한다. 위의 코드는 호출했어도 컨텍스트 상의 객체는 아직 저장소에 저장된 것이 아니다. 객체는 저장소에 저장하려면 관리객체 컨텍스트의 save:메서드를 호출해야 한다.

* 관리객체 가져오기
- 저장소에서 관리객체를 가져오려면 페치 리퀘스트를 하나 만들고 객체의 엔티티나 가져오길 원하는 객체를 명시한 NSEntityDescription을 사용하여 요청해야 한다.
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entityDescr = [NSEntityDescription entityForName:@"EntityName" inManagedObjectContext:context];
[request setEntity:entityDescr];

- 필요하다면 NSPredicate를 사용한 페치 리퀘스트의 조건을 명시할 수 있따. NSPredicate는 페치 리퀘스트의 실행 결과를 만들기 위해 필요한 기준을 정의하기 위해 사용되며 SQL문과 유사하다.
NSPredicate *pred = [NSPredicate predicateWithFormat:@"(name =%d)", nameString];
[request setPredicate:pred];

- 생성한 NSPredicate는 페치 리퀘스트에 명시된 엔티티의 모든 객체를 가져오는 대신 name프로퍼티에 nameString값이 저장되어 있는 객체만을 자여오게 하였다. 그래서 nameString이 @"Bob"이라는 값을 가지고 있다면 페치 리퀘스트는 오직 name 프로퍼티가 "Bob"인 객체만을 가져오게 될것이다.
- 엔티티 디스크립션과 함께 페치 리퀘스트를 생성하고 나면 NSManagedObjectContext: 인스턴스 메서드를 사용하여 패치 리퀘스트를 실행한다.
NSError *error;
NSArray *objects = [context executeFetchRequest:request error:&error];
if(objects == nil)
{
//handle error
}

- executeFetchRequest:error:는 저장소로부터 특정 객체를 불러올 것이며 배열을 통해 객체를 반환할 것이다. 만일 에러가 발생하면 배열 대신 nil값을 반환받게 될것이고 인자로 사용한 error포인터는 에러의 내용이 담긴 NSError객체를 가리키게 될 것이다. 에러가 발생하지 않으면 명시된 기준과 일치하는 데이터가 없어서 아무런 값을 담고 있지 않더라도 제대로 된 배열을 반환반게 될 것이다. 배열을 반환받는 시점부터 배열 안의 관리객체에 대한 모든 변경 내용은 관리객체 컨텍스트에 의해 기록될 것이며 컨텍스트에 save: 메시지를 보내면 저장될 것이다.

* 데이터 모델 설계하기
- Persistence_Core_Data.xcdatamodel을 클릭하고 Xcode의 데이터 모델 편집기를 실행한다.
- 데이터 모델 편집기의 왼쪽 상단의 창은 현재 데이터 모델의 모든 엔티티를 보여주기 때문에 엔티티 창(Entity pane)이라고 한다.
- 엔티티 창의 왼쪽 아래의 십자 모양의 아이콘을 클릭하면 Entity라는 이름의 엔티티 하나가 생성되고 선택될 것이다.
- 데이터 모델 편집기의 하단 창에는 새로운 무언가가 추라되었다. 상단의 3개의 창을 사용하여 데이터 모델을 만들면 화면의 하단 창에 데이터 모델이 시각적으로 표현되어 나타날 것이며 이것을 다이어그램 뷰(Diagram view)라고 한다.
- 데이터 모델 편집기의 우측 상단 창은 상세 정보창(detail pane)이라고 부른다. 데이터 모델 편집기의 상세 정보창은 현재 선택한 엔티티나 프로퍼티를 편집할 수 있는 기능을 제공한다.
- Name필드의 값을 Entity에서 Line으로 변경하자.
- 데이터 모델 편집기의 위쪽 창은 프로퍼티 창(property pane)이라고 한다. 프로퍼티 창은 엔티티에 새 프로퍼티를 추가 할때 사용한다.
- 프로퍼티 창의 왼쪽 하단의 모서리에서 있는 십자 버튼을 클릭하고 Add Attribute를 선택한다. 상세 정보창에서 속성의 이름을 newAttribute에서 lineNum로 변경하고 Integer값을 저장 할수 있도록 type을 Integer 16으로 변경하자. Name 필드 아래 체크 박스에서 Optional속성을 사용하지 않을 것이므로 체크를 해지 한다. 인터페이스 상에서 라인은 레이블에 대응되지 않으므로 필요가 없다. Transient속성은 미리 정의도니 속성 타입이 없는 비표준 객체를 저장하기 위해 사용된다. Indexed는 이 속성의 데이터를 저장하기 위해 SQL데이터베이스 내부의 인덱스를 생성하다.
- 다시 플러스 아이콘을 클릭하고 Add Attribute를 선택한다. 이번에는 lineText라는 이름의 속성을 하나 만들고 타입은 String으로 변경한다.

* Presistence 뷰와 컨트롤러 생성하기
- 윈도우 기반의 애플리케이션 템플릿을 선택하였으므로 뷰 컨트롤러가 제공되지 않는다. 그래서 Classes폴더를 클릭하고 새로운 파일을 추가 한다. Cocoa Touch Class의 UIViewController subclass를 선택한다. 파일 이름은 PersistenceViewController라고 입력하고 헤더 파일도 같이 생성한다. 또한 With XIB for user interface가 체크되었는지를 확인한다.

#### PersistenceViewController.h ####
#import 

@interface PersistenceViewController : UIViewController 
{
	UITextField *line1;
	UITextField *line2;
	UITextField *line3;
	UITextField *line4;
}

@property (nonatomic, retain) IBOutlet UITextField *line1;
@property (nonatomic, retain) IBOutlet UITextField *line2;
@property (nonatomic, retain) IBOutlet UITextField *line3;
@property (nonatomic, retain) IBOutlet UITextField *line4;

@end

#### PersistenceViewController.m ####
#import "PersistenceViewController.h"
#import "Core_Data_PersistenceAppDelegate.h"

@implementation PersistenceViewController
@synthesize line1;
@synthesize line2;
@synthesize line3;
@synthesize line4;

-(void)applicationWillTerminate:(NSNotification *)notification
{
	// 레퍼런스를 사용하여 생성된 관리객체 컨텍스트의 포인터를 만듬
	Core_Data_PersistenceAppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
	NSManagedObjectContext *context = [appDelegate managedObjectContext];
	
	// nil배열이 반환되었을 때 어떠한 에러가 발생했는지 시스템으로부터 가져오기 위해 NSError 포인터 하나를 생성
	NSError *error;
	
	// 각각의 레이블을 생성하기 4번 반복되는 순환문을 실행
	for(int i=1; i <= 4; i++)
	{
		// i 변수 값을 line이라는 글자 뒤에 붙이는 것으로 4개의 레이블의 이름을 만들고 valueForKey:를 사용하여
		// 적합한 필드를 가리키는 레퍼런스를 얻는데 사용
		NSString *fieldName = [NSString stringWithFormat:@"line%d",i];
		UITextField *theField = [self valueForKey:fieldName];
		
		// 페치 리퀘스트 하나를 만듬		
		NSFetchRequest *request = [[NSFetchRequest alloc] init];
		
		// 데이터 모델 편집기에서 설계한 Line엔티티를 서술하는 엔티티 디스크립션을 생성
		// 애플리케이션의 델리케이트로부터 컨텍스트를 가져오는데 사용
		// 엔티티 디스크립션을 생성하면 이것을 페치 리퀘스트에 넘겨서 리퀘스트가 어떤 타입의 엔티티를 찾아야 할지를 알려준다.
		NSEntityDescription *entityDescription = [NSEntityDescription entityForName:@"Line" inManagedObjectContext:context];
		[request setEntity:entityDescription];
		
		// 저장소에 관리객체가 저장되어 있는지를 확인하여 필드의 으측 객체를 식별하는 NSPredicate를 생성
		NSPredicate *pred = [NSPredicate predicateWithFormat:@"(lineNum = %d)", i];
		[request setPredicate:pred];
		
		// 아직은 저장소에서 관리객체를 가져올지 아니면 새로 운 관리객체를 만들지를 알수 없으므로 nil값을 할당		
		NSManagedObject *theLine = nil;
		
		// 컨텍스트에 페치 리퀘스트를 실행
		NSArray *objects = [context executeFetchRequest:request error:&error];
		
		// objects 변수가 nil값인지를 확인.. 만일 nil 값이라면 에러가 발생하였을 것이고 애플리케이션에
		// 필요한 에러 확인을 할 것이다. 
		if(objects == nil) {
			NSLog(@"There was an error!");
		}
		
		// 기준에 맞는 객체를 반환하였는지 확인하고 객체가 있다면 로딩하도록 한다.
		// 만일 객체를 반환받지 못하면 새로운 객체를 생성하여 텍스트 필드의 텍스트를 관리객체에 저장하기 위해 새로운 관리객체를 생성
		if([objects count] > 0)
			theLine = [objects objectAtIndex:0];
		else
			theLine = [NSEntityDescription insertNewObjectForEntityForName:@"Line" inManagedObjectContext:context];
		
		// 키-벨류 코딩을 사용하여 라인의 번호와 텍스트를 관리객체에 설정한다.
		[theLine setValue:[NSNumber numberWithInt:i] forKey:@"lineNum"];
		[theLine setValue:theField.text forKey:@"lineText"];
		
		[request release];
	}
	
	// 컨텍스트에 변경사항을 저장한다.
	[context save:&error];
}

- (void)viewDidLoad 
{
	// 애플리케이션의 델리게이트를 참조하는 레퍼런스를 만들고 애플리케이션의 기본 컨텍스트를 가져오는 코드를 생성
	Core_Data_PersistenceAppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
	NSManagedObjectContext *context = [appDelegate managedObjectContext];
	
	// 엔티티를 나타낼 엔티티 디스크립션을 생성
	NSEntityDescription *entityDescription = [NSEntityDescription entityForName:@"Line" inManagedObjectContext:context];
	
	// 페치 리퀘스트를 생성하고 엔티티 디스크립션을 리퀘스트로 넘겨줘서 어떠한 객체를 받아올 것인지를 설정
	NSFetchRequest *request = [[NSFetchRequest alloc] init];
	[request setEntity:entityDescription];
	
	// 저장소에 저장된 모든 Line 객체들을 받아와야 하므로 NSPredicate는 만들지 않고 리퀘스트를 실행하는 것으로
	// 저장된 모든 Line 객체를 가져오도록 컨텍스트에 요청
	NSError *error;
	NSArray *objects = [context executeFetchRequest:request error:&error];
	
	// 배열 값을 반환하였는지 확인하고 로그를 출력
	if(objects == nil)
	{
		NSLog(@"There was an error!");
	}
	
	// 루프 안에서 저장소에서 가져온 관리객체로부터 lineNum와 lineText값을 가져오고 사용자 인터페이스의 텍스트
	// 필드에 이 값들을 업데이트
	for(NSManagedObject *oneObject in objects)
	{
		NSNumber *lineNum = [oneObject valueForKey:@"lineNum"];
		NSString *lineText = [oneObject valueForKey:@"lineText"];
		NSString *fieldName = [NSString stringWithFormat:@"line%@", lineNum];
		UITextField *theField = [self valueForKey:fieldName];
		theField.text = lineText;
	}
	
	[request release];
	
	// 애플리케이션이 종료될 때 사용자가 수정한 모든 변경 사항을 저장할 수 있게..
	UIApplication *app = [UIApplication sharedApplication];
	[[NSNotificationCenter defaultCenter] addObserver:self
											 selector:@selector(applicationWillTerminate:)
												 name:UIApplicationWillTerminateNotification
											   object:app];
	
	[super viewDidLoad];
	
}

- (void)didReceiveMemoryWarning {
    // Releases the view if it doesn't have a superview.
    [super didReceiveMemoryWarning];
    
    // Release any cached data, images, etc that aren't in use.
}

- (void)viewDidUnload {
	self.line1 = nil;
	self.line2 = nil;
	self.line3 = nil;
	self.line4 = nil;
    [super viewDidUnload];
    // Release any retained subviews of the main view.
    // e.g. self.myOutlet = nil;
}

- (void)dealloc {
	[line1 release];
	[line2 release];
	[line3 release];
	[line4 release];
    [super dealloc];
}


@end

* Persistence 뷰 컨트롤러를 애플리케이션의 최상위 컨트롤러로 지정하기
- 윈도우 기반의 애플리케이션 템플릿을 사용하였으므로 애플리케이션을 실행하기 전에 해야 할 일이 한가지 더 있다. 애플리케이션의 최상위 컨트롤러로 사용하기 위해 PersistenceViewController의 인스턴스를 생성하고 Persistence 뷰를 애플리케이션의 메인 윈도우의 하위뷰로 추가 해야 한다.

#### Core_Data_PersistenceAppDelegate.h ####
#import 
#import 

@class PersistenceViewController;

@interface Core_Data_PersistenceAppDelegate : NSObject  {
    
    UIWindow *window;
	PersistenceViewController *rootController;
    
@private
    NSManagedObjectContext *managedObjectContext_;
    NSManagedObjectModel *managedObjectModel_;
    NSPersistentStoreCoordinator *persistentStoreCoordinator_;
}

@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) IBOutlet PersistenceViewController *rootController;

@property (nonatomic, retain, readonly) NSManagedObjectContext *managedObjectContext;
@property (nonatomic, retain, readonly) NSManagedObjectModel *managedObjectModel;
@property (nonatomic, retain, readonly) NSPersistentStoreCoordinator *persistentStoreCoordinator;

- (NSString *)applicationDocumentsDirectory;

@end

#### Core_Data_PersistenceAppDelegate.m ####
#import "Core_Data_PersistenceAppDelegate.h"
#import "PersistenceViewController.h"

@implementation Core_Data_PersistenceAppDelegate

@synthesize window;
@synthesize rootController;

#pragma mark -
#pragma mark Application lifecycle

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {    
	
    // Override point for customization after application launch.
	
	[window addSubview:rootController.view];
    [window makeKeyAndVisible];
	
	return YES;
}

- 마지막으로 인터페이스 빌더로 돌아가서 최상위 컨트롤러의 인스턴스를 생성하고 조금 전에 생성한 아웃렛과 연결해야 한다.
- MainWindow.xib를 더블 클릭해서 인터페이스 빌더를 실행한다. 라이브러리에서 View Controller를 가져와 MainWindow.xib라는 nib 메인 윈도우에 놓는다. Func + 4를 눌러서 아이텐티티 인스펙터를 띄우고 하위 클래스를 UIViewController에서 PersistenceViewController로 변경한다. 그리고 Core_Data_Persistene App Delegate아이콘을 Persistence View Controller 아이콘으로 컨트롤-드래그 한다. rootController 아웃렛을 선택한다.
:

[iPhone] - 소스 모음 추천사이트 (appsamuck)

iPhone/추천사이트 2010. 8. 12. 10:53

* iPhone 관련 유용한 소스를 받아볼수 있는 사이트
- 약 30개의 소스를 무료로 다운받을수 있으며 상세보기 창에서 해당 소스를 실행할수 있는 방법에 대해서 매우 잘 설명해 놓아다.. 당연히 영어로 되어 있다... (젠장 이놈에 영어.... )

appsamuck URL : http://www.appsamuck.com/
:

[iPhone] - 시작하세요! 아이폰 3 프로그래밍 - Part 10. 애플리케이션 설정과 사용자 기본값

iPhone/[위키북스]시작하세요! 아이폰3 프로그래밍 2010. 8. 10. 21:58
* 세팅 번들 이해하기
- Settings 애플리케이션은 어떤 애플리케이션이든지 세팅 번들이 있으면 사용자가 환경설정을 할 수 있게 해준다. 세팅 번들은 애플리케이션 안의 파일 묶음인데 그것은 Settings 애플리케이션에 어떤 환경설정을 사용자로부터 수집하고 싶은지 알려준다.

- 아이폰에서 사용자들이 기본값을 설정할 때 Settings 애플리케이션을 공용 사용자 인터페이스로 사용한다. 사용자가 기본설정은 애플리케이션 환경설정의 일부이고 설정을 저장하고 가져오는 역할을 한다. 사용자 기본설정은 NSUserDefaults클래스로 구현한다.

- 애플리케이션은 NSUserDefaults를 사용해서 환경설정 데이터를 키(Key)값을 통해 읽고 저장하는데 이것은 NSDictionary에서 키 값으로 데이터에 접근하는것과 똑같다. 맥과 다른 점은 NSUserDefaults 데이터는 메모리에 객체 인스턴스로 저장하는 것이 아니라 파일 시스템에 저장된다는 것이다.

- 애플리케이션에서 사용할 설정으로 프로퍼티 리스트를 만들면 Settings 애플리케이션이 인터페이스를 만들어 준다.

* 프로젝트 생성하기
- Xcode에서는 New Project를 선택한다. 새 프로젝트 도우미가 뜨면 왼쪽 창 iPhone밑의 Application를 선택하고 Utility Application아이콘을 클릭한다. 이름음 AppSettings이라고 한다.
- 이 프로젝트 템플릿을 살펴 보면 메인 뷰가 있고, 플립사이드(Flipside)뷰라고 부르는 보조 뷰가 있다. 메인 뷰의 정보 버튼을 탭하면 플립사이드 뷰가 나타나고, 플립사이드 뷰의 Done 버튼을 탭하면 다시 메인 뷰가 나타난다.

- 뷰 컨트롤러와 UIView의 하위클래스를 포함해서 메인 뷰를 구성하는 모든 클래스들은 Main View폴더안에 들어 있다. 플립사이드 뷰를 구현하는 데 필요한 모든 소스코드 파일은 Flipside View폴더에 있다. 애플리케이션 델리케이트는 Application Delegate폴더에 담겨 있다.

* 세팅 번들 다루기
- Settings 애플리케이션은 애플리케이션의 환경설정을 보여줄 때 그 애플리케이션 안의 세팅 번들을 기반으로 동작한다. 각 세팅 번들은 최상위 환경설정 뷰를 정의하는 Root.plist 프로퍼티 리스트를 포함해야 한다. 만약 적절한 Roop.plist 파일과 세팅 번들을 찾았다면 Settings 애플리케이션은 애플리케이션의 프로퍼팉 리스트 내용에 기반해서 설정 뷰를 만든다. 만약 설정에 하위뷰를 추가하고 싶다면, 추가 프로퍼티 리스트를 번들에 추가해야 하고 각 항목을 Root.plist에 추가해야 한다.

* 세팅 번들을 프로젝트에 추가하기
- AppSettings에서 새로운 파일을 추가 한다. 왼쪽 창에서 iPhone OS 밑의 Resource에서 Settings Bundle아이콘을 선택한다. 파일 이름은 Settings.bundle이다.

* 프로퍼티 리스트 설정하기
- Root.plist 파일을 보면 종류가 딕셔너리인 root 노드가 있는데, 이것은 NSDictionary처럼 값을 키 값을 사용해서 저장한다. 모든 딕셔너리 노드의 하위 항목은 키와 값이 필요하다. 그리고 주어진 프로퍼티 리스트에 root 노드는 하나만 있어야 하고 모든 노드는 그 밑에 있어야 한다.

- 첫번째 항목인 StringsTable은 애플리케이션의 언어를 다른 언어로 교체 할때 사용된다. 이번 장에서는 삭제....

- Root 노드의 다음 항목은 Type이 배열인 PreferenceSpecifiers이다. 이 배열의 각각은 설정 항목이나 사용자가 하위 항목으로 들어갈 수 있는 하위뷰를 나타낸다.

- Item 0을 클릭하지만 펼치지는 말자. 행의 오른쪽 모서리를 보면 더하기(+)아이콘이 있다. 이 버튼은 이행 다음에 형제 노드를 추가한다. 다시 말해 다른 노드를 이 노드와 같은 레벨에 추가한다.

- 이제 Item 0을 펼쳐 보는데 이때 더하기 버튼이 가로로 세 줄이 있는 다른 아이콘으로 바뀐다. 하위 노드를 추가 할때 가로 세줄 아이콘의 버튼을 클릭하면 된다.

- Item 0 아래의 첫번째 행은 Key가 Type인데 PreferenceSpecifiers 배령에 있는 모든 프로퍼티 리스트노드에는 Key가 Type인 항목이 있어야 한다. Type키는 Settings 애플리케이션이 이 아이템과 관련된 어떤 데이터 타입을 사용해야 하는지를 알게 해준다.

- Value을 PSGroupSpectifier로 변경한다. PSGroupSpecifier 타입 필드는 새 그룹의 시작 부분에 아이템을 나타내는데 사용한다. 각각의 아이템들은 PSGroupSpecifier 타입의 아이템이 나올때 까지 이 그룹의 일부를 따를것이다. Settings 애플리케이션은 설정 항목을 그룹화된 테이블로 제공한다. 그러므로 모든 테이블에 최소한 하나의 그룹은 있어야 하기 때문에 세팅 번들 프로퍼티 리스트의 PreferenceSpecifiers 배열 안에 있는 Item 0는 항상 PSGroupSpecifier로 설정해야 한다.
(개인적 생각 책의 말이 어려운데.. Settings 애플리케이션은 그룹회된 테이블로 제공하기 때문에 다음 PSGroupSpecifier가 나오기 전까지는 현재에 그룹에 묶으라는 의미 인거 같다.)

- Item 0에 있는 또 다른 항목의 Key는 Title이고 이것은 그룹화된 테이블용 헤더로 쓰인다. 값을 General Info로 변경한다.

* 텍스트 필드 설정 추가 하기
- Item 0의 펼쳐보기 삼각형을 접은 다음 Item 0을 선택하고 행 끝의 더하기 버튼을 클릭한다.

- 새 행의 Type은 기본값이 String으로 되어 있는데 PreferenceSpecifiers 배열에 있는 각 항목은 Dictionary여야 하기 때문에 String을 클릭하고 Dictionary 타입으로 바꾼다.

- 이제 Item 1의 펼쳐보기 삼각형을 클릭하고 새로운 행을 추가 한다.

- 기본 Type이 String인 새 행이 추가된다. 새 행에 있는 key의 기본 값은 New item인데 값을 Type으로 바꾸고 Value 값은 PSTextFieldSpecifier로 입력한다. Settings 애플리케이션에서 사용자가 텍스트 필드를 사용해서 설정값을 수정하도록 한다.

- 새로운 행을 추가 하고 텍스트 필드의 레이블을 설정한다. New item인 key를 Title로 변경하고 Value은 Username으로 입력

- 새로운 행을 또 추가 하고 Key를 Key로 입력한다. Value은 username이라고 입력한다. 현재 항목은 어떤 키를 사용해서 텍스트 필드에서 값을 입력받을지 Settings 애플리케이션에게 알려주는 역할을 한다. NSUserDefaults는 NSDictionary처럼키를 사용해서 사용자가 값을 저장하게 해준다.나중에 username에 대한 값을 요청할수도 있다.

- 새로운 행을 추가 하고 Key가 AutocapitalizationType고 Value은 None인 항목을 추가 한다. 이렇게 하면 사용자가 입력할 때 텍스트 필드가 자동으로 첫 글자를 대문자로 바꾸려 하지 않도록 설정한다.

- 마지막으로 새로운 행을 추가 하고 Key가 AutocorrectionType으로 하고 Value는 No로 한다. 이렇게 하면 텍스트 필드에 값을 입력할 때 Settings 애플리케이션이 자동으로 값을 정정하지 않는다.

- 이제 실행... GOGO


* 암호화된 텍스트 필드 설정 추가하기
- 새로운 노드를 쉽게 추가하는 방법을 알려주겠다. PreferenceSpecifiers배열의 Item 1를 접는다. 이제 Item 1를 선택하고 Func키 + C를 눌러 클립도으에 복사한 다음 Func + V를 눌러 다시 붙여 넣는다.

- 새 항목을 추가 하고 Key는 IsSecure로 하고 Type은 Boolean으로 바꾼다. 해당 체크 박스를 체크 한다. 이제 이 필드는 일반 텍스트 필드가 아니라 암호화된 필드인다.


* 멀티밸류 필드 추가하기
- 다음으로 추가할 항목은 멀티밸류 필드이다. 이런 종류의 필드는 더보기 표시가 있는 행을 자동으로 생성해주고 그 행을 클릭하면 다른 테이블 뷰가 나타나 여러 행 중에서 하나의 값을 선택하게 한다.

- Item 3를 추가한다. Item 3의 타입을 Dictionary로 바꾸고 펼쳐보기 삼각형을 클릭해서 Item 4를 펼친다.

- Key가 Type이고 Value가 PSMultiValueSpecifier인 하위 행을 만든다. 두번째 행은 Key를 Title로, Value를 Protocol로 생성한다. Key가 Key이고 Value가 protocol인 세번째 행을 추가한다.

- Item 3에 Array 타입 노드를 하위 항목으로 두개 더 추가 할것이다. 그 중 하나는 Titles이고 여기에는 사용자가 선택할 값을 넣는다.(사용자가 선택할때 보게 되는 값)

- 다른 하나인 Values는 사용자가 기본값으로 실제 저장될 값의 목록이다. 즉 사용자가 목록에서 Titles 배열의 첫 번째 아이템에 해당하는 항목을 선택하면 Settings 애플리케이션은 Values배열의 첫 번째 값을 실제로 저장한다.

- 이 Titles와 Values의 상으로 사용자들이 편하도록 타이틀을 보여주지만 실제 저장할 때는 숫자나, 날짜, 다른 문자열처럼 다른 값을 저장 한다. 두 배열 모두 필수 있다.

- Item 3에 새 하위 노드를 추가한다. Key를 Values로 바꾸고 타입을 Array로 한다. 배열을 펼치고 다섯개의 하위 노드를 추가한다. 노드 다섯 개의 타입은 전부 String이고 HTTP, SMTP, NNTP, IMAP, POP3 값을 가진다.

- 다섯 개를 입력하고 나면 Values를 접은 후 이항목을 선택한다. 그런 후에 Func + C를 눌러서 복사하고 Func + V를 눌러서 다시 복사한다. 이렇게 하면 Key가 Values-2인 새항목이 생긴다. Values-2를 Titles로 바꾼다.

- 딕셔너리에 기본값으로 사용할 필수 값 하나가 더 필요하다. 멀티밸류 필드는 꼭 하나의 행이 선택되어야 하므로 아무것도 선택하지 않았을 때의 기본값을 지정해야 하는데 기본값은 Values 배열 중 하나의 값이어야 한다. Item 3에 하위 노드를 추가한다. Key를 DefaultValue로, Value는 SMTP로 한다.


* 토글 스위치 세팅 추가하기
- 사용자에게서 얻어야 할 다음 항목은 워드(Warp) 엔진이 켜져 있는지를 나타내는 Boolean 값이다.

- Settings 애플리케이션의 PreferenceSpecifiers 배열에 타입이 PSToggleSwitchSpecifier인 항목을 추가한뒤 UISwitch를 이용해서 Boolean 값을 얻을 것이다.

- Item 4를 추가한다. 하위 행의 Key를 Type으로, 값은 PSToggleSwitchSpecifier로 한다. 또 다른 행을 추가해서 Key는 Title로, 값은 Wrap Drive로 입력한다. 다음으로 Key가 Key이고 Value가 wrap인 세 번째 행을 추가한다.

- 기본적으로 토글 스위치는 사용자 설정값으로 YES나 NO인 Boolean 값을 가진다. TrueValue / FalseValue키를 추가해서 on / off 위치에 다른 값을 할당할 수 있는데 이들 값이 필수는 아니다.

- Key가 TrueValue이고, Value가 Engaged인 행과 Key가 FalseValue이고, Value가 Disabled인 행을 추가해서 Boolean 값 대신 다른 값을 저장할 수 있게 한다.

- 이 딕셔너리에 기본 값 설정이 하나 더 필요하다. 만약 TrueValue와 FalseValue 행이 없다면 Key가 DefaultValue인 새행을 추가하고 타입을 String에서 Boolean으로 바꾼다. 그러나 두 행을 추가 했으니 TrueValue나 FalseValue의 값 중 하나를 DefaultValue의 Value로 입력한다.

- Key가 DefaultValue, Value를 Engaged로 한다. 문자열 "Engaged"는 사용자 기본값으로 저장되는 값이고 화면에 나타나는 것은 아니다.


* 슬라이더 세팅 추가하기
- Settings 애플리케이션에서 슬라이더는 양 끝쪽에 작은 이미지가 있지만 레이블이 없다. 레이블이 있는 슬라이더용 그룹을 만들고 거기에 슬라이더를 넣어서 사용자가 슬라이더가 무엇을 하는지 알 수 있도록 한다.

- 새로운 행을 하나 추가 하고 Type을 Dictionary로 바꾼다. 추가된 행은 Item 5가 된다.

- 새로운 2개의 하위 행을 생성한다. Key는 Type으로 하고 Value은 PSGroupSpecifier로 설정한다. Key가 Title이고, Value이 Warp Factor인 행을 추가한다. 이 행은 새로운 그룹을 추가하기 위해서 세팅하는것이다.

- 새로운 행을 하나 추가한다. Type을 Dictionary로 바꾼다. 추가된 행은 Item 6가 된다.

- Key가 Type이고 Value가 PSSliderSpecifier인 하위 노드를 추가 한다. 이렇게 하면 UISlider를 사용해서 사용자로부터 정보를 얻는다는 사실을 Setting 애플리케이션에서 알게 된다. Key가 Key이고 Value가 warpfactor인 새 행을 추가해서 이 값을 어떤 키에 저장할지 Settings 애플리케이션이 알게 된다.

- 사용자들이 1에서 10 사이의 값을 입력하도록 하고 기본값은 5로 설정하려 한다. 슬라이더는 최소값, 최대값, 기본 값이 필요하고, 문자열이 아닌 숫자로 저장해야 한다. Item 6에 새개의 하위 행을 추가 해서 이 값을 설정하는데 이 행의 Type을 모두 String에서 Number로 바꾼다. 첫번째 Key를 DefaultValue로 하고 Value는 5를 준다. Key가 MinimumValue이고 Value가 1인 행을 두 번째로 추가하고 마지막 행은 Key를 MaximumValue로 Value은 10으로 입력한다.

- 크기가 21 픽셀 * 21 픽셀인 이미지를 슬라이더의 양 끝에 둘수 있다.

- 이미지 둘 모두 세팅 번들에 추가해야 한다. 그렇게 하려면 파인더를 사용해서 Xcode프로젝트를 저장한 폴더를 열어야 한다. 그 폴더에서 이름이 Setting.bundle인 아이콘을 찾을 수 있다.

- 파인더에서 번들은 파일처럼 보이지만 실제로는 폴더이다. 번들의 내용을 보기 위해 버튼의 아이콘을 마우스 오른쪽 버튼으로 클릭하고 Show Package Contents를 선택한다. 이렇게 하면 새창이 뜨고 Xcode의 Setting.bundle에 있는 것과 같은 항목 두 개가 보인다. 10 AppSettings 폴더의 아이콘 파일 rabbit.png와 turtle.png를 이 폴더에 복사한다.

- Item 6에 하위 행 2개를 추가 한다. 첫번째 행은 Key를 MinimumValueImage로, Value로 turtle.png로 한다. 두 번째 행의 Key는 MaximumValueImage, Value는 rabbit.png로 한다.

* 하위 설정 뷰 추가하기
- 또 다른 설정 지정자를 추가해서 Settings 애플리케이션에서 하위 설정 뷰로 넘어갈 수 있게 할것이다. 이렇게 하면 더보기 펴시가 있는 행이 나타나고 탭하면 화면 전체가 설정인 새 뷰가 나타난다.

- 새로운 행 하나를 추가 한다. 행 이름은 Item 7이 되고 Type은 Dictionary로 변경한다. 새로운 Group을 생성하기 위해서 2개의 하위 뷰를 추가한다. Key를 Type으로 설정하고 Value은 PSGroupSpecifier로 한다. 새로운 하위 행의 Key는 Title로 설정하고 Value은 Additional Info로 설정한다.

- 새로운 행 하나를 추가 한다. 행 이름은 Item 8이 되고 Type은 Dictionary로 변경한다. 하위 행을 추가하고 Key를 Type으로, Value을 PSChildPaneSpecifier로 입력한다. Key가 Title, Value가 More Settings인 또 다른 하위 행을 추가한다.

- 마지막 행을 추가해서 Settings 애플리케이션이 More Settings 뷰를 보여줄 때 어떤 프로퍼티 리스트를 로드할지 알려준다. Key를 File로, Value는 More로 입력한다. 파일 확장자가 .plisit라면 꼭 추가할 필요는 없지만 확장자가 다를 때는 확장자를 입력하지 않으면 Settings 애플리케이션이 프로퍼티 리스트 파일을 찾지 못할 수도 있다.

- 메인 설정 뷰에 하위뷰를 추가하였다. 하위뷰의 설정 항목들은 More.plist 파일에 명시되어 있다. More.plist의 내용을 설정 항목 번들에 복사해야 한다. Xcode에서 새 파일을 번들에 추가할 수 없고 프로퍼티 리스트 에디터의 저장 창에서도 번들에 저장할 수 없다. 그래서 어딘가 다른 곳에서 새 프로퍼티 리스트를 만들어서 저장하고 파인더를 사용해서 Setting.bundle창에 끌어다 놓아야 한다.


* 애플리케이션에서 설정 읽기

- NSUserDefaults 클래스를 사용해서 사용자의 설정을 읽을 것이다. NSUserDefaults는 싱글톤패턴으로 구현되어서 실행 중인 애플리케이션에 오직 하나의 인스턴스만 존재한다. 이 인스턴스에 접근한려면 클래스 메서드인 standardUserDefults를 다음과 같이 호출한다.
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];

- NSUserDefaults의 인스턴스를 한번 얻고 나면 NSDictionary처럼 사용할 수 있다. 설정값을 얻으려면 objectForKey: 메서드를 호출하면 되고 이 메서드는 NSString, NSDate, NSNumber 같은 오브젝트 C객체를 리턴한다. 만약 int, float, BOOL 같은 스칼라 값을 얻으려면 initForKey: floatForKey: boolForKey: 같은 다른 메서드를 사용하면 된다.
- 사용자의 설정 항목과 연결된 모든 설정 지정자에는 Key라는 이름의 키가 있다. 사용자 설정값을 가져올 때 이 키를 사용한다.

#### MainViewController.h ####
#import "FlipsideViewController.h"

#define kUsernameKey @"username"
#define kPasswordKey @"password"
#define kProtocolKey @"protocol"
#define kWarpDriveKey @"warp"
#define kWarpFactorKey @"warpFactor"
#define kFavoriteTeaKey @"favoriteTea"
#define kFavoriteCandyKey @"favoriteCandy"
#define kFavoriteGameKey @"favoriteGame"
#define kFavoriteExcuseKey @"favoriteExcuse"
#define kFavoriteSinKey @"favoriteSin"

@interface MainViewController : UIViewController  {
	UILabel *usernameLabel;
	UILabel *passwordLabel;
	UILabel *protocolLabel;
	UILabel *warpDriveLabel;
	UILabel *warpFactorLabel;
	
	UILabel *favoriteTeaLabel;
	UILabel *favoriteCandyLabel;
	UILabel *favoriteGameLabel;
	UILabel *favoriteExcuseLabel;
	UILabel *favoriteSinLabel;	
}

@property (nonatomic, retain) IBOutlet UILabel *usernameLabel;
@property (nonatomic, retain) IBOutlet UILabel *passwordLabel;
@property (nonatomic, retain) IBOutlet UILabel *protocolLabel;
@property (nonatomic, retain) IBOutlet UILabel *warpDriveLabel;
@property (nonatomic, retain) IBOutlet UILabel *warpFactorLabel;

@property (nonatomic, retain) IBOutlet UILabel *favoriteTeaLabel;
@property (nonatomic, retain) IBOutlet UILabel *favoriteCandyLabel;
@property (nonatomic, retain) IBOutlet UILabel *favoriteGameLabel;
@property (nonatomic, retain) IBOutlet UILabel *favoriteExcuseLabel;
@property (nonatomic, retain) IBOutlet UILabel *favoriteSinLabel;

-(void)refreshFields;
-(IBAction)showInfo:(id)sender;

@end

- MainView.xib를 더블클릭해서 인터페이스 빌더에서 연다. 창이 뜨면 뷰의 배경색이 어두운 회색이라는 것을 발견할 것이다. 이 배경색을 흰색으로 바꿀 것이다. Nib 메인 창의 MainView 아이콘을 클릭하고 Func + 1를 눌러서 속성 인스펙터를 띄운다. Background 항목의 색 선택 창을 사용해서 배경을 흰색으로 바꾼다.
- MainView.xib 파일에서 View Mode를 리스트 모드로 변경한다. View 아이콘의 왼쪽에 있는 삼각형 아이콘을 클릭하면 Light Info Button라는 아이콘을 볼수 있다. Light Info Button 아이콘을 선택하고 Func+1를 눌러 속성 인스펙터를 띄운다. 버튼의 Type을 Info Light에서 Info Dark로 변경한다.
- 이제 레이블들을 Main View에 추가하여 아래 그림처럼 총 20개의 레이블을 설정한다. Text값이 없어서 그렇지 레이블 옆에 또 하나의 레이블이 있다. 그 후에 File's Owner에서 각 레이블로 컨트롤을 누르면서 끌어놓아서 레이블에 설정값이 출력되도록 한다.


#### MainViewController.m ####
@implementation MainViewController

@synthesize usernameLabel;
@synthesize passwordLabel;
@synthesize protocolLabel;
@synthesize warpDriveLabel;
@synthesize warpFactorLabel;
@synthesize favoriteTeaLabel;
@synthesize favoriteCandyLabel;
@synthesize favoriteGameLabel;
@synthesize favoriteExcuseLabel;
@synthesize favoriteSinLabel;

// refreshFields메서드는 프로퍼티 파일에 있는 키 값을 사용해서 사용자 설정값을 읽어 해당하는
// 레이블의 텍스트 프로파티를 설정한다.
-(void)refreshFields
{
	NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
	usernameLabel.text = [defaults objectForKey:kUsernameKey];
	passwordLabel.text = [defaults objectForKey:kPasswordKey];
	protocolLabel.text = [defaults objectForKey:kProtocolKey];
	warpDriveLabel.text = [defaults objectForKey:kWarpDriveKey];
	// 슬라이더의 설정값은 NSNumber 객체를 가져오는데 여기서는 stringValue를 호출해서 객체의 값을 문자열로 표현한 문자열을 가져온다.
	warpFactorLabel.text = [[defaults objectForKey:kWarpFactorKey] stringValue];
	
	favoriteTeaLabel.text = [defaults objectForKey:kFavoriteTeaKey];
	favoriteCandyLabel.text = [defaults objectForKey:kFavoriteCandyKey];
	favoriteGameLabel.text = [defaults objectForKey:kFavoriteGameKey];
	favoriteExcuseLabel.text = [defaults objectForKey:kFavoriteExcuseKey];
	favoriteSinLabel.text = [defaults objectForKey:kFavoriteSinKey];
}

......

// viewDidAppear는 코드에서 서브뷰로 추가될 때만 호출된다.
// viewDidAppear - 이 메서드는 뷰 컨트롤러가 관리하는 각각의 뷰가 뷰 사이의 네비게이션에 의해
// 화면에 완전히 나타난 다음 호출된다. 최초로 뷰가 나타날 때도 호출된다.
// viewDidDisappear - 이 메서드는 viewDidAppear: 와 반대로 뷰 컨트롤러가 관리하는
// 각각의 뷰가 뷰 사이의 네비게이션에 의해 화면에서 사라진 후에 호출된다.
-(void)viewDidAppear:(BOOL)animated
{
	[self refreshFields];
	[super viewDidAppear:animated];
}

.......

// 사용자가 몇가지 항목을 설정할 수 있는 맞은편 뷰를 종료하면 컨트롤러 클래스에서 이벤트가 전달될 것이다.
// 이벤트가 전달될 때 변경된 내용을 보여줄 수 있도록 레이블을 업데이트해야 한다. 아래 메서드가 이벤트를 전달 받는다.
// 반대편 뷰는 자신의 모달 부모인 메인뷰와 함께 모달의 형태로 처리되기 때문에 반대편 뷰가 소멸될때 
// MainViweController인 viewDidAppear:메서드는 호출되지 않는다 그래서 refreshFields:를 이 메서드에 추가한다.
- (void)flipsideViewControllerDidFinish:(FlipsideViewController *)controller {
    [self refreshFields];
	[self dismissModalViewControllerAnimated:YES];
}

......

- (void)viewDidUnload 
{
	self.usernameLabel = nil;
	self.passwordLabel = nil;
	self.protocolLabel = nil;
	self.warpDriveLabel = nil;
	self.warpFactorLabel = nil;
	self.favoriteTeaLabel = nil;
	self.favoriteCandyLabel = nil;
	self.favoriteGameLabel = nil;
	self.favoriteExcuseLabel = nil;
	self.favoriteSinLabel = nil;
	[super viewDidUnload];
}

.....

- (void)dealloc {
	[usernameLabel release];
	[passwordLabel release];
	[protocolLabel release];
	[warpDriveLabel release];
	[warpFactorLabel release];
	[favoriteTeaLabel release];
	[favoriteCandyLabel release];
	[favoriteGameLabel release];
	[favoriteExcuseLabel release];
	[favoriteSinLabel release];
    [super dealloc];
}

.....


* 애플리케이션에서 설정 바꾸기
- 여기서는 Settings애플리케이션이 사용하는 스위치, 슬라이더와 똑같은 컨트롤을 사용할 것이다.

####  FlipsideViewController.h ####
#import 

@protocol FlipsideViewControllerDelegate;


@interface FlipsideViewController : UIViewController {
	id  delegate;
	UISwitch *engineSwitch;
	UISlider *warpFactorSlider;
}

@property (nonatomic, assign) id  delegate;
@property (nonatomic, retain) IBOutlet UISwitch *engineSwitch;
@property (nonatomic, retain) IBOutlet UISlider *warpFactorSlider;

- (IBAction)done:(id)sender;
@end


@protocol FlipsideViewControllerDelegate
- (void)flipsideViewControllerDidFinish:(FlipsideViewController *)controller;
@end

- 이제 FlipsideView.xib를 더블클래서 인터페이스 빌더에서 연다. 속성 인스펙터에서 배경 색을 밝은 회색 그림자로 변경한다. 회색을 25%로 설정하면 될 것이다. 다음으로, 라이브러리에서 Labels 두 개를 끌어서 Flipside View창에 놓는다. 레이블 중 하나를 더블클릭해서 타이틀을 Warp Engines:로 바꾸고 다른 레이블을 더블 클릭해서 타이틀을 Warp Factor: 라고 입력한다.
- 뷰의 상단에 있는 Title을 더블클릭하고 Warp Settings로 변경한다.
- Switch를 라이브러리에서 끌어서 Warp Engines라고 쓰인 레이블 오른쪽에 놓는다. File's Owner 아이콘에서 컨트롤을 누른 채로 클릭해서 새 스위치까지 끌어다 놓고, engineSwitch 아웃렛에 연결한다.
- Slider를 라이브러리에서 끌어서 Warp Factor레이블 밑에 놓는다. 슬라이더를 왼쪽과 오른쪽의 여백에 있는 파란 안내선까지 늘려서 슬라이더의 크기를 조절하고, File's Owner 아이콘에서 슬라이더로 컨트롤을 누른 채로 끌어서 warpFactorSlider 아웃렛에 연결한다.
- 슬라이더를 클릭하고 Func+1을 눌러서 속성 인스펙터를 띄운다. Minimum을 1.00으로, Maximum은 10.0으로, Initial은 5.00으로 설정한다. 다음으로 Min Image의 값으로 trurtle.png를 선택하고, Max Image의 값은 rabbit.png를 선택한다.

#### FlipsideViewController.m ####
#import "FlipsideViewController.h"
#import "MainViewController.h"

@implementation FlipsideViewController

@synthesize delegate;
@synthesize engineSwitch;
@synthesize warpFactorSlider;

- (void)viewDidLoad {
    
	//self.view.backgroundColor = [UIColor viewFlipsideBackgroundColor];
	NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
	// 워프 드라이버를 설정할 때 Bollean 대신 문자열을 사용했고 UISwitch의 인스턴스는 BOOL 프로퍼티로
	// 설정하기 때문에 문자열을 Boolean으로 변환할 필요가 있다.
	engineSwitch.on = ([[defaults objectForKey:kWarpDriveKey] isEqualToString:@"Engaged"]) ? YES : NO;
	warpFactorSlider.value = [defaults floatForKey:kWarpFactorKey];
	
	[super viewDidLoad];
}

// 메인 뷰로 다시 돌아오기 전에 사용자 설정을 제대로 반영할 수 있게 하려고 viewWillDisappear:메서드도 오버라이드 했다.
// 왜냐하면 메인 뷰의 viewWillAppear:메서드가 호출되기 전에 컨트롤러의 viewDidDisappear:메서드가 
// 실행되기 때문에 바뀐 설정값을 가져와서 제대로 된 새 값을 뷰에 반영한다.
-(void)viewWillDisappear:(BOOL)animated
{
	NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
	NSString *prefValue = (engineSwitch.on) ? @"Engaged" : @"Disabled";
	[defaults setObject:prefValue forKey:kWarpDriveKey];
	[defaults setFloat:warpFactorSlider.value forKey:kWarpFactorKey];
	[super viewWillDisappear:animated];
}

....

- (void)viewDidUnload {
	self.engineSwitch = nil;
	self.warpFactorSlider = nil;
	[super viewDidUnload];
}

- (void)dealloc {
	[engineSwitch release];
	[warpFactorSlider release];
    [super dealloc];
}

:

[iPhone] [동영상]iPhone SDK Programming : A Beginner's Guide - James Brannan

iPhone/추천사이트 2010. 8. 10. 10:14

iPhone SDK Programming : A Beginner's Guide 

James Brannan이 지은 iPhone SDK Programming : A Beginner's Guide에 대한 동영상 강자가 있습니다.
아직 수입서 이기 때문에 모두 영문으로만 되어 있다는..ㅋㅋ

출처 : http://www.jamesabrannan.com/iphonesdk_tutorials.html

목차

  • Chapter One. The iPhone Software Development Kit (SDK)
  • Chapter Two. A C Refresher
  • Chapter Three. Just Enough Objective-C -- Part One
  • Chapter Four. Just Enough Objective-C -- Part Two
  • Chapter Five. Deploying to an iPhone, Debugging, and Testing
  • Chapter Six. UIApplication and UIApplicationDelegate
  • Chapter Seven. UIView and UIViewController
  • Chapter Eight. UITabBar and UITabBarController
  • Chapter Nine. UINavigationBar and UINavigationController
  • Chapter Ten. Tables Using UITableView and UITableViewController
  • Chapter Eleven. Activity Progress and Alerting Users
  • Chapter Twelve. Controls -- Part One
  • Chapter Thirteen. Controls Two -- Using Pickers and Using the Camera
  • Chapter Fourteen. Application Settings
  • Chapter Fifteen. Property Lists and Archiving
  • Chapter Sixteen. Data Persistence Using SQLite
  • Chapter Seventeen. Core Data
  • Chapter Eighteen. Multimedia
  • --------------------------------------------------------------------------------------

    Chapter One.

    Videos

    Xcode projects

    --------------------------------------------------------------------------------------

    Chapter Two.

    Videos

    Xcode projects

    --------------------------------------------------------------------------------------

    Chapter Three.

    Videos

    Xcode projects

    --------------------------------------------------------------------------------------

    Chapter Four.

    Videos

    Xcode projects

    --------------------------------------------------------------------------------------

    Chapter Five.

    Videos

    Xcode projects

    --------------------------------------------------------------------------------------

    Chapter Six.

    Videos

    Xcode projects

    --------------------------------------------------------------------------------------

    Chapter Seven.

    Videos

    Xcode projects

    --------------------------------------------------------------------------------------

    Chapter Eight.

    Videos

    Xcode projects

    --------------------------------------------------------------------------------------

    Chapter Nine.

    Videos

    Xcode projects

    --------------------------------------------------------------------------------------

    Chapter Ten.

    Videos

    Xcode projects

    --------------------------------------------------------------------------------------

    Chapter Eleven.

    Videos

    Xcode projects

    --------------------------------------------------------------------------------------

    Chapter Twelve.

    Videos

    Xcode projects

    --------------------------------------------------------------------------------------

    Chapter Thirteen.

    Videos

    Xcode projects

    --------------------------------------------------------------------------------------

    Chapter Fourteen.

    Videos

    Xcode projects

    --------------------------------------------------------------------------------------

    Chapter Fifteen.

    Videos

    Xcode projects

    --------------------------------------------------------------------------------------

    Chapter Sixteen.

    Videos

    Xcode projects

    --------------------------------------------------------------------------------------

    Chapter Seventeen.

    Videos

    Xcode projects

    --------------------------------------------------------------------------------------

    Chapter Eighteen.

    Videos

    XCODE PROJECTS

    'iPhone > 추천사이트' 카테고리의 다른 글

    [iPhone] - 소스 모음 추천사이트 (appsamuck)  (0) 2010.08.12
    :