[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

: