본문 바로가기

개발/App Developer

테이블 뷰 입문

* 테이블

- 테이블은 데이터의 목록을 표시한다.

- 테이블 목록의 각각의 항목은 행(row), 각 행마다 열(column)은 하나만 있다.

- 테이블 뷰는 테이블에 있는 데이터를 보여주는 뷰이고, UITableView 클래스의 인스턴스이다.

  테이블에 각각의 행은 UITableViewCell 클래스로 구현된다.

  그래서 테이블 뷰는 테이블의 전체적인 모양을 담당하는 객체이고, 테이블 뷰 셀이 테이블의 각각의 행을 그리는 일을 담당한다.

- 테이블 뷰는 테이블의 데이터를 저장하는 역할은 하지 않는다.

  단지 현재 보여주는 행을 그릴 때 필요한 데이터만 저장하낟.

- 테이블 뷰의 설정 데이터는 UITableViewDelegate 프로토콜을 따르는 객체에서 구하고,

  각 행의 데이터는 UITableViewDataSource 프로토콜을 따르는 객체로부터 얻는다.

- 테이블 뷰에는 두가 기본 스타일이 존재.

  그룹으로 묶은 방식(Group Table) : 그룹으로 묶은 테이블의 각각의 그룹은 둥근 사각형에 둘러쌓인 행의 집합이다.

  인덱스로 구분한 테이블(Indexed Table) : 둥근 사각형이 없는 테이블

- 테이블의 나누어진 영역이 데이터 소스에서는 섹션이다.

  그룹으로 묶은 테이블에서 각 그룹은 섹션이다.

  인덱스로 묶은 테이블에서는 데이터를 인덱스별로 묶은 것이 섹션이다.

 

* TableView Data Source

//  이것은 특정 섹션에 몇개의 행이 있는지 질의 하는데 사용한다.

 

-(NSInteger)tableView:(UITableView*)tableView numberOfRowsInsection:(NSInteger)section

{

     return [self.listData count];  // 여기서는 배열의 카운트를 리턴해 준다.

}

 

// 행을 그릴 필요가 있을 때 테이블 뷰가 이 메서드를 호출한다.

// NSIndexPath 로부터 행이나 섹션을 얻을 수 있다.

// 첫번째 인자인 tableView 는 현재 메서드를 호출한 테이블의 레퍼런스

// 두번째 인자는 NSIndexPath 의 객체

 

-(UITableViewCell *)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath

{

      /*

      정적 문자열 인스턴스의 선언

      이 문자열은 테이블 셀의 종류를 나타내는 키로 쓰인다.

      여기에서는 셀의 종류가 하나만 있으므로 하나만 선언한다.

 

      테이블 뷰는 아이폰의 작은 화면에 단지 몇개의 행만 표시할 수 있지만 테이블 자체는 훨씬 더 많은 데이터를 담을 수 있다.

      테이블의 각 행은 UITableViewCell 의 인스턴스로 나타냄을 염두에 둔다.

      또 UITableViewCell 은 UIView 의 하위 클래스인데 이는 각각의 행은 하위뷰를 가질 수 있다는 것을 의미한다.

 

      테이블이 클 경우 현재 보이지 않더라도 각각의 행마다 하나의 테이블 뷰 셀을 유지한다면 이것은 엄청난 메모리 손실을 의미한다.

      다행히테이블은 이런 방식으로 동작하지 않는다.

     

      대신 테이블 뷰의 셀들이 스크롤되면서 화면에서 사라지게 되면 재 사용이 가능한 셀의 큐(queue)에 들어간다.

      시스템의 메모리가 부족하면 테이블 뷰는 큐의 셀을 제거하지만 메모리 여유가 있는 한 다시 사용할 때를 대비해서 유지한다.

 

      테이블 뷰 셀이 화면에서 밀려날 때마다 화면 반대편에 다른 셀을 보여줄 기회가 있다.

      만약 새로운 행이 이전에 사라졌던 행 중에서 다시 사용하게 된 경우라면 시스템은 끊임없이 이러한 뷰를 만들고 해제하는 부담을 피할 수 있다.

 

      이러한 방법을 사용하기 위해서 디큐(dequeued) 된 셀 중에서 필요한 타입을 테이블 뷰에서 얻어야 하는 데

      이때 앞에서 아래에 선언한 NSString 식별자를 사용하는 것에 주목한다.

      사실상 SimpleTableIdentifier 와 같은 종류의 재사용 가능한 셀을 요청하는 것이다.

      */

      static NSString *SimpleTableIdentifier = @"SimpleTableIdentifier";

 

      // 테이블 뷰의 셀들이 스크롤 돼서 화면에서 사라지면 재사용 가능한 셀의 뷰(queue)에 들어간다.

      // 새로운 행이 이전에 사라졌던 행 중에서 다시 사용하게 된다면 시스템은 끊임없이 이러한 뷰를 만들고 해제하는 부담을 피할 수 없다.

      // 이러한 방법을 사용하기 위해서 디큐(dequeue) 된 셀 중에서 필요한 타입을 테이블 뷰에 얻어야 한다. (SimpleTableIdentifier)

     

      UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:SimpleTableIdentifier];

 

     

      /*

      테이블 뷰가 여분의 셀이 전혀 없을 가능성도 있으므로 셀이 nil 인지 확인한다.

      만약, nil 이라면 이전의 식별자 문자열을 사용해서 수동으로 새로운 테이블 뷰 셀을 만든다.

      언젠가 여기서 만든 셀을 재사용할 수도 있으니 같은 종류인지 확인할 필요가 있다.

      */

      if(cell == nil)

      {

            // 재사용 가능하게 동일한 셀로 만든다.

           

            //default

            // cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:SimpleTabeIdentifier] autorelease];

 

            /*

            * 3.0 에서 추가된 detailTextLabel 을 사용하기 위해서 스타일을 변경한다.

            * UITableViewCellStyleSubtitle

            */

            // cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:SImpleTableIdentifier] autorelease];

 

            /* 

            * UITableViewCellStyleValue1

            * 이 스타일은 셀 이미지를 사용하지 않으므로 텍스트 레이블과 상세 레이블을 한줄에 배치하고 서로 대칭되도록 정렬한다.

            */

            // cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:SimpleTableIdentifier] autorelease];

 

            /*

            * UITableViewCellStyleValue2

            * 이 스타일은 셀에 대한 정보 설명을 위해 사용되는 레이블과 나란히 출력할 때 사용된다.

            * 셀 아이콘을 보여줄 때는 나타나지 않지만, 세부 레이블을 텍스트 레이블의 왼쪽으로 배치한다.

            */

            cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue2 reuseIdentifier:SimpleTableIdentifier] autorelease];

           

      }

 

     /*

     이제 테이블 뷰에 넘겨 줄 테이블 뷰 셀을 만들었고, 보여주고 싶은 정보를 셀 안에 표시하는 것이 남았다.

     그리고 테이블 뷰의 값에 접근하기 위해서는 어떤 행의 값을 사용할 것인지 결정해야 한다.

     아래 구문은 받은 indexPath 의 row 로 얻은 int 를 배열의 인자로 사용한다.

     */

 

      // 어떤 행의 값을 사용할 것인지 결정

      NSUInteger row = [indexPath row];

 

     // textLabel.text 는 오직 아이폰 SDK 3.0 이상에서만 동작한다.

      cell.textLabel.text = [listData objectAtIndex:row];

 

     // 폰트 크기 변경

      cell.textLabel.font = [UIFont boldSystemFontOfSize: 50];

 

      return cell;

}

 

 

매서드의 첫 번째 인자인 tableView 는 현재 매서드를 호출한 테이블의 레퍼런스(참조)이다.

이것은 여러 테이블용 데이터소스처럼 행동하는 클래스를 만들도록 해준다.

 

매서드의 두번째 인자는 NSIndexPath 의 개체임을 주목해야 한다.

이 객체를 사용해 테이블 뷰가 섹션과 행을 하나의 객체로 감싼다.

둘 다 int 값을 리턴하는 행 메서드나 섹션 메서드 가운데 하나를 호출하면 NSIndexPath 로부터 행이나 섹션을 얻을 수 있다.

 

 

* 각 행에 이미지 추가하기

 

 

-(UITableViewCell *)tableView:(UITableView *) cellForRowAtIndexPath:(NSIndexPath *)indexPath

{

      static NSString *SimpleTableIdentifier =@"SimpleTableIdentifier";

 

      UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:SimpleTableIdentifier];

 

      if(cell == nil)

      {

            cell = [[[[UITableViewCell alloc] initWithFrame:CGRectZero] reuseIdentifier:SimpleTableIdentifier] autorelease];

      }

 

      // cell 에 이미지를 추가한다.

     // UIImage 는 파일 이름을 기반으로 캐쉬 기술을 사용하므로, 매번 새 이미지를 로딩하지 않을 것이다.

     // 대신 캐쉬된 이미지를 사용할 것이다. 

     // 이미지는 셀의 텍스트 왼편에 보이며 셀이 선택 되었을 때 highlightedImage 에 명시된 이미지로 바뀐다.      

 

      UIImage *image = [UIImage imageName:@"star.png"];

      cell.imageView.image = image;

 

      // 해당 cell 이 선택되었을 때 이미지 추가

     // UIImage *highlightedImage = [UIImage imageName:@"star2.png"];

     // cell.imageView.highlightedImage = highlightedImage;

 

 

      NSUInteger row = [indexPath row];

      cell.textLabel.text = [listData objectAtIndex:row];

      return cell;

 

}

 

 

 

 

 

[테이블 뷰 셀 스타일]

아이폰 SDK 3.0 이전 버전까지는 단일 셀 스타일로 제한되었고 3.0 부터 몇가지 기능을 표준 셀에 추가했다.

- 이미지 : 이미지는 특정 스타일의 한 부분으로 이미지는 셀 텍스트의 왼쪽에 보여진다.

- 텍스트 레이블 : 셀의 첫 번째 텍스트이다. 이전 버전에서 UITableViewCellStyleDefault 는 셀에 유일한 텍스트 레이블을 위해 사용되었다.

- 세부 텍스트 레이블 : 보통 설명을 위한 글이나 레이블을 형태로 사용되는 셀의 두 번째 텍스트이다.

 

//UITableViewCellStyleDefault 를 사용하면 설명을 위한 텍스트가 만들어지지 않는다.

if(row < 7)

          cell.detailTextLabel.text = @"Mr. Ban";

else{

          cell.detailTextLabel.text = @"Mr. Minam";

}

 

* UITableViewCellStyle 의 종류

- 하위 제목 스타일 적용 : 하위 제목은 작은 글자로 텍스트 레이블을 설명하는 글을 담고 있으며,

                                    텍스트 레이블 밑에 회색 컬러를 사용하여 출력된다.

 

UITableViewCellStyleDefault 를 UITableViewCellStyleSubtitle 로 변경해보면

detailTextLabel 을 확인할 수 있다.

 

 

cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:SimpleTableIdentifier] autorelease];

cell.detailTextLabel.text = @"xxxxxx";

 

 

 

- 텍스트 레이블과 상세 레이블을 한 줄에 배치하고 서로 대칭되도록 정렬 : 텍스트 레이블은 검은색으로 셀의 왼쪽에 나타나고,

                                                                                                     상세 텍스트는 파란색으로 셀의 오른쪽에 나타난다.

 

cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:SimpleTableIdentifier] autoreleas];

cell.detailTextLabel.text = @"xxxxxx";

 

 

- 셀에 대한 정보를 설명하기 위해 사용되는 레이블과 나란히 출력할 때 사용된다.

 

cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue2 reuseIdentifier:SimpleTableIdentifier] autorelease];

cell.detailTextLabel.text = @"xxxxxx";

 

 

 

 

 

 

[테이블 델리게이트]

델리게이트의 목적은 테이블 뷰의 모습을 설정하고 특정 사용자 상호작용을 처리하는 것이다.

 

 

* Delegate 를 사용해서 몇몇 행은 들어쓰기 (Indent)

 

#pragma mark -

#pragma mark Table Delegate Methods

 

// 들어쓰기 레벨 설정

-(NSInteger)tableView:(UITableView *)tableView indentationLevelForRowAtIndexPath:(NSIndexPath *)indexPath

{

      // 각 행의 행 번호만큼 들여쓰기 레벨을 설정하도록 한다.

      NSUInteger row = [indexPath row];

      return row ;

}

 

 

 

 

 

* 특정 행 선택 불가능 하게 하기

- 델리게이트는 두 개의 메서드를 사용해서 사용자가 특정행을 선택했는지 알 수 있다.

  그 중 한 개가 행이 선택되기 전에 호출되고 이 매서드에서 행이 선택되는 것을 막거나 심지어 선택되는 행을 바꿀 수도 있다.

 

