[iPhone] - 시작하세요! 아이폰 3 프로그래밍 - Part 9. 내비게이션 컨트롤러와 테이블 뷰

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

* 네비게이션 컨트롤러
- 계층구조 애플리케이션을 만드는 데 사용할 가장 주된 도구는 UINavigationController이다.
- UINavigationController는 계층 구조를 다루기 적합한 스택(Stack)을 사용해서 구현되었다.

* 스택을 사용하면 좋은 점
- 스택은 새로 추가된 자료는 맨 마지막에 추가되고, 읽을 때는 가장 나중에 추가된 자료를 꺼내오는 후입선출(Last In, First Out:LIFO)원칙에 근간해서 작동하는 흔히 통용되는 자료구조. 다시 말해서.. 입력되는 데이터는 무조건 뒤쪽에 들어가고 꺼내올때도 뒤에서 부터 꺼내오는 방식.
- 객체를 스택에 추가하는 행위를 푸시(Push). 즉 객체를 스택에 밀어넣게되는 행위
- 객체를 스택에서 제거하는 행위를 팝(Pop). 즉 객체를 스택에서 꺼내오는 과정.

* 컨트롤러들의 스택
- 네비케이션 컨트롤러는 뷰 컨트롤러들을 스택에 담아두고 있다.
- 네비게이션 컨트롤러를 설계 할때는 먼저 사용자가 맨 처음 보게 되는 뷰를 지정해야 한다. 이 뷰는 계층의 가장 하단에 위치하고, 이 뷰를 담당하는 컨트롤러를 최상위 컨트롤러라고 부른다.
- 최상위 컨트롤러는 네비게이션 컨트롤러가 자신의 스택에 맨 처음으로 푸시하는 뷰 컨트롤러다.

* 여섯 부분으로 구성된 계층 구조 애플리케이션, Nav
- 상세보기 버튼은 단순한 아이콘이 아닌 사용자가 누를 수 있는 일종의 독립적인 컨트롤이다. 따라서 주어진 행에 대해, 두 가지 상반된 옵션을 지정할 수 있다. 한 액션은 사용자가 행을 선택할 때 시작된다. 다른 액션은 사용자가 더보기(disclosure)버튼을 누를 때 시작된다.
- 행을 눌러 완전히 다른 뷰가 나타난다면, 그 뷰는 행에 대한 상세 뷰라고 할 수 없다. 이럴때는 회색 화살표의 더 보기(dislosure indicator)를 행에 표시하면된다.

* Nav 애플리케이션의 뼈대 구성하기
- Xcode에서 Window-based Application을 선택하되 Use Core Data for storage는 사용하지 않는다. 프로젝트 이름은 Nav로 설정
- 모든 네비게이션 컨트롤러는 각각 고유한 최상위 컨트롤러를 가져야 한다.

* 최상위 레벨의 뷰 컨트롤러 생성하기
- UITableViewController의 하위 클래스를 만들어 사용하면 nib파일 없이도 테이블 뷰를 생성하는 것이 가능하다.
- Class 폴더에 새로운 파일을 취가한다. Cocoa Touch Class를 선택한 뒤 Objective-C class를 선택하고 Subclass of에서는 NSObject를 선택한다. 파일이름은 FirstLevelViewController.m으로 변경하고 헤더 파일도 같이 생성한다.

#### FirstLevelViewController.h ####
#import 

@interface FirstLevelViewController:UITableViewController
{

}
@end


* 네비게이션 컨트롤러 설정하기
- 애플리케이션의 최상위 계층에는 최상위 컨트롤러 이름의 윈도우에 추가되는 뷰와 관련된 컨트롤러가 위치하게 된다.
- 이번 애플리케이션과 같은 경우에는 다른 모든 뷰로의 전환과 뷰의 게층구조를 담당하는 네비게이션 컨트롤러가 최상위 컨트롤러에 해당된다.
- 애플리케이션의 최상위 컨트롤러는 navController로 사용되고, navController의 최상위 컨트롤러를 FirstLevelController으로 상용할것이다.

#### NavAppDelegate.h #####
#import 

@interface NavAppDelegate : NSObject  
{
    UIWindow *window;
	UINavigationController *navController;
}

@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) IBOutlet UINavigationController *navController;

@end

#### NavAppDelegate.m ####
@synthesize navController;

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

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

- 다음으로 네비게이션 컨트롤러를 만들고, 방금 선언한 navController아웃렛에 연결해야 한다. 그리고 나서 네비게이션 컨트롤러에게 최상위 컨트롤러를 알려줘야 한다.
- 인터페이스 빌더에서 MainWindow.xib를 더블크릭해서 파일을 열어보자.
- 네비게이션 컨트롤러를 라이브러리에서 찾은 후에 nib의 메인 위도우로 드래그 해보자 (view 창이 아닌 xib파일에 추가)
- Nav App Delgate 아이콘을 컨트롤-드래그해서, 새로 추가한 Navigation Controller로 끌어다 놓고 navController 아웃렛을 선택
- 이베 네비게이션 컨트롤러에게 최상위 컨트롤러의 위치를 알려줘야 한다. View Mode를 리스트 모드로 변경하여 Navigation Controller의 좌측에 작은 삼각형 모양의 더보기표시를 클릭하고 View Controller(Navigation Item)를 클릭한 후 아이덴티티 인스펙터 4를 실행한다. 클래스를 FirstLevelViewController로 변경한다.

- 각 행마다 아이콘을 표시하고 싶기 때문에 UIImage속성을 생성하는 모든 하위컨트롤러에 푸가해야 한다.

모든 컨트롤러는 UITableViewContoller가 아닌 SecondLevelViewController를 상속받아야 한다.
여기서는 SecondLevelViewController를 상속하고 있기 때문에 모든 클래스들은 각각의 행의 이미지를 저장할 수 있는 속성을 가지게 된다.
#### SecondLevelViewController.h ####
#import 

@interface SecondLevelViewController:UITableViewController
{
	UIImage *rowImage;
}

@property (nonatomic, retain) UIImage *rowImage;

@end

#### SecondLevelViewController.m ####
#import "SecondLevelViewController.h"

@implementation SecondLevelViewController
@synthesize rowImage;

@end

#### FirstLevelViewController.h ####
#import 

@interface FirstLevelViewController:UITableViewController
{
	// 해당 배열은 뷰 컨트롤러들의 인스턴스들을 가지게 된다. 이제 이것을 테이블의 데이터를 채워 넣는데 사용할것이다.
	NSArray *controllers;
}

@property (nonatomic, retain) NSArray *controllers;

@end

#### FirstLevelViewController.m ####
#import "FirstLevelViewController.h"
#import "SecondLevelViewController.h"

@implementation FirstLevelViewController

@synthesize controllers;

-(void)viewDidLoad
{
	// 네비게이션 컨트롤러는 네비게이션 바의 제목(title)부분에 무엇을 표시 할지 현재 활성화 상태에 있는 컨트롤러에게 물어보게 된다.
	self.title = @"First Level";
	
	// 나중에 테이블에 행을 추가할 준비가 되면 뷰 컨트롤러를 이 배열에 추가 할것이다.
	NSMutableArray *array = [[NSMutableArray alloc] init];
	self.controllers = array;
	
	[array release];
	[super viewDidLoad];
}

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

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

#pragma mark -
#pragma mark Table Data Source Methods
// 배열에서 개수를 리턴한다. 섹션당 몇개에 행이 있는지 알기 위해서.
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
	return [self.controllers count];
}

// 재사용 가능한 셀을 언거나, 그런 셀이 없으면 새 셀을 만드는 역할
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
	static NSString *FirstLevelCell = @"FirstLevelCell";
	
	UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:FirstLevelCell];
	
	if(cell == nil)
		cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:FirstLevelCell] autorelease];
	
	// Configure the cell
	NSUInteger row = [indexPath row];
	SecondLevelViewController *controller = [controllers objectAtIndex:row];
	
	// 선택된 뷰의 제목을 설정한다.
	cell.textLabel.text = controller.title;
	// 해당하는 이미지를 추가 한다.
	cell.imageView.image = controller.rowImage;
	// 더보기 버튼을 추가 시킨다.
	cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
	return cell;
}

#pragma mark -
#pragma mark Table View Delegate Methods
// 사용자가 행을 누르면 호출되는 메서드
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
	NSUInteger row = [indexPath row];
	SecondLevelViewController *nextController = [self.controllers objectAtIndex:row];
	
	// 네비게이션 컨트롤러의 스택에 푸시하기 위해 애플리케이션의 네비게이션 컨트롤러를 가리키는 navigationContoller프로퍼티를 사용
	[self.navigationController pushViewController:nextController animated:YES];
	
}


@end


* 첫번째 하위컨트롤러 : 더보기 버튼 뷰
-  뷰 컨트롤러는 지연 로딩 방식을 사용한다. 컨트롤러를 생성하더라도, 실제로 화면에 표시 할 일이 있을 때까지는 nib파일을 로딩하지 않는 것이다. 컨트롤러를 네비게이션 컨트롤러의 스택에 푸시하고 나면, 그때는 레이블을 설정 할 수 있는지 여부를 확인할 수 없다.
- viewDidLoad 메서드의 문제는 처음 컨트롤러의 뷰가 로딩될 때만 호출된다. 그래서 사용자가 DisclosureDetailController 뷰가 맨 처음 업데이트되는 시점에서만 업데이트될 것이다. 두번째 픽사 영화를 선택하더라도 여전히 첫 번째 픽사 영화에 대한 상세 메시지를 가진다. 그래서 여기서는 viewWillAppear 메서드를 사용한다.
- viewWillAppear: 메서드를 사용하면 뷰가 그려질 때마다 매번 메서드가 호출된다.

### DisclosureDetailController.h ###
#import 

@interface DisclosureDetailController:UIViewController
{
	UILabel *label;
	NSString *message;
}

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

@end

### DisclosureDetailController.m ###
#import "DisclosureDetailController.h"


@implementation DisclosureDetailController

@synthesize label;
@synthesize message;

-(void)viewWillAppear:(BOOL)animated
{
	label.text = message;
	[super viewWillAppear:animated];
}

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

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

@end

- nib 파일 생성.
  1.Resources폴더에서 새파일을 추가한다. 좌측 창에서 User Interface를 선택하고, View XIB를 선택하고 파일명은 DisclosureDetail.xib로 설정
 2. 인터페이스 빌더에서 File's Owner를 클릭하고 아이텐티티 인스펙터 4에서 클래스를 DisclosureDetailController로 변경하고 File's Owner 아이콘을 컨트롤 키를 누른 상태에서 view아이콘으로 드래그해 놓자. 그리고 뷰 아웃렛을 선택하자 class를 변경하였기때문에 연결이 끊어져있는 상태이다.
 3. Label을 하나 추가하고 label 아웃렛과 연결한다.

### DisclosureButtonController.h ####
#import 
#import "SecondLevelViewController.h"
@class DisclosureDetailController;

@interface DisclosureButtonController:SecondLevelViewController
{
	NSArray *list;
	DisclosureDetailController *childController;
}

@property (nonatomic, retain) NSArray *list;

@end

### DisclosureButtonController.m ####
#import "DisclosureButtonController.h"
#import "NavAppDelegate.h"
#import "DisclosureDetailController.h"

@implementation DisclosureButtonController
@synthesize list;

-(void)viewDidLoad
{
	NSArray *array = [[NSArray alloc] initWithObjects:@"Toy Story",
					  @"A Bug's Life", @"Toy Story 2", @"Monsters, Inc.",
					  @"Finding Nemo", @"The Incredibles", @"Cars",
					  @"Retatouille", @"WALL-E", @"Up", @"Toy Story 3",
					  @"Cars 2", @"The Bear and the Bow", @"Newt", nil];
	
	self.list = array;
	[array release];
	[super viewDidLoad];
	
}

-(void)viewDidUnload
{
	self.list = nil;
	[childController release];
	childController = nil;
}

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

#pragma mark -
#pragma mark Table Data Source Methods
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
	return [list count];
}

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
	static NSString *DisclosureButtonCellIdentifier = @"DisclosureButtonCellIdentifier";
	
	UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:DisclosureButtonCellIdentifier];
	
	if(cell == nil)
		cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:DisclosureButtonCellIdentifier] autorelease];
	
	NSUInteger row = [indexPath row];
	NSString *rowString = [list objectAtIndex:row];
	cell.textLabel.text = rowString;
	cell.accessoryType = UITableViewCellAccessoryDetailDisclosureButton;
	[rowString release];
	return cell;
	
}

#pragma mark -
#pragma mark Table Delegate Methods
// 행이 선택되었을때 반응 하는 메서드 
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
	UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Hey, do you see the disclosure Button?"
													message:@"If you're trying to drill down, touch the instead"
												   delegate:nil
										  cancelButtonTitle:@"Won't happen again"
										  otherButtonTitles:nil];
	
	[alert show];
	[alert release];	
}

// 사용자가 상세보기 버튼을 클릭하면 호출되는 델리게이트 메서드
-(void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath
{
	//childController가 nil일 경우 메모리에 할당하고 초기화 한다.
	if(childController == nil)
		childController = [[DisclosureDetailController alloc] initWithNibName:@"DisclosureDetail" bundle:nil];
	
	// 스택에 밀어넣기 전에 화면상에 출력될 문자열을 할당하는 작업
	childController.title = @"Disclosure Button Pressed";
	NSUInteger row = [indexPath row];
	
	// 선택된 버튼을 확인..
	NSString *selectedMovie = [list objectAtIndex:row];
	NSString *detailMessage = [[NSString alloc] initWithFormat:@"You pressed the disclosure button for %@.", selectedMovie];
	
	childController.message = detailMessage;
	childController.title = selectedMovie;
	
	[detailMessage release];
	
	// 네비게이션 스택에 세부 내용을 보여줄 뷰의 컨트롤러를 밀어 넣는다.
	[self.navigationController pushViewController:childController animated:YES];
	
}

@end




* 두 번쨰 하위컨트롤러 : 체크리스트
- 테이블 뷰를 이용. 액세서리 아이콘을 사용해서, 사용자가 목록에서 단 하나의 항목을 선택할 수 있다.
- SecondLevelViewController의 하위클래스로 만들 필요가 있다.
- Classes 폴더에서 Cocoa Touch Classes를 선택하고, Objective-C class와 Subclass of의 NSObject를 선택하자.
- 파일명은 CheckListController이다. 헤더 파일과 같이 생성

### CheckListController.h ###
#import 
#import "SecondLevelViewController.h"

@interface CheckListController:SecondLevelViewController
{
	NSArray *list;
	NSIndexPath *lastIndexPath;
}

@property (nonatomic, retain) NSArray *list;
@property (nonatomic, retain) NSIndexPath *lastIndexPath;

@end

### CheckListController.m ###
#import "CheckListController.h"

@implementation CheckListController
@synthesize list;
@synthesize lastIndexPath;

-(void)viewDidLoad
{
	NSArray *array = [[NSArray alloc] initWithObjects:@"Who Hash",
					  @"Bubba Gump Shrimp Etouffee", @"Who Pudding", @"Secooby Snacks",
					  @"Everlastring Gobstopper", @"Green Eggs and Ham", @"Soylent Green",
					  @"Hard Tack", @"Lembas Bread", @"Roast Beast", @"Blancmange", nil];
	
	self.list = array;
	
	[array release];
	[super viewDidLoad];
}

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

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

#pragma mark -
#pragma mark Table Data Source Methods
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
	return [list count];
}

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
	static NSString *CheckMarkCellIdentifier = @"CheckMarkCellIdentifier";
	
	UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CheckMarkCellIdentifier];
	
	if(cell == nil)
		cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CheckMarkCellIdentifier] autorelease];
	
	// 현재 선택된 셀에서 행에 대한 정보를 추출한다.
	NSUInteger row = [indexPath row];
	// 기존에 선택된 셀에서 행에 대한 정보를 추출한다.
	NSUInteger oldRow = [lastIndexPath row];	
	// 배열에서 값을 갖온 후에 셀의 제목을 할당한다.
	cell.textLabel.text = [list objectAtIndex:row];
	// 액세서리 아이콘을 사용해, 체크 표시를 보여주거나 아예 아무것도 표시하지 않는다.
	// 현재 선택한 앵이, 테이블의 해당 셀이 가리키는 행과 동일하다면, 행이 액세서리 아이콘을 설정해서
	// 체크 표시를 설정하고, 그렇지 않다면 아무것도 설정하지 않는다.
	// lastIndexPath가 nil이 아닌지 여부도 반드시 확인해야 한다.
	// 아무것도 선택되지 않았는되도 row 메서드를 호출하면 nil의 값이 0으로 리턴될수도 있기 때문이다.
	cell.accessoryType = (row == oldRow && lastIndexPath != nil) ? UITableViewCellAccessoryCheckmark : UITableViewCellAccessoryNone;

	return cell;
}

#pragma mark -
#pragma mark Table Delegate Methods
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
	int newRow = [indexPath row];
	int oldRow = (lastIndexPath != nil) ? [lastIndexPath row] : -1;
	
	// 이전행과 새항이 다르다면
	if(newRow != oldRow)
	{
		// 현재 선택된 셀을 가져와서, 액세서리 아이콘으로 체크 표시 정보를 할당한다.
		UITableViewCell *newCell = [tableView cellForRowAtIndexPath:indexPath];
		newCell.accessoryType = UITableViewCellAccessoryCheckmark;
		
		// 이전에 선택되었던 셀을 가져와서, 액세서리 아이콘이 아무것도 설정되지 않은 상태로 설정한다.
		UITableViewCell *oldCell = [tableView cellForRowAtIndexPath:lastIndexPath];
		oldCell.accessoryType = UITableViewCellAccessoryNone;
		
		// 방금 선택했던 인덱스 경로 정보를 lastIndexPath에 저정한다.
		lastIndexPath = indexPath;
	}
	
	// 선택한 행이 계속 하이라이트 상태로 표시할 필요가 없기 때문에 선택을 해제한다.
	[tableView deselectRowAtIndexPath:indexPath animated:YES];
	
}

@end

### FistLevelViewController.m 에 아래 내용 추가 ###
#import "CheckListController.h"

-(void)viewDidLoad
{
......
	CheckListController *checkListController = [[CheckListController alloc] initWithStyle:UITableViewStylePlain];
	checkListController.title = @"Check One";
	checkListController.rowImage = [UIImage imageNamed:@"checkmarkControllerIcon.png"];
	[array addObject:checkListController];
	
	[checkListController release];
.....
}




* 세 번째 하위컨트롤러 : 테이블 행 위에 컨트롤 추가
- 테이블 뷰 셀에 하위뷰를 추가해서 모양을 바꾸는 방법
- Classes 폴더에 새로운 파일을 하나 추가 한다. Cocoa Touch Classes를 선택하고 나서 Objective-C와 Subclass of의 NSObject를 선택한다. 파일명은 RowControlsController.m을 입력한다. 헤더 파일까지 같이 생성한다.

### RowControlsController.h ###
#import 
#import "SecondLevelViewController.h"

@interface RowControlsController:SecondLevelViewController
{
	NSArray *list;
}

@property (nonatomic, retain) NSArray *list;
-(IBAction)buttonTapped:(id)sender;
@end

### RowControlsController.m ###
#import "RowControlsController.h"

@implementation RowControlsController
@synthesize list;

-(IBAction)buttonTapped:(id)sender
{
	// 매번 sender에 대한 형변환을 하지 않기 위해서
	UIButton *senderButton = (UIButton *)sender;
	// 버튼의 상위뷰인 테이블 뷰 셀의 인스턴스를 만든다.
	// 사용자가 누른 행과 행의 제목을 얻는다.
	UITableViewCell *buttonCell = (UITableViewCell *)[senderButton superview];
	NSUInteger buttonRow = [[self.tableView indexPathForCell:buttonCell] row];	
	NSString *buttonTitle = [list objectAtIndex:buttonRow];
	
	UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"You tapped the button"
													message:[NSString stringWithFormat:@"You tapped the button for %@", buttonTitle]
												   delegate:nil
										  cancelButtonTitle:@"OK"
										  otherButtonTitles:nil];
	[alert show];
	[alert release];	
}

-(void)viewDidLoad
{
	NSArray *array = [[NSArray alloc] initWithObjects:@"R2-R2",
					  @"C3PO", @"Tik-Tok",@"Robby", @"Rosie", @"Uniblab",
					  @"Bender", @"Marvin", @"Lt. Commander Data",
					  @"Evil Brother Lore", @"Optimus Prime", @"Tobor", @"HAL",
					  @"Orgasmatron", nil];
	
	self.list = array;
	[array release];
	[super viewDidLoad];
}

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

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

#pragma mark -
#pragma mark Table Data Source Methods
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
	return [list count];
}

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
	static NSString *ControlRowIdentifier = @"ControlRowIdentifier";
	
	UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ControlRowIdentifier];
	
	if(cell == nil)
	{
		cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:ControlRowIdentifier] autorelease];
		
		UIImage *buttonUpImage = [UIImage imageNamed:@"button_up.png"];
		UIImage *buttonDownImage = [UIImage imageNamed:@"button_down.png"];
		
		// UIButton의 buttonType 플로퍼티는 읽기 전용으로 선언되어 있으므로 buttonWithType:라는 팩토리
		// 메서드를 사용하여 버튼을 생성. 
		// 사용자화된 버튼을 만들때는 UIButtonTypeCustom을 사용하는데 UIButtonTypeCustom 타입의 버튼은 
		// alloc이나 init를 사용하여 생성할수 없다.
		UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
		button.frame = CGRectMake(0.0f, 0.0f, buttonUpImage.size.width, buttonUpImage.size.height);
		[button setBackgroundImage:buttonUpImage forState:UIControlStateNormal];
		[button setBackgroundImage:buttonDownImage forState:UIControlStateHighlighted];
		[button setTitle:@"Tap" forState:UIControlStateNormal];
		
		// 버튼에 대한 Touch Up Inside 이벤트에 대한 액션 메서들 설정하고 셀의 액세서리 뷰를 할당한다.
		[button addTarget:self action:@selector(buttonTapped:) forControlEvents:UIControlEventTouchUpInside];
		cell.accessoryView = button;
	}
	
	NSUInteger row = [indexPath row];
	NSString *rowTitle = [list objectAtIndex:row];
	
	cell.textLabel.text = rowTitle;
	
	return cell;	
}

#pragma mark -
#pragma mark Table Delegate Methods
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
	NSUInteger row = [indexPath row];
	NSString *rowTitle = [list objectAtIndex:row];
	
	UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"You tapped the row."
													message:[NSString stringWithFormat:@"You tapped %@.", rowTitle]
												   delegate:nil
										  cancelButtonTitle:@"OK"
										  otherButtonTitles:nil];
	[alert show];
	[alert release];
	[tableView deselectRowAtIndexPath:indexPath animated:YES];
	
}


@end

### FirstLevelViewController.m ####
#import "RowControlsController.h"

-(void)viewDidLoad
{
.....
	RowControlsController *rowControlsController = [[RowControlsController alloc] initWithStyle:UITableViewStylePlain];
	rowControlsController.title = @"Row Controls";
	rowControlsController.rowImage = [UIImage imageNamed:@"rowControlsIcon.png"];
	[array addObject:rowControlsController];
	
	[rowControlsController release];
.....
}




* 네번째 하위컨트롤러 : 이동 가능한 행
- 또 다른 2차 레벨 뷰 컨트롤러를 만들고 애플리케이션에 추가해볼 것이다.

* 편집모드
- 테이블의 특정 지점에 행을 이동하고 삭제하고, 삽입하는 일련의 동작은 사실구현하기 쉬운 편이다.
- 세가지 동작 모두 테이블 뷰에서 setEditing:animated:를 사용해서, 편집 모드라고 부르는 상태로 전환하면 구현할 수 있다. 이 메서드는 두 개의 불린(Boolean) 값을 인자로 취한다.
- 첫번쨰 불린 값은 편집 모드를 켤 것인지, 끌것인지의 여부
- 두번째 불린 값은 테이블 모양이 바뀔때 애니메이션 효과를 사용할지 여부를 나타낸다.

* 새로운 2차 레벨 컨트롤러 생각하기
- Classes폴더에 새로운 파일을 추가한다. Cocoa Touch Classes를 선택하고, UIViewController subclass아이콘을 선택하자. 파일명은 MoveMeController.m

### MoveMeController.h ###
#import 
#import "SecondLevelViewController.h"

@interface MoveMeController:SecondLevelViewController
{
	NSMutableArray *list;
}

@property (nonatomic, retain) NSMutableArray *list;
-(IBAction)toggleMove;
@end

### MoveMeController.m ###
#import "MoveMeController.h"

@implementation MoveMeController
@synthesize list;

-(IBAction)toggleMove
{
	[self.tableView setEditing:!self.tableView.editing animated:YES];
	
	if(self.tableView.editing)
		[self.navigationItem.rightBarButtonItem setTitle:@"Done"];
	else 
		[self.navigationItem.rightBarButtonItem setTitle:@"Move"];
	
}

-(void)viewDidLoad
{
	if(list == nil)
	{
		NSMutableArray *array = [[NSMutableArray alloc] initWithObjects:
								 @"Eeny", @"Meeny", @"Miney", @"Moe", @"Catch", @"A",
								 @"Tiger", @"By", @"The", @"Toe", nil];
		self.list = array;
		[array release];		
	}
	
	// 네비게이션 바에 위치할 버튼 바 아이템을 생성한다.
	// 제목은 Move
	// 간단한 버튼임을 나타내는 UIBarButtonItemStyleBordered
	// Target과 action이라는 두 갱의 인자는 버튼이 눌릴때 무엇을 해야 할지를 알려주는 역할
	// 버튼에게 언제든지 버튼이 눌리면 toggleMove 메서드를 호출하라고 지정한다.
	UIBarButtonItem *moveButton = [[UIBarButtonItem alloc] initWithTitle:@"Move"
																   style:UIBarButtonItemStyleBordered
																  target:self
																  action:@selector(toggleMove)];
	self.navigationItem.rightBarButtonItem = moveButton;
	[moveButton release];
	[super viewDidLoad];
	
}

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

#pragma mark -
#pragma mark Table Data Source Methods
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
	return [list count];
}

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
	static NSString *MoveMeCellIdentifier = @"MoveMeCellIdentifier";
	
	UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MoveMeCellIdentifier];
	
	if(cell == nil)
	{
		cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:MoveMeCellIdentifier] autorelease];
		
		// 표준 액세서리 아이콘은 셀의 accessoryType 프로퍼티를 사용하여 설정할수 있다.
		// 하지만 재배치가 가능한 컨트롤은 표준 액세서리 아이콘의 범주에 속하지 않는다.
		// 편집 모드상에서만 테이블에 표시되는 컨트롤이기 때문이다.
		// 재배치 가능한 컨트롤을 활성화시키려면 셀에 직접 속성을 설정해야 한다.
		// 이 showReorderControl 속성을 YES로 지정한다고 하더라도 
		// 테이블이 편집 모드로 바뀌기 전까지는 재배치 컨트롤이 실제로 표시되지 않는다.
		cell.showsReorderControl = YES;
	}
	
	NSUInteger row = [indexPath row];
	cell.textLabel.text = [list objectAtIndex:row];
	
	return cell;
}

// 이 메서드는 테이블 뷰에게 특정 행이 삭제가능한 상태인지 또는 특정 지점에 삽입 가능한지를 물어보는 역할을 한다.
// 재배치만을 지원할 것이기때문에 UITableViewCellEditingStyleNone를 리턴해서 행의 삽입이나 삭제를 지원하지 않는다고 알려준다.
-(UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath
{
	return UITableViewCellEditingStyleNone;
}

// 이 메서드는 각행마다 호출되어서 특정열의 이동을 원천적으로 막을 수 있는 기능을 제공한다.
// No를 리턴하면 재배치 컨트롤이 해당 행에 대해서는 나타나지 않고, 사용자는 현재 위치에서 더 이상 행을 움직일 수 없다.
-(BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath
{
	return YES;
}

// 실제로 사용자가 행을 움직일 때 호출되는 메서드 
// 매개변수는 하나는 움직인 행을 가리키고, 다른 하나는 행의 새로운 위치를 가리킨다.
// 데이터 모델을 업데이트해서 실제 행과 데이터상의 행 위치를 동기화 하지 않으면, 나중에 화면을 표시 할때 문제가 있을수 있다.
-(void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath
{
	// 움직여야 하는 행에 대한 정보를 가져온다.
	NSUInteger fromRow = [fromIndexPath row];
	// 행의 새위치에 대한 정보를 가져온다.
	NSUInteger toRow = [toIndexPath row];
	
	// 이동이 일어나려는 객체에 대한 포인터를 가져와서 저장한다.
	// 배열에서 객체를 지울때, 객체가 아예 없어지는 일을 막기 위해서 말이다.
	id object = [[list objectAtIndex:fromRow] retain];
	[list removeObjectAtIndex:fromRow];
	
	// 미리 저장해둔 새로운 위치에 객체를 다시 삽입한다.
	[list insertObject:object atIndex:toRow];
	
	[object release];
}

@end

### FirstLevelViewController.m ###
#import "MoveMeController.h"

-(void)viewDidLoad
{
......
	MoveMeController *moveMeController = [[MoveMeController alloc] initWithStyle:UITableViewStylePlain];
	moveMeController.title = @"Move Me";
	moveMeController.rowImage = [UIImage imageNamed:@"moveMeIcon.png"];
	[array addObject:moveMeController];
	
	[moveMeController release];
......
}




* 다섯 번째 하위컨트롤러 : 삭제 가능한 행
- Classes폴더에 새파일을 추가한다. Cocoa Touch Classes을 선택하고 나서, Objective-C class를 선택하고 Subclass of의 NSObject를 선택한다. 파일 이름은 DeleteMeController.m을 입력한다. 헤더파일도 같이 생성한다.

### DeleteMeController.h ###
#import 
#import "SecondLevelViewController.h"

@interface DeleteMeController:SecondLevelViewController
{
	NSMutableArray *list;
}

@property (nonatomic, retain) NSMutableArray *list;
-(IBAction)toggleEdit:(id)sender;
@end

### DeleteMeController.m ###
#import "DeleteMeController.h"

@implementation DeleteMeController
@synthesize list;

// 현재 편집 모드가 아니면 편집 모드로 전환하거나 편집 모드라면 일반 모드로 전환
-(IBAction)toggleEdit:(id)sender
{
	[self.tableView setEditing:!self.tableView.editing animated:YES];
	
	if(self.tableView.editing)
		[self.navigationItem.rightBarButtonItem setTitle:@"Done"];
	else
		[self.navigationItem.rightBarButtonItem setTitle:@"Delete"];
}

-(void)viewDidLoad
{
	if(list == nil)
	{
		NSString *path = [[NSBundle mainBundle] pathForResource:@"computers" ofType:@"plist"];
		
		NSMutableArray *array = [[NSMutableArray alloc] initWithContentsOfFile:path];
		
		self.list = array;
		[array release];
	}
	
	UIBarButtonItem *editButton = [[UIBarButtonItem alloc] initWithTitle:@"Delete"
																   style:UIBarButtonItemStyleBordered
																  target:self
																  action:@selector(toggleEdit:)];
	self.navigationItem.rightBarButtonItem = editButton;
	[editButton release];
	[super viewDidLoad];
}

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

#pragma mark -
#pragma mark Table Data Source Methods
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
	return [list count];
}

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
	static NSString *DeleteMeCellIdentifier = @"DeleteMeCellIdentifier";
	
	UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:DeleteMeCellIdentifier];
	
	if(cell == nil)
		cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:DeleteMeCellIdentifier] autorelease];
	
	NSUInteger row = [indexPath row];
	cell.textLabel.text = [list objectAtIndex:row];
	return cell;
}

#pragma mark -
#pragma mark Table View Data Source Methods
// 사용자가 편집동작을 수행할때, 즉 삽입 또는 삭제를 수행할때 테이블 뷰가 이 메서드를 호출하게 된다.
// 첫번째 인자는 테이블 뷰에서 어떤 행이 편집 중인지를 나타낸다.
// 두번째 인자인 editingStyle는 방금 어떤 종류의 편집 동작이 수행되었는지를 알려주는 준다.
// 그중 하나는 UITableViewCellEditingStyleNone / UITableViewCellEditingStyleDelete / UITableViewCellEditingStyleInsert
// 이 메서드에는 절대 UITableViewCellEditingStyleNone이 절대로 이 메서드에는 전달되지 않는데, 이 옵션은 해당 행을 편집 할 수 없다는 사실을 나타내는 데 사용되기 때문이다.
// 세번째 인자인 indexPath는 현재 편집 중인 행이 어는 것인지를 알려준다.
-(void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
	// indexPath를 통해 편집 중인 행에 대한 정보를 가져온다.
	NSUInteger row = [indexPath row];
	// 변경 가능한 배열에서 앞서 생성했던 객체를 삭제 한다.
	[self.list removeObjectAtIndex:row];
	
	// 테이블이 행을 삭제 할때 어떤 애니메이션 타입을 사용해서 삭제효과를 나타낼지를 알려주는 용도..
	[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
}

@end

### FirstLevelViewController.m ###
#import "DeleteMeController.h"

-(void)viewDidLoad
{
......
	DeleteMeController *deleteMeController = [[DeleteMeController alloc] initWithStyle:UITableViewStylePlain];
	deleteMeController.title = @"Delete Me";
	deleteMeController.rowImage = [UIImage imageNamed:@"deleteMeIcon.png"];
	[array addObject:deleteMeController];
	
	[deleteMeController release];
......
}




* 여섯 번째 하위컨트롤러 : 편집 가능한 상세 창
- 편집 가능한 상세 뷰를 구현하는 방법

* 데이터 모델 객체 생성하기
- Xcode에서 Classes폴더를 클릭해서 새로운 파일을 만들자. Cocoa Touch Classes를 선택하고 Objective-C class와 Subclass of의 NSObject를 선택한다. 클래스의 이름은 President.m으로 하고 헤더 파일 생성 체크박스도 선택한다.

#### President.h ####
#define kPresidentNumberKey @"President"
#define kPresidentNameKey @"Name"
#define kPresidentFromKey @"FromYear"
#define kPresidentToKey @"ToYear"
#define kPresidentPartyKey @"Party"

#import 

@interface President:NSObject  
{
	int number;
	NSString *name;
	NSString *fromYear;
	NSString *toYear;
	NSString *party;
}

@property int number;
@property (nonatomic, retain) NSString *name;
@property (nonatomic, retain) NSString *fromYear;
@property (nonatomic, retain) NSString *toYear;
@property (nonatomic, retain) NSString *party;

@end

#### President.m ####
#import "President.h"

@implementation President

@synthesize number;
@synthesize name;
@synthesize fromYear;
@synthesize toYear;
@synthesize party;

-(void)dealloc
{
	[name release];
	[fromYear release];
	[toYear release];
	[party release];
	[super dealloc];
}

// encodeWithCoder: / initWithCoder: 메서드는 하드 디스크에 데이터를 저장할때 사용하는 NSCoding프로토콜의 일부분
#pragma mark -
#pragma mark NSCoding
// encodeWithCoder: 메서드는 저장할 객체를 인코딩하는 역할
-(void)encodeWithCoder:(NSCoder *)coder
{
	[coder encodeInt:self.number forKey:kPresidentNumberKey];
	[coder encodeObject:self.name forKey:kPresidentNameKey];
	[coder encodeObject:self.fromYear forKey:kPresidentFromKey];
	[coder encodeObject:self.toYear forKey:kPresidentToKey];
	[coder encodeObject:self.party forKey:kPresidentPartyKey];
}

// initWithCoder: 메서드는 저장소로부터 새로운 객체를 생성하는 사용된다.
-(id)initWithCoder:(NSCoder *)coder
{
	if(self = [super init])
	{
		self.number = [coder decodeIntForKey:kPresidentNumberKey];
		self.name = [coder decodeObjectForKey:kPresidentNameKey];
		self.fromYear = [coder decodeObjectForKey:kPresidentFromKey];
		self.toYear = [coder decodeObjectForKey:kPresidentToKey];
		self.party = [coder decodeObjectForKey:kPresidentPartyKey];
	}
	
	return self;
}


@end

* 컨트롤러 생성하기
- 두개에 새로운 컨트롤러를 만든것이다. 하나는 편집할 목록을 보여주는데 사용, 하나는 목록에서 선택된 아이템의 상세항목을 편집하는 뷰로 사용
- Xcode에서 Classes폴더를 선택하고 새로운 파일을 추가한다. Cocoa Touch Classes를 선택하고 Objective-C class와 Subclass of의 NSObject를 선택한다. 이름은 PresidentsViewController.m을 입력하고 헤더 파일도 같이 생성한다.
- 위와 같은 방법으로 PresidentDetailController.m파일도 생성. 역시 헤더 파일도 같이 생성한다.

#### PresidentsViewController.h ####
#import 
#import "SecondLevelViewController.h"

@interface PresidentsViewController:SecondLevelViewController 
{
	NSMutableArray *list;
}

@property (nonatomic, retain) NSMutableArray *list;

@end

#### PresidentsViewController.m ####
#import "PresidentsViewController.h"
#import "PresidentDetailController.h"
#import "President.h"

@implementation PresidentsViewController
@synthesize list;

-(void)viewDidLoad
{
	NSString *path = [[NSBundle mainBundle] pathForResource:@"Presidents" ofType:@"plist"];
	
	// 인코딩된 저장소를 임시로 담고 있는 데이터 객체
	NSData *data;
	// 저장소에서 실제로 객체를 복원하는데 사용되는 객체
	NSKeyedUnarchiver *unarchiver;
	
	// 속성 리스트를 data로 로딩
	data = [[NSData alloc] initWithContentsOfFile:path];
	// unarchiver를 초기화하는데 data를 사용
	unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
	
	NSMutableArray *array = [unarchiver decodeObjectForKey:@"Presidents"];
	self.list = array;
	[unarchiver finishDecoding];
	[unarchiver release];
	[data release];
	
	[super viewDidLoad];
	
}

-(void)viewWillAppear:(BOOL)animated
{
	// 사용자가 상세보기 뷰에서 무언가를 변경한다면, 상위뷰가 변경된 데이터를 보여 주어야 하기 때문에
	// 데이터를 다시 로딩한다.
	[self.tableView reloadData];
	[super viewWillAppear:animated];
}

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

#pragma mark -
#pragma mark Table Data Source Methods
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
	return [list count];
}

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
	static NSString *PresidentListCellIdentifier = @"PresidentListCellIdentifier";
	
	UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:PresidentListCellIdentifier];
	
	if(cell == nil)
		cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:PresidentListCellIdentifier] autorelease];
	
	NSUInteger row = [indexPath row];
	President *thePres = [self.list objectAtIndex:row];
	cell.textLabel.text = thePres.name;
	cell.detailTextLabel.text = [NSString stringWithFormat:@"%@ - %@", thePres.fromYear, thePres.toYear];
	
	return cell;
	
}
	
#pragma mark -
#pragma mark Table Data Delegate Methods
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
	NSUInteger row = [indexPath row];
	President *prez = [self.list objectAtIndex:row];
	
	// 선택된 셀에 대한 상세 뷰를 생성한다.
	PresidentDetailController *childController = [[PresidentDetailController alloc] initWithStyle:UITableViewStyleGrouped];
	
	childController.title = prez.name;
	childController.president = prez;
	
	[self.navigationController pushViewController:childController animated:YES];
	[childController release];
}


@end

#### PresidentDetailController.h ####
#import 

@class President;

#define kNumberOfEditableRows 4
#define kNameRowIndex 0
#define kFromYearRowIndex 1
#define kToYearRowIndex 2
#define kPartyIndex 3

#define kLabelTag 4096

// UITextFieldDelegate 프로토콜을 따르면 사용자가 텍스트 필드의 값을 변경했는지 여부를 알수 있다.
@interface PresidentDetailController:UITableViewController 
{
	// 이객체는 실제로 이 뷰를 편집할 때 사용되는 객체로, 현재 선택된 행의 부모 컨트롤러의 
	//tableView:didSelectRowAtIndexPath: 메서드에서 설정된다.
	President *president;
	
	// Index 상수 kNameRowIndex / kFromYearRowIndex / kToYearRowIndex / kPartyIndex에 대응하는
	// 레이블의 목록을 가지고 있는 배열
	NSArray *fieldLabels;
	
	// 사용자가 텍스트 필드에서 변경한 값을 임시로 가지고 있게 하는 객체
	// 사용자가 cancel버튼을 선택할수도 있기 때문에 임시로 저장 
	// 변경사항을 통보받으면 키를 나타내는 속성의 이름을 사용해서 딕셔너리에 새로운 값을 저장한다.
	// UITextFieldDelegate가 이 사실을 통보해준다.
	NSMutableDictionary *tempValues;
	
	// 사용자가 텍스트 필드 중 하나를 클릭할때 textFieldBeingEdited는 해당 텍스트 필드를 가리키도록 지정
	UITextField *textFieldBeingEdited;
}

@property (nonatomic, retain) President *president;
@property (nonatomic, retain) NSArray *fieldLabels;
@property (nonatomic, retain) NSMutableDictionary *tempValues;
@property (nonatomic, retain) UITextField *textFieldBeingEdited;

-(IBAction)cancel:(id)sender;
-(IBAction)save:(id)sender;
-(IBAction)textFieldDone:(id)sender;

@end

#### PresidentDetailController.m ####
#import "PresidentDetailController.h"
#import "President.h"

@implementation PresidentDetailController
@synthesize president;
@synthesize fieldLabels;
@synthesize tempValues;
@synthesize textFieldBeingEdited;

// 사용자가 cancel버튼을 누를 때 이 메서드가 호출된다.
-(IBAction)cancel:(id)sender
{
	// 현재 뷰를 스택에서 꺼내오고(pop) 이전 뷰는 스택의 상단으로 한 단계 위치를 이동하게 된다.
	[self.navigationController popViewControllerAnimated:YES];
}

