iPhone Programming for Dummy


5. 2단계 테이블 작성

이번에는 앞에서 작성한 메뉴판 프로그램을 수정 & 확장해서, 흔히 사용되는 Tree 구조의 메뉴 목록을 만들어 보도록 하자. 아마 가장 흔하게 사용되는 응용 프로그램의 형태가 이런 단계를 가진 테이블 뷰 형태가 아닐까 생각된다. 애플 문서에서는 테이블을 다단계 형태로 만들 경우, 3단계 이상의 깊이가 되지 않도록 강력 권유하고 있다. 이런 점은 모든 메뉴트리 구성에서 피해야 할 금기 사항이다. 만일 여러분이 가지고 있는 휴대 전화기에서 3단계 이상의 깊이를 가진 메뉴가 존재한다면, 그 단말기의 UI 는 잘못된 것이다.

물론 작성자가 원한다면 100 단계 테이블을 작성해도 된다! 어찌되었건 원하는 만큼의 단계를 가진 테이블을 작성하기 위한 기본적인 방법을 살펴보자.



5.1. 메뉴 데이터 구성하기

일단, 메뉴판의 데이터, 즉 MVC 패턴 중 Model 부분을 어떻게 구성할지 생각해보자. 사실 이 부분은 상당히 중요하다. 다단계 테이블을 작성하기 위해 '반드시' 이렇게 해야 한다는 규정은 없다. 하지만 정석적인 방법으로 NSArray 와 NSDictionary 를 사용한다.

우리가 새로 구성할 메뉴판의 형태는 다음과 같다.

  • 중국음식
    • 삼선볶음밥.삼선짬뽕.물만두.만안전석.칠리새우.짜장면
  • 한국음식
    • 비빔밥.김밥.된장찌개.빈대떡.막걸리.미역국
  • 패스트푸드
    • 햄버거.감자튀김.닭튀김.새우버거.콜라

음식들이 왜 이러냐고? 그냥 생각나는대로 막 적다보니 그렇다. 여러분은 여러분만의 메뉴를 구성하기 바란다. 이렇게 계획된 데이터를 기본으로 Model 데이터를 구성한다. 여기서 NSDictionary 를 사용하는데, 하나의 음식 종류에 대해서 다음과 같은 형태의 NSDictionary 를 만들기로 한다. 중국음식의 예를 보자.

NSDictionary chineseFood
키값:"title"   객체:"중국음식"
키값:"menu" 객체:menuList

NSArray menuList
객체:"삼선볶음밥","삼선 짬뽕","물만두","만안전석","칠리새우","짜장면",nil

즉 중국 음식에 대한 NSDictionary 객체는 두 개의 객체를 가지는데, 하나는 메뉴의 제목이고 다른 하나는 메뉴의 상세 내용을 가지고 있는 NSArray 배열 객체다. NSDictionary 에 대한 내용은 Objecive-C 에 대한 온라인 레퍼런스 문서나 다른 참고자료를 보기 바란다.

한국음식과 패스트푸드도 같은 형태의 NSDictionary 를 만든다. 그리고, 각 세 개의 NSDictionary 를 하나의 배열로 묶은 것이 전체 메뉴 데이터가 되는 것이다.

이제, 다음과 같이 createDemoData 메소드의 내용을 수정하자. 어떤식으로 작성했는지 그 형태가 쉽게 보일 것이다.


// 데모용 리스트 데이터를 만든다.
- (void)createDemoData {
    NSArray *mainMenu, *menuList;
    NSDictionary *chineseFood, *koreanFood, *fastFood;
   
    menuList = [[NSArray alloc] initWithObjects:@"삼선볶음밥", @"삼선 짬뽕", @"물만두", @"만안전석", @"칠리새우", @"짜장면",nil];
    chineseFood = [[NSDictionary alloc] initWithObjectsAndKeys:@"중국음식", @"title", menuList, @"menu", nil ];
    [menuList release];

    menuList = [[NSArray alloc] initWithObjects:@"비빔밥", @"김밥", @"된장찌개", @"빈대떡", @"막걸리", @"미역국",nil];
    koreanFood = [[NSDictionary alloc] initWithObjectsAndKeys:@"한국음식", @"title", menuList, @"menu", nil ];
    [menuList release];
   
    menuList = [[NSArray alloc] initWithObjects:@"햄버거", @"감자튀김", @"닭튀김", @"새우버거", @"콜라", nil];
    fastFood = [[NSDictionary alloc] initWithObjectsAndKeys:@"패스트푸드", @"title", menuList, @"menu", nil ];
    [menuList release];
   
    mainMenu = [[NSArray alloc] initWithObjects:chineseFood, koreanFood, fastFood, nil];
    [chineseFood release];
    [koreanFood release];
    [fastFood release];
   
    self.list = mainMenu;
    [mainMenu release];
}


만일 3단계의 메뉴를 작성하고자 한다면 이 부분이 좀 더 달라질 것이다. NSDictionary 가 하위 테이블을 위해서 또 다른 NSDictionary 를 가지게 되는 형태가 될 것이다. 그리고, 우리는 Plain 형태의 테이블만 구성하고 있지만, 만일 Group 형태의 테이블을 구성하고자 한다면 보다 세분화된 형태의 NSDictionary 를 구성해야 한다. 앞서 말한대로 테이블의 Model 을 작성하는데에 강제로 정해진 규칙은 없지만, NSDictionary 를 사용하는 것이 가장 편리해 보인다.



5.2. Controller 추가하기

컨트롤러를 하나 더 추가하는 작업이 필요하다. 즉, 메뉴판에서 가장 상위 메뉴 테이블에 대한 컨트롤러 외에도, 하위 메뉴 테이블에 대한 동작을 처리할 컨트롤러가 필요한 것이다.

알다시피, 컨트롤러는 하나의 객체로서, 별도의 소스파일로 존재한다. Xcode 의 메뉴에서 New File 을 선택하면 다음 그림과 같이 프로젝트에 원하는 형태의 객체를 추가할 수 있다.
우 리는 UIViewController subclass 를 선택하자. UITableView 의 컨트롤러 객체가 필요한데, 그것은 UIViewController 의 서브 클래스이기 때문이다. Next 버튼을 누르면 다음 화면으로 진행한다.



클래스의 소스 파일 이름을 기입하는 창이 나타난다. 여기서는 DetailViewController 라고 정하기로 하자. 창의 나머지 부분들은 생성될 클래스 파일이 프로젝트에 포함되는 것에 대한 여러 가지 설정인데, 보통 기본값으로 그냥 내버려두면 된다. 그러면 Finish 버튼을 누르는 것으로, 헤더 파일과 함께 두 개의 파일이 프로젝트에 포함될 것이다.


자, 이제 새로운 클래스 파일이 프로젝트에 생성되었는다. 이것이 제대로 동작되도록 내용을 구성하기 전에, 먼저 기존에 있든 RootViewController 컨트롤러의 내용을 수정하자. 현재 메뉴 데이터가 변경되었기 때문에, 컨트롤러의 내용을 수정해야만 메뉴의 초기 화면이 화면에 나타날 것이다.



5.3. 다단계 테이블 구현

수정된 RootViewController.h 의 내용은 다음과 같다. 변경된 부분을 살펴보기 바란다. 새로 추가된 DetailViewController 를 참조하기 위해 클래스 변수를 선언하고 있다.

//
//  RootViewController.h
//  MyNavi
//
//  Created by 김태한 on 08. 07. 07.
//  Copyright __MyCompanyName__ 2008. All rights reserved.
//

#import <UIKit/UIKit.h>

@class DetailViewController;

@interface RootViewController : UITableViewController {
    DetailViewController *detailViewController;
}

@property (nonatomic, retain) DetailViewController *detailViewController;

@end

물론, 지금은 완성하지 않은 상태이지만, DetailViewController.h 헤더 파일을 #import 해서 작성하는 것도 좋은 방법이다. 단, 클래스 선언만 필요한 경우 위의 예제에서 보인 것 처럼 간단히 @class 지시문을 사용하는 것이 편리하다.

이제 RootViewController.m 의 내용을 수정하자. 일단, 최상위 메뉴 테이블의 모든 항목은 하위 메뉴 테이블로 이동하게 되므로, 악세사리로 UITableViewCellAccessoryDisclosureIndicator 를 달아주어야 한다. 해당 메소드를 다음과 같이 수정하자.

- (UITableViewCellAccessoryType)tableView:(UITableView *)tabelView
         accessoryTypeForRowWithIndexPath:(NSIndexPath *)indexPath {
    return UITableViewCellAccessoryDisclosureIndicator;
}


그리고, 각 메뉴 테이블의 내용을 반환하는 곳이 수정되어야 한다. 이전과는 달리 단순한 배열이 아니라 NSDictionary 가 포함되어 있는 구조로 변경되었기 때문이다. 그리고, 현재 RootViewController 가 원하는 테이블의 내용은 각 NSDictionary 의 @"title" 키 값으로 묶여 있는 내용이다.
해당 메소드의 내용을 다음과 같이 수정한다. 많은 부분은 이전과 동일하다. cell 에 값을 전달해주는 마지막 두 줄을 참고하기 바란다.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
   
    static NSString *MyIdentifier = @"MyIdentifier";
   
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:MyIdentifier] autorelease];
    }
   
    // 디스플레이할 객체를 얻고 cell에 값을 설정한다.
    MyNaviAppDelegate *appController =
                 (MyNaviAppDelegate*)[[UIApplication sharedApplication] delegate];
   
    // Set up the cell
    NSDictionary *itemAtIndex =
                (NSDictionary *)[appController objectInListAtIndex:indexPath.row];
    cell.text = [itemAtIndex objectForKey:@"title"];
    return cell;
}


여기서, 일단 결과를 보기 위해 기존에 있던 didSelectRowAtIndexPath: 메소드의 내용은 삭제한 후 프로그램을 Build & Run 해보도록 하자. 만일 성공적으로 실행되었다면 다음과 같은 화면이 나타날 것이다. 단, 현재 각 메뉴를 선택하면 다음 단계로 넘어가는 부분은 작성하지 않았기 때문에, 메뉴를 터치해도 아무 일도 일어나지 않는다.


그러면, 다시 RootViewController.m 으로 돌아와서, 이제 테이블의 각 항목을 터치하면 다음 단계의 테이블로 이동하는 코드를 작성하여 프로그램을 완성해 보도록 하자.

RootViewController 에서 해 주어야 할 일은, 사용자가 테이블의 어느 한 항목을 터치하면, 테이블 뷰의 컨트롤러를 DetailViewController 로 넘겨주고, 화면 상단의 제목 표시 부분, 즉 네비게이션 컨트롤에게 한 단계 진행되었음을 알려주는 것이다.

먼저, 기본 작업을 위해 DetailViewController.h 헤더 파일의 내용을 다음과 같이 작성하자. 대부분의 내용은 자동으로 생성된 것이고, 사용자가 추가할 것은 detailItem 변수에 대한 것이다. 참고로, DetailViewController 의 상속 클래스를 UITableViewController 로 변경하는 것을 잊지 말자.

//  DetailViewController.h
//  MyNavi
//
//  Created by 김태한 on 08. 07. 30.
//  Copyright 2008 __MyCompanyName__. All rights reserved.
//

#import <UIKit/UIKit.h>


@interface DetailViewController : UITableViewController {
    NSDictionary *detailItem;
}

@property (nonatomic, retain) NSDictionary *detailItem;

@end


detailItem 은 사용자가 선택한 메뉴의 상세 메뉴 테이블 데이터를 가지게 될 딕셔너리 멤버 변수다.
이제, RootViewController.m 에서 해 주어야 할 마지막 수정 작업으로, 사용자가 메뉴 중 하나를 선택(터치)한 경우 처리할 일을 마무리 짓는 것이다.

앞서 말한대로, 네비게이션 컨트롤에게 한 단계 진행되었음을 알려주고나서 테이블 뷰의 컨트롤러를 새로운 컨트롤러로 변경한다.

다음의 메소드가 그러한 일을 하도록 변경된 코드다.

//  터치하면 하위 메뉴로 진행한다.

 - (void)tableView:(UITableView *)tableView
           didSelectRowAtIndexPath:(NSIndexPath *) indexPath
{
     // Create the detail view lazily
     if (detailViewController == nil) {
         DetailViewController *aDetailViewController =
                                  [[DetailViewController alloc] initWithStyle:UITableViewStylePlain ];
         self.detailViewController = aDetailViewController;
         [aDetailViewController release];
     }

     // Set the detail controller's inspected item to the currently-selected item
     MyNaviAppDelegate *appController =
                               (MyNaviAppDelegate *)[[UIApplication sharedApplication] delegate];
     detailViewController.detailItem = [appController objectInListAtIndex:indexPath.row];
     [[self navigationController] pushViewController:detailViewController animated:YES];
}

소스 코드에서는 처음 하위 메뉴로 진행될 때, 새로운 객체인 DetailViewController 를 생성하고 초기화 해서 할당하고 있다. 모든 객체를 이렇게 실제로 사용할 때가 되어서만 할당하는 것이 보자 메모리 사용 효율을 높이기 위해 추천되는 방법이다. 물론 나중에 시스템이 메모리가 모자라다는 메시지를 보내는 경우에, 가능한 상황이라면 이렇게 할당된 메모리는 다시 반환될 수도 있을 것이다. 시스템이 필요로 하는 경우 메모리를 잘 반환해 주는 것도 좋은 프로그램을 작성하는 길이다.

메소드의 뒷 부분에서는 detailItem 에 사용자가 선택한 해당 메뉴의 상세 메뉴에 해당하는 딕셔너리 객체를 할당해 주고 있다. 그리고 마지막으로는 네비게이션 컨트롤러에 새로 배정된 컨트롤러가 어떤것인지를 알려주고, 다음 단계로 넘어가는 동작을 사용자에게 보여주도록 지시한다. 네비게이션 컨트롤러에 새로운 컨트롤러 객체를 push 해 주는 것 만으로도, 네비게이션 컨트롤은 다시 이전 컨트롤러로 돌아갈 수 있는 back 버튼을 좌측에 만들어 주고, 심지어 그 버튼을 누르는 경우 다시 RootViewController 로 돌아가는 동작까지 알아서 다 처리해 준다. 정말 편리하기 서울역에 그지 없다.

이로서 RootViewController.m 에서 수정할 작업은 모두 끝났다.

이제 DetailViewController.m 을 살펴보자. 먼저, 다음과 같은 메소드를 추가한다. 이것은 현재 상세 메뉴로 처음 진입한 후 처리를 의미한다.

- (void)viewWillAppear:(BOOL)animated {
    // Update the view with current data before it is displayed
    [super viewWillAppear:animated];
   
    // Scroll the table view to the top before it appears
    [self.tableView reloadData];
    [self.tableView setContentOffset:CGPointZero animated:NO];
    self.title = [(NSDictionary *)detailItem objectForKey:@"title"];
}

처음 진입하고서 초기화 과정을 거치고, detailItem 에 있는 딕셔너리 데이터 중에서 @"title" 키 값에 해당하는 객체를 현재의 제목으로 지정한다.

그리고 나서 해야 할 일은, 기본적으로 RootViewController 와 같다고 보면 된다. 이것 역시 마찬가지로 하나의 테이블에 대한 컨트롤러이기 때문이다. 먼저, 이 테이블이 몇 개의 섹션으로 되어 있으며, 전체 아이템 갯수가 몇개인지를 알려주어야 한다.

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    // plain 형태이므로, 1을 반환한다.
    return 1;
}

// 몇 개의 항목이 있는지 알려주어야 한다. 이걸 모르면 시스템은 목록을 작성하지 않는다.
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return [[detailItem objectForKey:@"menu"] count];
}

항목의 갯수를 반환하기 위해서 @"menu" 키 값의 배열애 대한 count 값을 반환한다는 것만 주의하면, 이제 너무 간단해 보일 것이다.

마지막 남은 것은 각 Cell 아이템의 인덱스에 해당하는 올바른 메뉴 이름만 반환해 주면 된다.

- (UITableViewCell *)tableView:(UITableView *)tableView
                            cellForRowAtIndexPath:(NSIndexPath *)indexPath
{   
    static NSString *MyIdentifier = @"MyId2";
   
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:RundMyIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:MyIdentifier] autorelease];
    }

    // Set up the cell
    cell.text = [[detailItem objectForKey:@"menu"] objectAtIndex:indexPath.row];
    return cell;
}

Cell 의 메모리를 할당하거나 초기화 하는 작업 역시 RootViewController 와 마찬가지로 하면 된다. 단, 여기서는 Identifier 는 다른 임의의 문자열 값을 사용하고 있다. 그리고 cell 의 text 값으로 @"menu" 배열에서 올바른 인덱스 값에 해당하는 문자열 객체를 전달해 주면 된다. 이것으로 끝이다. 모든 것이 RootViewController 와 크게 다를 것이 없다는 사실만 생각한다면 간단한 작업들이다.

이제 프로젝트를 Build & Run 한 다음, 임의의 메뉴 목록을 터치해보자. 만일 중국 음식을 선택했다면, 테이블은 우측에서 좌측으로 전환되면서 다음과 같이 화면이 변경될 것이다.


네비게이션 컨트롤의 좌측에는 다시 상위 테이블 항목으로 돌아가는 버튼이 이미 생성되어 있으며, 이것을 터치하면 실제로 그렇게 된다.



다음은 두 컨트롤 객체의 소스 코드다. 전체 소스를 여기 싣는것이 테스트 하거나 참조하는데에 좀더 편할것으로 생각되어서 이렇게 해 보았다.

//
//  RootViewController.m
//  MyNavi
//
//  Created by 김태한 on 08. 07. 07.
//  Copyright __MyCompanyName__ 2008. All rights reserved.
//

#import "RootViewController.h"
#import "MyNaviAppDelegate.h"

@implementation RootViewController

@synthesize detailViewController;


- (void)viewDidLoad {
    // Add the following line if you want the list to be editable
    // self.navigationItem.leftBarButtonItem = self.editButtonItem;
    self.title = @"메뉴판";
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    // plain 형태이므로, 1을 반환한다.
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    MyNaviAppDelegate *appController = (MyNaviAppDelegate*)[[UIApplication sharedApplication] delegate];
   
    return [appController countOfList];
}


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
   
    static NSString *MyIdentifier = @"MyIdentifier";
   
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:MyIdentifier] autorelease];
    }
   
    // 디스플레이할 객체를 얻고 cell에 값을 설정한다.
    MyNaviAppDelegate *appController = (MyNaviAppDelegate*)[[UIApplication sharedApplication] delegate];
   
    // Set up the cell
    NSDictionary *itemAtIndex = (NSDictionary *)[appController objectInListAtIndex:indexPath.row];
    cell.text = [itemAtIndex objectForKey:@"title"];
    return cell;
}

- (UITableViewCellAccessoryType)tableView:(UITableView *)tabelView
         accessoryTypeForRowWithIndexPath:(NSIndexPath *)indexPath {
    return UITableViewCellAccessoryDisclosureIndicator;
}

// 터치하면 하위 메뉴로 진행한다.
 - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
     // Navigation logic
     // Create the detail view lazily
     if (detailViewController == nil) {
         DetailViewController *aDetailViewController = [[DetailViewController alloc] initWithStyle:UITableViewStylePlain ];
         self.detailViewController = aDetailViewController;
         [aDetailViewController release];
     }
     // Set the detail controller's inspected item to the currently-selected item
     MyNaviAppDelegate *appController = (MyNaviAppDelegate *)[[UIApplication sharedApplication] delegate];
     detailViewController.detailItem = [appController objectInListAtIndex:indexPath.row];
     [[self navigationController] pushViewController:detailViewController animated:YES];
}


//
//  DetailViewController.m
//  MyNavi
//
//  Created by 김태한 on 08. 07. 30.
//  Copyright 2008 __MyCompanyName__. All rights reserved.
//

#import "DetailViewController.h"

@implementation DetailViewController

@synthesize detailItem;

- (void)viewWillAppear:(BOOL)animated {
    // Update the view with current data before it is displayed
    [super viewWillAppear:animated];
   
    // Scroll the table view to the top before it appears
    [self.tableView reloadData];
    [self.tableView setContentOffset:CGPointZero animated:NO];
    self.title = [(NSDictionary *)detailItem objectForKey:@"title"];
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    // There are no sections
    return 1;
}

// 몇 개의 항목이 있는지 알려주어야 한다. 이걸 모르면 시스템은 목록을 작성하지 않는다.
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return [[detailItem objectForKey:@"menu"] count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
   
    static NSString *MyIdentifier = @"MyId2";
   
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:MyIdentifier] autorelease];
    }

    // Set up the cell
    cell.text = [[detailItem objectForKey:@"menu"] objectAtIndex:indexPath.row];
    return cell;
}

- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    // 이번에는 각 셀을 사용자가 선택하는 기능은 없다. 따라서 여기서 nil 을 리턴한다.
    return nil;
}

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
    // Return YES for supported orientations
    return (interfaceOrientation == UIInterfaceOrientationPortrait);
}


- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning]; // Releases the view if it doesn't have a superview
    // Release anything that's not essential, such as cached data
}


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


@end




5.4. 마치며

지금까지 간단한 2단계 테이블 작성을 살펴보았다. 보다 더 많은 내용은 애플의 SimpleDrillDown 예제나 다른 각종 예제 소스 코드를 참조하면 많은 내용을 볼 수 있을 것이다. 특히 Grouped 테이블을 구성하는 것에 대해서 Developer Conndetion 의 각종 예제를 살펴보면 구체적인 정보를 얻을 수 있을 것이다. 예제 한가지만 살펴보면 어렵지 않게 알 수 있다.
테이블을 구성하는 것은 처음에는 상당히 복잡한 작업 처럼 느껴지지만, 실제 결과물을 보면 작업한 양에 비해서 상당히 효과가 크고 편리하다는 것을 알 수 있다. 또한 이렇게 테이블 뷰를 사용할 수 있게 되면 여러가지 iPhone 용 응용 프로그램을 작성하기가 편해질 것으로 생각된다.

이 게시물을..