-(NSIndexPath *)tableView:(NSTableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath

{

      NSUInteger row = [indexPath row];

      // 선택하려는 항목이 첫번째이라면 nil 을 리턴하여 선택하지 못하게 한다.

      if(row == 0)

           return nil;

      return indexPath;

}

 

* 선택한 특정 행 알아내기

 

// 이 메서드는 실제 선택을 다룬다

// 사용자가 행을 골랐을 때 적절한 동작을 수행하는 곳이며, 선택했을 때 안내 메시지 창을 띄운다.

-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath

{

      NSUInteger row = [indexPath row];

      NSString *rowValue = [listData objectaAtIndex:row];

      NSString *message = [[NSString alloc] initWithFormat:@"You selected %@", rowValue];

      UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Row Selected" message:message delegate:nil cancelButtonTitle:@"Yes I Dis" otherButtonTitles:nil];

      [alert show];

      [message release];

      [alert relase];

 

     /*

     선택한 행과 다른 행이나 영역을 선택하는 효과가 있는 인덱스패스(indexPath)를 바꿀 수 있다.

     아래 행을 주석 처리하면 경고 메시지 창이 뜨고 확인 후 선택되어 있는 행을 볼 수 있지만

     주석을 풀면 선택된 행은 없어진다.

     */

      [tableView deselectRowAtIndexPath:IndexPath animated:YES];

}

 

* 폰트 크기 바꾸기

 

cell.TextLabel.font = [UIFont boldSystemFontOfSize:50];

 

 

* 델리게이트에서 테이블 높이 지정

 // 테이블 행의 높이를 지정하는 델리게이트

// 필요하다면 각각의 행마다 고유의 값을 지정할 수도 있다.

-(CGFolat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath

{

      return 70;

}

 

 

* 맞춤형 테이블 뷰 만들기

- UITableViewCell 이 지원하는 방식이 아닌 다른 방식

  UITableViewCell 에 하위 뷰를 추가 하는 것.

  UITableViewCell 의 하위 클래스를 만드는 것

 

  UITableViewCell 에 하위 뷰를 추가 하는 방식

 

-(UITablveViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath

{

      static NSString *CellTableIdentifier = @"CellTableIdentifier";

      UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellTableIdentifier];

 

      if(cell == nil)

      {

             // 새로 추가될 새로운 셀을 만든다

             cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellTableIdentifier] autorelease];

 

             CGRect nameLabelRect = CGRectMake(0,5,70,15);

             UILabel *nameLabel = [[UILabel alloc] initWithFrame:nameLabelRect];

             nameLabel.textAlignment = UITextAlignmentRight;

             nameLabel.text = @"Name";

             nameLabel.font = [UIFont boldSystemFontOfSize:12];

             [cell.contentView addSubview:nameLabel];

             [nameLabel release];

 

             CGRect colorLabelRect = CGRectMake(0,26,70,15);

             UILabel *colorLabel = [[UILable alloc] initWithFrame:colorLabelRect];

             colorLabel.textAlignment = UITextAlignmentRight;

             colorLabel.text = @"Color";

             colorLabel.font = [UIFont boldSystemFontOfSize:12];

             [cell.contentView addSubview:colorLabel];

             [colorLabel release];

 

             CGRect nameValueRect = CGRectMake(80,5,200,15);

             UILabel *nameValue = [[UILabe alloc] initWithFrame:nameValueRect];

             nameValue.tag = kNameValueTag;   //나중에 해당 레이블에게 값을 할당할 수 있도록 이 필드를 찾을 방법을 추가

             [cell.contentView addSubview:nameValue];

             [nameValue release];

 

             CGRect colorValueRect = CGRectMake(80,25,200,15);

             UILabel *colorValue = [[UILabel alloc] initWithFrame:colorValueRect];

             colorValue.tag = kColorValueTag;    // 나중에 해당 레이블에게 값을 할당할 수 있도록 이 필드를 찾을 방법을 추가

             [cell.contentView addSubview:colorValue];

             [colorValue release];

       }

      NSUInteger row = [indexPath row];

      NSDictionary *rowData = [self.computers objectAtIndex:row];

      UILabel *name = (UILabel *)[cell.contentView viewWithTag:kNameValueTag];  // 해당 레이블 찾기

      name.text = [rowData objectForKey:@"Name"];

      UILabel *color = (UILabel *)[cell.contentView viewWithTag:kColorValueTag];   // 해당 레이블 찾기

      color.text = [rowData objectForKey:@"Color"];

      return cell;

}

 

 

 

 

 

  UITableViewCell 의 하위 클래스 만들어 사용하기

- 인터페이스 빌더를 사용해서 테이블 셀 뷰를 설계하기

- UITableViewCell 의 하위 클래스와 테이블 뷰 셀을 담을 새 nib 파일을 만든다.

  그런 후에 테이블 뷰 셀이 행을 표현할 때 테이블 뷰 셀에 하위 뷰를 추가하는 대신에 단순히 nib 파일에서

  구현한 하위 클래스를 로드하고 두 개의 새 아웃렛을 사용해서 이름과 색을 결정할 것.

 

1. Xcode 에서 class 폴더에 Cocoa Touch Classes 를 고르고 Objective-C class 를 선택한다.

   하위 클래스로 UITableViewCell 을 선택하고 새로운 파일을 추가한다.

   파일명은 "CustomCell"으로 하고  Also create 가 선택되었는지 확인.

 

2. Xcode 에서 resources 폴더에 User Interfaces 를 클릭하고 Empty XIB 을 추가한다.

   파일명은 "CustomCell".

 

3. CustomCell.h 파일에 아래 코드를 추가

 

#import <UIKit/UIKit.h>

 

@interface CustomCell : UITableViewCell

{

      UILabel *nameLabel;

      UILabel *colorLabel;

}

 

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

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

 

@end

 

4. CustomCell.m 파일에 추가

 

@synthesize nameLabel;

@synthesize colorLabel;

 

-(void)dealloc {

      [nameLabel release];

      [colorLabel release];

      [super dealloc];

}

 

5. CustomCell.xib 을 더블클릭해서 인터페이스 빌더를 연다.

- Table View Cell 을 라이브러리에서 찾아서 nib 메인 윈도우에 끌어 놓는다.

- 테이블 뷰 셀을 선택했는지 확인하고 아이덴티티 인스펙터를 띄우고 ClassIdentify 에서 Class 를 CustomCell 로 변경.

- View size 에서 높이를 65 로 변경

- Table View Cell 에서 Identifier 를 CustomCellIdentifier 로 변경

- 라이브러리에서 view 를 찾아서 Custom cell 윈도우를 추가한다.

- View 크기를 x 를 0 , y 를 0 , w 를 320 , h 를 65 로 변경

- 라이브러리에서 Label 을 4개 추가하여 위치를 맞추고 모양을 결정한다.

- Custom Cell 아이콘에서 드래드 해서 nameLabel 과 colorLabel 을 아웃렛에 할당한다.

- 이 테이블 셀은 데이터를 보여주기 위해서 사용하지만 사용자와의 상호작용은 테이블 뷰가 수행하므로

  독자적인 컨트롤러 클래스가 필요없다.

 

6. 새 테이블 뷰 셀 사용하기

 

#import "CustomCell.h"

 

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath

{

      static NSString *CustomCellIdentifier = @"CustomCellIdentifier";

      CustomCell *cell = (CustomCell *)[tableView dequeueReusableCellWithIdentifier:CustomCellIdentifier];

 

      if(cell == nil)

      {

            NSArray *nib = [[NSBundle mainBundle] loadNibNamed:@"CustomCell" owner:self options:nil];

 

            for(id oneObject in nib)

                  if([oneObject isKindOfClass:[CustomCell class]])

                        cell = (CustomCell *)oneObject;

      }

 

      NSUInteger row = [indexPath row];

      NSDictionary *rowData = [self.computers objectAtIndex:row];

      cell.colorLabel.text = [rowData objectForKey:@"color"];

      cell.nameLabel.text = [rowData objectForKey:@"Name"];

      return cell;

}

 

 

7. 이 델리게이트 메서드는 셀이 생기기 전에 호출돼서 필요한 값을 셀에서 얻지 못하므로 그 값을 하드코딩해야 한다.

 

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath

{

      return 66;

}

 

 

* 그룹으로 묶은 섹션과 인덱스로 구분한 섹션
- 인터페이스 빌더에서 추가한 TableView 에서 Table View Style 을 Grouped 로 변경한다.
 

### SectionsViewController.h #### 

 

#import <UIKit/UIKit.h>

@interface SectionsViewController:UIViewController <UITableViewDelegate, UITableViewDataSource>
{
    NSDictionary *names;
    NSArray *keys;
}

@property (nonatomic, retain) NSDictionary *names;
@property (nonatomic, retain) NSArray *keys;

@end

 


### SectionsViewController.m ####

#import "SectionViewController.h"

@implementation SectionsViewController
@synthesize names;
@synthesize keys;

-(void)viewDidLoad
{
    NSString *path = [[NSBundle mainBundle] pathForResource:@"sortednames" ofType:@"plist"];
    NSDictionary *dict = [[NSDictionary alloc] initWithContentsOfFile:path];

    self.names = dict;
    [dict release];

    NSArray *array = [[names allKeys] sortedArrayUsingSelector:@selector(compare:)];
    self.keys = array;
}

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

-(void)delloc
{
    [names release];
    [keys release];
    [super delloc];
}

#pragma mark -
#pragma mark Table View Data Source Methods
// 세션의 갯수가 몇개인지를 리턴한다.

-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return [keys count];
}

// 특정 섹션의 행의 갯수가 몇개인지를 리턴한다.
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    NSString *key = [keys objectAtIndex:section];
    NSArray *nameSection = [names objectForKey:key];
    return [nameSection count];
}

// 테이블 셀 생성
// indexPath에서 section과 row를 뽑아서 딕셔너리의 어떤 값에 해당하는지 찾을때 이용한다.
// section을 사용해서 names Dictionary에서 어떤 배열을 가져올지 알수 잇고 그래서 row를 그 배열에서 어떤 값을 이용할지 알아내는 데 사용할수 있다.
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSUInteger section = [indexPath section];
    NSUInteger row = [indexPath row];

    NSString *key = [keys objectAtIndex:section];
    NSArray *nameSection = [names objectAtIndex:key];

    static NSString *SelectionsTableIdentifier = @"SelectionsTableIdentifier";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:SelectionsTableIdentifier];

    if(cell == nil)
    {
        cell = [[[UITableView alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:SelectionsTableIdentifier] autorelease];
    }

    cell.textLabel.text = [nameSection objectAtIndex:row];
    return cell;
}

// 각 섹션의 헤더 값을 설정할수 있다.
-(NSString *)viewTable:(UIViewTable *)viewTable titleForHeaderInSection:(NSInteger)section
{
    NSString *key = [keys objectAtIndex:section];
    return key;
}

// 다음 메서드를 추가해서 Table View style이 Plain (인덱스 정렬)일 경우 Index를 추가 할수 있다.
-(NSArray *)sectionIndexForTitlesTableview:(UITableView *)tableView
{
    return keys;
}

 

 

 


 

* 검색 창 구현하기
- 디자인 다시 생각하기
--- 델리케이트와 데이터소스는 수정 가능한 딕셔너리에서 읽고 검색 기준이 바뀌거나 검색을 취소하면 수정 가능한 딕셔너리를 수정 불가능한 딕셔너리의 내용으로 고치면 될듯

* 깊은 뮤터블 복사
- NSDictionary는 NSMutableDictionary 프로토콜을 따르기 때문에 얕은 복사본인 NSMutableDictionary를 리턴한다. 이말은 mutableCopy 메서드를 호출 했을때 원래 딕셔너리가 가진 객체 모두를 가지는 새 NSMutableDictionary 객체를 만드는것을 위미한다.
- 얕은 복사는 해당 메모리 영역을 가리키는 주소 값만을 복사하는것을 의미한다. 보통 얇은 복사를 하게 되면 한 메모리 영역을 두 개의 포인터가 가리키게 되므로, 한쪽에서 할당된 메모리를 해제하면 다른 한쪽에서 사용할 수 있는 메모리 영역을 잃게 되는것이다.
- 깊은 복사는 메모리 영역 전체를 별도로 복사하는것을 의미한다.

* 카테고리 사용
- 카테고리를 사용하면 하위클래스를 만들지 않고 이미 존재하는 객체에 메서드를 추가할 수 있다.

 

### NSDictionary-MutableDeepCopy.h ####

 

#import <foundation foundation.h="">

@interface NSDictionary (MutableDeepCopy)
-(NSMutableDictionary *)mutableDeepCopy
@end
</foundation>


### NSDictionary-MutableDeepCopy.m ####
#import "NSDictionary-MutableDeepCopy.h"
-(NSMutableDictionary *)mutableDeepCopy
{
 NSMutableDictionary *ret = [[NSMutableDictionary alloc] initWithCapacity:[self count]];
 
 NSArray *keys = [self allKeys];
 
 for( id key in keys )
 {
  id oneValue = [self valueForKey:key];
  id oneCopy = nil;
  
  if([oneValue respondsToSelector:@selector(mutableDeepCopy)])
   oneCopy = [oneValue mutableDeepCopy];
  else if([oneValue respondsToSelector:@selector(mutableCopy)])
   oneCopy = [oneValue mutableCopy];
  
  if(oneCopy == nil)
   oneCopy = [oneValue copy];
  
  [ret setValue:oneCopy forKey:key];
 }
 
 return ret;

}
 

 

 

### SectionsViewController.h ###


#import <uikit uikit.h="">

@interface SectionsViewController : UIViewController <uitableviewdelegate, uitableviewdatasource,="" uisearchbardelegate="">
{
 // 검색 결과에 따라서 테이블에게 자신의 내용을 갱신하라고 전달할 필요가 있기 때문에 
 UITableView *table;
 // 검색할 때 필요한 컨트롤
 UISearchBar *search;
 // 원본 데이터가 들어 있는 NSDictionary
 NSDictionary *allNames;
 // 검색된 결과가 들어 있는 NSMutableDictionary
 NSMutableDictionary *names;
 // 검색된 결과가 들어가는 NSMutableArray
 NSMutableArray *keys;
 // 인덱스가 cancel 버튼위에 있는 것을 막기 위해서
 BOOL isSearching; 
}

@property (nonatomic, retain) IBOutlet UITableView *table;
@property (nonatomic, retain) IBOutlet UISearchBar *search;
@property (nonatomic, retain) NSDictionary *allNames;
@property (nonatomic, retain) NSMutableDictionary *names;
@property (nonatomic, retain) NSMutableArray *keys;

-(void)resetSearch;
-(void)handleSearchForTerm:(NSString *)searchForm;

@end

</uitableviewdelegate,></uikit>

### SectionsViewController.m ###
#import "SectionsViewController.h"
#import "NSDictionary-MutableDeepCopy.h"

@implementation SectionsViewController
@synthesize names;  // NSMutableDictionary
@synthesize keys;  // NSMutableArray
@synthesize table;  // UITableView
@synthesize search;  // UISearchBar
@synthesize allNames; // NSDictionary

#pragma mark -
#pragma mark Custom Methods
// 이 메소드는 검색을 취소하거나 검색어가 바뀌면 호출된다.
// allNames의 뮤터블 복사본을 만들어서 names에 할당하고 그런 후에 keys 배열을 갠신해서 알파벳의 모든 문자를 포함하게 한다.
-(void)resetSearch
{
 self.names = [self.allNames mutableDeepCopy];
 
 // 검색 결과에 한 섹션의 모든 값이 필요 없다면 그 섹션 자체도 제거해야 하기 때문에 수정가능한 배열로 선언한다.
 NSMutableArray *keyArray = [[NSMutableArray alloc] init];
 
 // 테이블뷰 인덱스 위에 검색창을 나타내기 위한 돋보기 아이콘 추가를 위한 값
 [keyArray addObject:UITableViewIndexSearch];
 
 [keyArray addObjectsFromArray:[[self.allNames allKeys] sortedArrayUsingSelector:@selector(compare:)]];
 self.keys = keyArray;
 [keyArray release];
 
}

// 비록 검색을 검색창 델리게이트 메서드에서 시작되지만 실제 아래 메소드가 검색을 담당한다.
// -(void)searchBarSearchButtonClicked:searchBar
// -(void)searchBar:textDidChange
-(void)handleSearchForTerm:(NSString *)searchTerm
{
 // 이 배열을 나중에 빈 섹션(검색 결과가 없는)을 지울때 사용한다.
 NSMutableArray *selectionsToRemove = [[NSMutableArray alloc] init];
 [self resetSearch];
 
 // 새로 복구한 keys 배열의 모든 키에 빠른 열거를 수행한다.
 for(NSString *key in self.keys)
 {
  // 매번 루프를 돌면서 현재 키에 해당하는 names배열을 구한다.
  NSMutableArray *array = [names valueForKey:key];
  // names 배열에서 지울 값들을 담은 배열을 선업한다.
  NSMutableArray *toRemove = [[NSMutableArray alloc] init];
  
  // 현재 배열의 전체 이름을 훑어보기 위해서 빠른 열거를 수행한다.
  for(NSString *name in array)
  {
   // NSString의 메서드 중 하나인 문자열 안의 부분 문자열의 위치를 리턴하는 메서드를 사용한다.
   // location과 length인 멤버 두개가 있는 NSRange struct인 rangeOfString을 사용한다.
   // 만약 섬색어가 없으면 location은 NSNotFound로 설정된다.
   // NSCaseInsensitiveSearch 옵션을 사용하여 대소문자 구분을 무시한다.
   // 검색된 결과가 없다면 추후에 지우기 위해서 지운는 배열에 추가한다.
   if([name rangeOfString:searchTerm options:NSCaseInsensitiveSearch].location == NSNotFound)
    [toRemove addObject:name];
  }
  
  // 만약 검색된 결과가 전부 없다면 해당 섹션을 삭제하기 위해서 배열에 추가해 놓는다.
  if([array count] == [toRemove count])
   [selectionsToRemove addObject:key];
  
  // 원래 값에서 검색 결과가 없는 배열을 삭제 한다.
  [array removeObjectsInArray:toRemove];
  [toRemove release];
 }
 
 // 필요없는 섹션을 삭제한다.
 [self.keys removeObjectsInArray:selectionsToRemove];
 [selectionsToRemove release];
 [table reloadData];
 
}

// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
- (void)viewDidLoad {
 
 NSString *path = [[NSBundle mainBundle] pathForResource:@"sortednames" ofType:@"plist"];
 
 NSDictionary *dict = [[NSDictionary alloc] initWithContentsOfFile:path];
 self.allNames = dict;
 [dict release];
 
 [self resetSearch];
 
 // 일반적으로 애플리케이션이 실행될 때는 사용자가 테이블을 눈으로 확인하기 전에 reloadData가 실행된다.
 // 그래서 호출할 필요는 없지만 setContentOffset를 호출하기 위해서는 테이블의 데이터들이 올바르게
 // 로딩된 상태인지를 반드시 확인해야 하기 때문에 reloadData를 호출한다.
 // reloadData가 호출되면 데이터들이 로딩되었다는 것을 보장한다.
 [table reloadData];
 // 테이블의 컨텐츠를 검색창의 높이인 44픽셀만큼 제외하고 계산하다. 그래서 첨에는 검색창이 보이지 않는다.
 [table setContentOffset:CGPointMake(0.0f, 44.0f) animated:NO];
 
    [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 {
 // Release any retained subviews of the main view.
 // e.g. self.myOutlet = nil;

 self.table = nil;
 self.search = nil;
 self.allNames = nil;
 self.names = nil;
 self.keys = nil;
 
}


- (void)dealloc {
 [table release];
 [search release];
 [allNames release];
 [keys release];
 [names release];
    [super dealloc];
}

#pragma mark -
#pragma mark Table View Data Source Methods
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
 // 테이블 뷰는 최소한 하나의 섹션을 가져야 하기 때문에 검색된 결과가 하나도 없을수 있기 때문에
 // 검색된 결과가 없을때는 section의 값을 1개로 보장한다.
 return ([keys count] > 0) ? [keys count] : 1;
}

-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
 if([keys count] == 0)
  return 0;
 
 NSString *key = [keys objectAtIndex:section];
 NSArray *nameSection = [names objectForKey:key];
 
 return [nameSection count];
 
}

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
 NSUInteger section = [indexPath section];
 NSUInteger row = [indexPath row];
 
 NSString *key = [keys objectAtIndex:section];
 NSArray *nameSection = [names objectForKey:key];
 
 static NSString *SectionTableIdentifier = @"SectionTableIdentifier";
 
 UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:SectionTableIdentifier];
 
 if(cell == nil)
  cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:SectionTableIdentifier] autorelease];
 
 cell.textLabel.text = [nameSection objectAtIndex:row];
 return cell;
 
}

// 각섹션에 헤더값을 설정할수 있는 메서드
-(NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
 if([keys count] == 0)
  return nil;
 
 NSString *key = [keys objectAtIndex:section];
 // 섹션 헤더 제목을 나타날때 특정 값을 숨겨야 한다.
 // UITableViewIndexSearch 값이 들어온다면 nil 값을 리턴한다.
 if(key == UITableViewIndexSearch)
  return nil;
 
 return key;
}

// 테이블뷰 옆에 인덱스를 추가한다.
-(NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView
{
 // 사용자가 검색창을 사용하고 있다면 nil를 리턴한다.
 if(isSearching)
  return nil;
 
 return keys;
}

#pragma mark -
#pragma mark Table View Delegate Methods
// 사용자가 행을 선택할때 선택되기 전에 미리 호출된다.
-(NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
 // 사용자가 행을 선택하면 검색창에 대한 키보드를 없앤다.
 [search resignFirstResponder];
 isSearching = NO;
 search.text = @"";
 [tableView reloadData];
 return indexPath;
}

#pragma mark -
#pragma mark Search Bar Delegate Methods
// 사용자가 리턴이나 키보도의 search 키를 탭하면 호출되는 메서드
// 검색창을 사용할때는 항상 구현해야 하는 메서드 이다.
-(void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
{
 NSString *searchTerm = [searchBar text];
 [self handleSearchForTerm:searchTerm];
}

// 사용자가 검색창에 새로운 검색어를 입력할때마다 검색을 진행한다.
-(void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchTerm
{
 // 검색어가 없을경우 초기화를 하고 데이터를 다시 로드한다.
 if([searchTerm length] == 0)
 {
  [self resetSearch];
  [table reloadData];
  return;
 }
 
 [self handleSearchForTerm:searchTerm];
 
}

// 사용자가 cancel을 클릭했을 때 호출되는 메서드
-(void)searchBarCancelButtonClicked:(UISearchBar *)searchBar
{
 // 사용자가 검색이 마치면 isSearching을 NO로 설정하여 테이블뷰에 인덱스가 나오게 한다.
 isSearching = NO;
 // 검색창을 초기화 시키고 키보드를 삭제 시킨다.
 search.text = @"";
 [self resetSearch];
 [table reloadData];
 [searchBar resignFirstResponder];
}

// 검색이 시작되면 isSearching에 YES를 할당하도록 설정
-(void)searchBarTextDidBeginEditing:(UISearchBar *)searchBar
{
 isSearching = YES;
 [table reloadData];
}

// 테이블 뷰 옆에 인덱스가 선택된다면 호출되는 메서드
-(NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index
{
 NSString *key = [keys objectAtIndex:index];
 // 만약 돋기기 라면
 if(key == UITableViewIndexSearch)
 {
  // 컨텐츠의 오프셋을 옆에야 한다. 그리고 NSNotFound를 리턴한다면 화면 상단으로 이동한다.
  [tableView setContentOffset:CGPointZero animated:NO];
  return NSNotFound;
 } else 
  return index;
}

@end
 
 

 

 



 
 
 

'개발 > App Developer' 카테고리의 다른 글

멀티뷰 애플리케이션  (0) 2010.08.30
탭바와 피커  (0) 2010.08.30
리눅스 쬐금  (0) 2010.08.30
tableView 의 클래스와 프로토콜 이해  (0) 2010.08.29
UITableViewDataSource 프로토콜의 주요 메서드  (0) 2010.08.29