// Save버튼을 누를 때 호출되는 메서드.
// Save버튼을 누르면 사용자가 이미 입력했던 값들이 tempValues 딕셔너리에 저장된다.
-(IBAction)save:(id)sender
{
	// 만약 아직 textField가 편집 상태일 경우 현재 편집중인 textField에서 데이터를 가져와서
	// tempValues에 저장한다.
	// 현재 편집 상태인지는 textFieldBeingEdited가 nil인지 아닌지 확인하면된다.
	// textFieldDidBeginEditing: 메서드에서 현재에 textField값을 저장해준다.
	if(textFieldBeingEdited != nil)
	{
		NSNumber *tagAsNum = [[NSNumber alloc] initWithInt:textFieldBeingEdited.tag];
		[tempValues setObject:textFieldBeingEdited.text forKey:tagAsNum];
		[tagAsNum release];
	}
	
	// 딕셔너리의 모든 키와 값을 조회하기 위해, 행 번호를 키로 활용하는 열거문을 사용
	// NSDictionary에는 int와 같은 기본 데이터형은 저장할 수 없다. 이때문에 int형 대신 행번호를 사용해서
	// NSNumber 객체를 생성해서 사용한다.
	for(NSNumber *key in [tempValues allKeys])
	{
		// intValue메서드를 사용해서 이 키가 가리키는 값을 다시 int형으로 변환..
		switch ([key intValue])
		{
			// 매칭되는 데이터를 다시 president 객체의 지정된 필드에 저장.
			case kNameRowIndex :
				president.name = [tempValues objectForKey:key];
				break;
			case kFromYearRowIndex :
				president.fromYear = [tempValues objectForKey:key];
				break;
			case kToYearRowIndex :
				president.toYear = [tempValues objectForKey:key];
				break;
			case kPartyIndex :
				president.party = [tempValues objectForKey:key];
				break;
			default :
				break;
		}
	}
	
	// 데이터를 저장하고 뷰 계층에서 한 단계 위로 올라간다.
	[self.navigationController popViewControllerAnimated:YES];
	
	//  사용자가 편집을 하였기 때문에 무보 뷰의 데이블에게 데이터를 다시 로딩하라고 알려준다.
	NSArray *allControls = self.navigationController.viewControllers;
	UITableViewController *parent = [allControls lastObject];
	[parent.tableView reloadData];
	
}

// 사용자가 Done 버튼을 클릭시 호출된다.
-(IBAction)textFieldDone:(id)sender
{
	// 키보드를 보이지 않게 처리한다.
	[sender resignFirstResponder];
}

-(void)viewDidLoad
{
	NSArray *array = [[NSArray alloc] initWithObjects:@"Name:", @"From:", @"To:", @"Party:", nil];
	
	self.fieldLabels = array;
	[array release];
	
	// Cancel버튼을 네비게이션 바 좌측에 추가한다. 버튼 클릭시 cancel:메서드가 반응하게 한다.
	UIBarButtonItem *cancelButton = [[UIBarButtonItem alloc] initWithTitle:@"Cancel"
																	 style:UIBarButtonItemStylePlain
																	target:self
																	action:@selector(cancel:)];
	self.navigationItem.leftBarButtonItem = cancelButton;
	[cancelButton release];
	
	// Save버튼을 네비게이션 바 우측에 추가한다. 버튼 클릭시 save:메서드가 반응하게 한다.
	// 또한 style을 UIBarButtonItemStyleDone으로 지정하여 파란색이되게 한다.
	UIBarButtonItem *saveButton = [[UIBarButtonItem alloc] initWithTitle:@"Save"
																   style:UIBarButtonItemStyleDone
																  target:self
																  action:@selector(save:)];
	self.navigationItem.rightBarButtonItem = saveButton;
	[saveButton release];
	
	// 변경가능한 NSDictionary 객체를 하나 생성하여 tempValues에 할당한다.
	// 사용자가 cancel버튼을 누를수도 있기 때문에 곧바로 President 객체를 바로 변경하는것은 
	// 원본 데이터를 되돌리기 쉽지 않기 때문이다.
	NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
	self.tempValues = dict;
	[dict release];
	[super viewDidLoad];
}

-(void)dealloc
{
	[textFieldBeingEdited release];
	[tempValues release];
	[president release];
	[fieldLabels release];
	
	[super dealloc];
}

#pragma mark -
#pragma mark Table Data Source Methods
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
	return kNumberOfEditableRows;
}

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
	static NSString *PresidentCellIdentifier = @"PresidentCellIdentifier";
	
	UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:PresidentCellIdentifier];
	
	if(cell == nil) 
	{
		cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:PresidentCellIdentifier] autorelease];
		
		// 해당 Cell에 Label를 하나 추가 하고 텍스트 정렬을 right로 하고 bold처리를 한다.
		UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(10, 10, 75, 25)];
		label.textAlignment = UITextAlignmentRight;
		label.tag = kLabelTag;
		label.font = [UIFont boldSystemFontOfSize:14];
		[cell.contentView addSubview:label];
		[label release];
		
		// 사용자가 실제로 내용을 입력하게 되는 TextField를 추가한다.
		UITextField *textField = [[UITextField alloc] initWithFrame:CGRectMake(90, 12, 200, 25)];
		// 사용자가 편집을 시작했을때 데이터가 지워지기를 원하지 않기 때문에 clearsOnBeginEditing은 ON으로 설정한다.
		textField.clearsOnBeginEditing = NO;
		// 텍스트 필드의 델리케이트를 self로 설정함으로써 UITextFieldDelegate 프로토콜의 적합한 메서드를
		// 구현하면 텍스트 필드가 해당 이벤트를 알려주도록 한다.
		[textField setDelegate:self];
		// 키보드의 리턴 키에 해당하는 타입도 설정한다.
		// returnKeyType 을 UIReturnKeyDone으로 설정하여 키보드의 우측 하단의 키에 적을 텍스트를 명시한다.
		textField.returnKeyType = UIReturnKeyDone;
		// 텍스트 필드가 Did End on Exit 이벤트를 발생시킬 때 textFieldDone: 메서드가 호출되게 한다.
		[textField addTarget:self action:@selector(textFieldDone:) forControlEvents:UIControlEventEditingDidEndOnExit];
		[cell.contentView addSubview:textField];		
	}
	
	// 이 셀이 어떤 행을 나타낼지를 지정한다.
	NSUInteger row = [indexPath row];
	
	// 셀에 있는 레이블과 텍스트 필드에 대한 참조를 획득한다.
	// label의 경우는 tag를 사용해서 찾으면 쉽제 찾을수 있다.
	UILabel *label = (UILabel *)[cell viewWithTag:kLabelTag];
	UITextField *textField = nil;
	
	// 셀의 contentView의 하위 뷰에 단 하나의 텍스트 필드만 존재한다고 가정하여...
	// 각 하위뷰를 순환하고 해당 뷰가 UITextField라면 textField에 할당한다.
	for(UIView *oneView in cell.contentView.subviews)
	{
		if([oneView isMemberOfClass:[UITextField class]])
			textField = (UITextField *)oneView;
	}
	
	// 이 행이 가르키는 president 객체의 필드값에 적합한 값을 할당.
	label.text = [fieldLabels objectAtIndex:row];
	
	// NSDictionary 객체에서 값을 찾기 위해서 NSUInteger 객체를 NSNumber 형태로 변경한다.
	// 이값을 key로 사용하기 때문에..
	NSNumber *rowAsNum = [[NSNumber alloc] initWithInt:row];
	
	// -(BOOL)containsObject:(id)anObject 는 현재 배열에 anObject오브젝트를 포함하면 YES 아니면 NO
	switch (row) {
		// case 0 : 과 같은 것이다.
		// tempValues에 값이 들어 있지 않다면, 이 필드에 대해서는 아직 변경사항이 없는것으로 간주하고 President에 있는 값을 할당하고
		// 값이 있다면 변경되었다고 판단하고 tempValues에 있는 값으로 할당한다.
		case kNameRowIndex :
			if([[tempValues allKeys] containsObject:rowAsNum])
				textField.text = [tempValues objectForKey:rowAsNum];
			else
				textField.text = president.name;
			break;
		// case 1 : 과 같은 것이다.
		case kFromYearRowIndex :
			if([[tempValues allKeys] containsObject:rowAsNum])
				textField.text = [tempValues objectForKey:rowAsNum];
			else
				textField.text = president.fromYear;
			break;
		// case 2 : 과 같은 것이다.
		case kToYearRowIndex :
			if([[tempValues allKeys] containsObject:rowAsNum])
				textField.text = [tempValues objectForKey:rowAsNum];
			else
				textField.text = president.toYear;
			break;
		// case 3 : 과 같은 것이다.
		case kPartyIndex :
			if([[tempValues allKeys] containsObject:rowAsNum])
				textField.text = [tempValues objectForKey:rowAsNum];
			else
				textField.text = president.party;
			break;
		default :
			break;
			
	}
	
	// 만약 우리가 사용하고 있는 필드가 현재 편집중이라면 우리가 textFieldBeingEdited에 가지고 있는 값은
	// 더 이상 유효하지 않기 때문에 textFieldBeingEdited를 nil로 설정해야 한다. 
	// 텍스트 필드가 해제되거나 재사용되면 텍스트 필드의 델리게이트가 호출되고, tempValues 딕셔너리에 이미 
	// 올바른 값이 들어가 있을것이다.
	if(textFieldBeingEdited == textField)
		textFieldBeingEdited = nil;
	
	// 텍스트 필드의 tag에 어떤 행을 가리키는지를 설정해서, 어떤 필드가 텍스트 필드의 델리게이트 메서드를
	// 호출하는지를 알려 주도록하자.
	textField.tag = row;
	[rowAsNum release];
	return cell;
	
}

#pragma mark -
#pragma mark Table Delegate Methods
// 행을 편집 상태로 만드는것이지 하이라트 상태로 만드는것을 원치 않기 때문에
// 행이 선택되지 전에 호출되는 willSelectRowAtIndexPath: 메서드에서 nil 을 리턴한다.
-(NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
	return nil;
}

#pragma mark Text Field Delegate Methdos
// 델리게이트가 퍼스트 리스폰더가 될 때 호출되는 메서드다. 사용자가 텍스트 필드를 눌러 키보드를 띄우면 알 수 있게 된다.
// 이 메서드에서 현재 편집 중인 필드를 가리키는 포인터를 저장.
-(void)textFieldDidBeginEditing:(UITextField *)textField
{
	self.textFieldBeingEdited = textField;
}

// 텍스트 필드에 대한 편집을 멈추는 경우에 호출된다.
// 사용자가 다른 텍스트 필드를 누르거나 Done버튼을 누를 때등....
-(void)textFieldDidEndEditing:(UITextField *)textField
{
	NSNumber *tagAsNum = [[NSNumber alloc] initWithInt:textField.tag];
	[tempValues setObject:textField.text forKey:tagAsNum];
	[tagAsNum release];
}
@end



* 한 가지가 더 남아 있다..
- 키보드에서 Done버튼 대신  Return 버튼을 가지고 있어서 버튼을 누르면 사용자는 다음 행의 텍스트 필드로 이동하게 하는것..

먼저 tableView:cellForRowAtIndexPath:메서드에서 다음 코드를 주석처리 한다.
#### PresidentDetailController.m ####
//		textField.returnKeyType = UIReturnKeyDone;

그리고 textFieldDone:메서드에서 sender에게 다음 필드가 퍼스트 리스톤더 상태가 되었음을 알려주어야 한다.
#### PresidentDetailController.m ####
// 사용자가 Done 버튼을 클릭시 호출된다.
-(IBAction)textFieldDone:(id)sender
{
	// 셀은 어떤 행을 나타내고 있는지 알지 못한다.
	// 하지만, 테이블 뷰에서는 해당 셀이 어떤 행을 나타내는지를 알고 있다. 즉, 테이블 뷰의 참조를 해결할 수 있는 문제라는 말이다.
	// 이 액션 메서드를 실행하는 텍스트 필드가 테이블 셀 뷰의 콘텐츠 뷰의 하의뷰라는 것은 익히 알고 있다. 
	// 즉, sender의 상의 뷰의 상위뷰를 알아야 한다는 말이다.
	
	// 이 경우에 sender는 편집중인 텍스트 필드이다.
	// sender의 상위뷰는 텍스트 뷰와 레이블의 그룹으로 구성된 콘텐츠 뷰다.
	// sender의 상위뷰의 상위뷰는 이 콘테츠 뷰를 포함 하는 셀이 된다.
	UITableViewCell *cell = (UITableViewCell *)[[sender superview] superview];
	// 셀이 포함하고 있는 테이블 뷰에 전급.
	UITableView *table = (UITableView *)[cell superview];
	// 셀이 어떤 행을 나타내는지를 물어보자. 응답으로 NSIndexPath를 얻을 수 있고, 여기에서 행 정보를 얻어낼 수 있다.
	NSIndexPath *textFieldIndexPath = [table indexPathForCell:cell];
	NSUInteger row = [textFieldIndexPath row];
	
	// row값을 하나 증가시켜 현재 선택되어 있는 행의 다음 행을 가르키도록 세팅
	row++;
	
	// 마지막 행 번호를 넘어간다면 다시 row를 0으로 세팅.
	if(row >= kNumberOfEditableRows)
		row = 0;
	
	// 다음행을 가리키는 새로운 NSIndexPath를 만들어서, 다음 행을 현재 나타나게 되는 셀에 대한 참조를 얻어 인덱스 경로로 사용하자.
	NSInteger newIndex[] = {0, row};
	NSIndexPath *newPath = [[NSIndexPath alloc] initWithIndexes:newIndex length:2];
	UITableViewCell *nextCell = [self.tableView cellForRowAtIndexPath:newPath];
	[newPath release];
	
	// 셀의 콘텐츠 뷰가 가지고 있는 하위뷰를 하나하나씩 순회하도록한다.
	UITextField *nextField = nil;
	for(UIView *oneView in nextCell.contentView.subviews)
	{
		if([oneView isMemberOfClass:[UITextField class]])
		{
			nextField = (UITextField *)oneView;
		}
		// 새로운 텍스트 필드가 퍼스트 리스폰더가 되었음.
		[nextField becomeFirstResponder];
	}
	
	
	// 키보드를 보이지 않게 처리한다.
	//[sender resignFirstResponder];
}
: