본문 바로가기
  • SDXL 1.0 + 한복 LoRA
  • SDXL 1.0 + 한복 LoRA
Development/iPhotoDiary(BabyPhotoDiary)

[옛 글] [실전 소스 분석] 8. 테이블 뷰 컨트롤러 써보기 - 1

by 마즈다 2013. 7. 18.
반응형

최초 작성일 : 2010/10/11 00:58 


1. 네비게이션 아이템을 통한 화면의 이동


오늘은 잡설 없이 바로 본론으로 들어갑니다…^^;;;


지난 시간 보았던 메인 화면 상단의 navigation controller에는 좌측과 우측에 각각 버튼이 하나씩

있습니다. 좌측의 버튼은 등록된 아이들의 리스트를 보고 수정 및 삭제를 할 수 있는 화면으로

이동하는 버튼이고 우측의 버튼은 새로 아이를 등록하는 화면으로 이동하는 버튼입니다.


네비게이션 컨트롤러에 별다른 커스터마이징을 하지 않았다면 대체로 이 버튼들은

UIBarButtonITem의 인스턴스들이고 이 인스턴스를 생성하는 메서드들에는 이벤트와

이벤트 발생시 수행되는 메서드(@selector로 지정되는)를 설정할 수 있도록 되어있습니다.

(다만 기본 생성 메서드인 initWithBarButtonSystemItem에는 이벤트는 별도로 지정하지

않고 selector만 지정합니다.)


iPhotoDiary의 메인 화면에서는 다음과 같이 2개의 버튼을 붙였습니다.


UIBarButtonItem *addButton = [[UIBarButtonItem alloc]

initWithBarButtonSystemItem:UIBarButtonSystemItemAdd

target:self 

action:@selector(addChild)];

self.navigationItem.rightBarButtonItem = addButton;

[addButton release];

UIBarButtonItem *editButton = [[UIBarButtonItem alloc]

initWithBarButtonSystemItem:UIBarButtonSystemItemEdit 

target:self 

action:@selector(editChild)];

self.navigationItem.leftBarButtonItem = editButton;

[editButton release];


그리고 각 버튼의 selector들은 다음과 같습니다.


- (void)addChild {

AddNewChildViewController *nextViewController =

[[AddNewChildViewController alloc

 initWithManagedObjectContext:managedObjectContext];

nextViewController.hidesBottomBarWhenPushed = YES;

[self.navigationController pushViewController:nextViewController

   animated:YES];

}


- (void)editChild {

childList = [[ChildListViewController alloc]

 initWithManagedObjectContext:self.managedObjectContext];

childList.hidesBottomBarWhenPushed = YES;

[self.navigationController pushViewController:childList 

   animated:YES];

}


너무도 평범한 내용이라서 굳이 글로 옮기는 것 자체가 낭비네요…^^;;;

다만 초반에도 제가 말씀드린대로. 네비게이션 컨트롤러의 구성을 잘 못한 경우 네비게이션 컨트롤러의

기본적인 기능은 동작하지만 childList.hidesBottomBarWhenPushed = YES;이 문장이

적용되지 않는 경우가 있습니다. 다른 방법으로 하단의 탭바를 감출 수는 있지만 작업이 복잡해지죠.

탭바의 숨김과 보여짐을 좀더 쉽게 처리하기 위해서는 IB를 통해 네비게이션 컨트롤러를 구성하는 것이

좀 더 쉬운 방법이라 판단됩니다.

참고 링크 : [실전 소스 분석] 2. 프로젝트 생성과 메인 화면 구성


또, 이미 잘 알고 계시는 바와 같이 네비게이션 컨트롤러를 통해 움직이는 뷰들은 계층 구조를

가지고 있으며 이 것은 메모리 구조상의 stack 형태를 가지고 있습니다. 즉 가장 나중에

push된 것이 가장 상위에서 사용자에게 보여지게 되는 것입니다. 그리고 가장 나중에

push된 것부터 먼저 pop되어 stack에서 빠져나가면 그 바로 아래에 있던 view가 사용자게에

보여지게 됩니다. 메서드명에 push와 pop이라는 단어가 들어간 것으로도 이미 짐작을

하셨을 것입니다.


이렇게 해서 메인 화면에 보여질 데이터를 등록하는 화면과 등록된 데이터를 수정/삭제할 수 있는

화면으로 이동이 가능하게 되었습니다.


나의 실수


처음에 뷰 컨트롤러의 인스턴스를 생성하여 그 인스턴스를 pushViewController에 사용하였는데

자꾸 에러가 발생하는 것이었습니다. 나중에 알고보니 습관적으로 뷰 컨트롤러의 init 메서드를

사용하는데 기본 init메서드인 initWithNibName:…를 사용해서 문제가 된 것이었습니다.


IB에 대해서도 앞서 언급을 했지만 간혹 IB를 통해 xib 파일을 만들었다가 굳이 xib가 필요치 않아

삭제를 하거나 혹은 디자인 설계 용도로만 사용하고 삭제를 하는 경우가 있었습니다.

이 때 xib 파일이 있으니까 initWithNibName:…메서드를 통해 뷰 컨트롤러의 인스턴스를

만들었다가 xib 파일을 지우고나니 이 xib 파일을 찾지 못해  initWithNibName:…에서 에러를

낸 것이었습니다.


저는 초짜라 그런지 꽤 자주 겪은 실수였습니다…^^;;;



2. 테이블 뷰 컨트롤러의 사용


대부분의 애플리케이션에서 설정 화면이나 로그인 또는 등록 화면에 테이블 뷰(컨트롤러)를 많이

사용합니다. 아마도 테이블 뷰가 입력 항목이 많아졌을 때 아래 목록까지 스크롤을 시킨다든가

하는 부분에서 수고를 덜 수 있고 또 비슷한 입력 항목끼리 그룹화하기도 편하기 때문이 아닌가

싶습니다.


저같은 경우 사실은 입력 항목이 많지 않아 스크롤 될 필요도 없고 또 오히려 테이블 뷰를

사용하는 과정에서 문제가 발생하여 굳이 테이블 뷰를 사용하지 않아도 될뻔했습니다.

거의 '테이블 뷰 컨트롤러에 대해 공부하겠다'는 생각만으로 테이블 뷰 컨트롤러를 선택하게

되었습니다…^^;;;


일단 화면 구성은 아래와 같습니다.






그룹 타입의 테이블이고 3개의 그룹으로 구성되어 있으며 첫 그룹은 1개의 커스텀 셀로 되어

있습니다.


여기까지만 봐도 커스텀 셀을 만드는 방법, 또 2개의 셀에는 segment 컨트롤이 들어있는데

이렇게 셀에 컨트롤을 넣는 방법, 그리고 이 화면은 나중에 수정 화면으로도 같이 사용되는데

그랬을 경우 Core Data와 연계하여 사용하는 방법, 테이블 뷰에 배경 이미지 넣는 방법 등

할 이야기가 많지만 이 내용들은 다음 절로 넘기고 여기서는 테이블 자체를 만드는 내용만

살펴보도록 하겠습니다.


먼저 테이블 뷰 컨트롤러를 바로 만들었을 경우에는 기본적으로 구현되어있는 메서드들이

몇가지 있습니다. 먼저 다음 3개의 메서드가 있습니다.


- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView

- (NSInteger)tableView:(UITableView *)tableView 

numberOfRowsInSection:(NSInteger)section

- (UITableViewCell *)tableView:(UITableView *)tableView 

cellForRowAtIndexPath:(NSIndexPath *)indexPath


그리고 이 메서드들이 선언된 앞쪽에는  pragma mark에 Table view data source라고

적혀있습니다. 네 바로 데이터와 연동되어 테이블의 구조를 만들어내는 메서드들입니다.

물론 데이터는 코어 데이터일 수도 있고, 단순 배열일 수도 있고, Dictionary일 수도 있고

그렇습니다.


각각 차례대로


- 테이블의 섹션 갯수를 설정하는 메서드

- 각 섹션의 Row 수를 가져오는 메서드

- 각 Row의 TableViewCell을 설정하는 메서드입니다.


이번에 다루고 있는 화면 같은 경우 동적으로 변하는 구조가 아니라 섹션과 Row 수가

등록할 입력 값의 수로 고정되어있는 화면이라서 몇몇 곳에서는 리턴값으로 바로

정수형 상수를 사용하고 있습니다.


예를 들어 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView

경우에는 return 3;이 코드의 전부입니다.


물론 numberOfRowsInSection메서드의 경우에도 3개의 섹견 각각에 몇개의 Row가 필요한지

이미 결정되어있는 상태라 if문을 통해 두번째 인자로 넘어오는 section 값을 확인하여

각 섹션에 맞는 Row 수를 상수로 리턴하고 있습니다.


// Return the number of rows in the section.

    NSInteger rows = 0;

    

    /*

     The number of rows depends on the section.

     In the case of ingredients, if editing, add a row in editing mode to present an "Add Ingredient" cell.

 */

    switch (section) {

        case BASIC_SECTION:

rows = 1;

break;

        case BODY_SECTION:

            rows = 2;

            break;

        case BORN_SECTION:

rows = 3;


          break;

default:

            break;

    }

    return rows;


여기에서 대문자로 적혀있는 것들은 매크로로 지정해놓은 section값입니다. 차례대로 0,1,2입니다.


나의 후회


물론 저는 처음이고 공부를 하면서 개발을 하던 상태였기에 이렇게 코딩을 하였지만

나중에 이 코드를 재사용한다는 측면에서도 상수를 바로 사용하는 것은 좋지 못한 습관이겠죠^^;;


가급적이면 변수를 사용하여 나중에 이 코드에 동적인 데이터를 사용하더라도 수정하게되는

범위가 줄어들 수 있도록 하는 것이 좋을 것입니다.


여기까지는 별게 없는데요…마지막  cellForRowAtIndexPath 메서드가 많이 복잡해졌습니다.

일단 각 섹션별로 Row의 수가 틀리고 첫번째 섹션과 나머지 섹션이 각각 서로 다른

커스텀 셀들을 사용하고 있습니다. 그리고 각각의 타이틀이 틀리고 어떤 셀에는 컨트롤이

포함되어있습니다. 어떤 식으로 구성하였는지 소스에 주석을 다는 형식으로 살펴보도록

하겠습니다.


일단 기본적인 커스텀 셀은 2가지입니다. ChildBasicInfoCell과 ChildInfoCell인데요

이 때까지만 해도 제가 테이블 셀에 컨트롤을 넣는 방법을 제대로 몰라서  segment 컨트롤이

들어가 별도의 커스텀 셀을 하나 더 만들었습니다. ChildGenderInfoCell이 그것입니다.


이 메서드이 내용은 이렇게 3가지 형태를 갖고 있는 커스텀 셀들을 적절한 위치에 배치시키는

것입니다.


if (indexPath.section == BASIC_SECTION) {

//먼저 첫번째 섹션의 경우 처리입니다.

static NSString *CellIdentifier = @"ChildBasicInfo";

    

//UITableViewCell이 아니라 커스텀 셀의 인스턴스를 만듭니다.

ChildBasicInfoCell *cell = (ChildBasicInfoCell *)[tableView 

dequeueReusableCellWithIdentifier:CellIdentifier];

if (cell == nil) { //만일 재사용할 셀이 없을 경우의 처리죠…

//이 부분부터 특이해지는데요 메인 번들을 통해 Nib 파일 이름을 통해 커스텀 셀들의

   배열을 가지고옵니다.

NSArray *nib = [[NSBundle mainBundle]

loadNibNamed:@"ChildBasicInfoCell" 

owner:self 

options:nil];

//가지고 온 커스텀 셀들의 배열을 for문을 돌려서...

for (id oneObject in nib) {

//이 섹션에서 사용할 커스텀 셀과 같은 클래스이면…작업을 합니다.

if ([oneObject isKindOfClass:[ChildBasicInfoCell class]]) {

cell = (ChildBasicInfoCell *) oneObject;

cell.delegate = self;

//이 if문은 이 화면이 수정 화면으로 쓰일 경우의 처리입니다.

                                   //기존 데이터를 해당 셀에 대입하는 것이죠.

if (childData != nil) {

cell.nameField.text = childData.childname;

cell.birthDay.text =

[self.dateFormatter 

stringFromDate:childData.birthday];

cell.birthDate = childData.birthday;

cell.isLunar.selectedSegmentIndex = [[childData 

valueForKey:@"islunar"integerValue];

CGSize size = childData.thumbnailImage.size;


CGFloat ratio = 0;


//화면에 보여줄 이미지의 크기를 조절합니다.

if (size.width > size.height) {

ratio = 116.0 / size.width;

else {

ratio = 116.0 / size.height;

}

CGRect rect =

CGRectMake(0.0,

   0.0,

   ratio * size.width,

   ratio * size.height);

UIGraphicsBeginImageContext(rect.size);

[childData.thumbnailImage drawInRect:rect];

[cell.photoViewButton 

setImage:

UIGraphicsGetImageFromCurrentImageContext()

forState:

UIControlStateNormal];

UIGraphicsEndImageContext();

}

childBasicInfoCell = cell;

}

}

}

return cell;

else { //섹션이 두번째나 세번째인 경우

//세번째 섹션의 2번째 Row의 경우 segment 컨트롤을 넣기 위해 별도의 커스텀 셀로

  //만들었습니다,

if (indexPath.section == BORN_SECTION && indexPath.row == 2) {

static NSString *CellIdentifier = @"childGenderInfo";

//성별을 입력하는 부분은 segment 컨트롤을 사용하고 있어 별도의 커스텀 셀을

//만들었습니다.

ChildGenderInfoCell *cell = (ChildGenderInfoCell *)[tableView

dequeueReusableCellWithIdentifier:CellIdentifier];

if (cell == nil) { //역시 재사용할 셀이 없는 경우...

NSArray *nib = [[NSBundle mainBundle]

loadNibNamed:@"ChildGenderInfoCell" 

owner:self 

options:nil];

for (id oneObject in nib) {

//사용할 커스텀 셀과 같은 클래스인 경우

if ([oneObject

 isKindOfClass:[ChildGenderInfoCell class]]) {

cell = (ChildGenderInfoCell *) oneObject;

cell.delegate = self;

cell.labelField.text = @"";

}

}

}

//셀 타이틀을 지정합니다.

cell.labelField.text = [self.cellTitle2 

objectAtIndex:indexPath.row];

//역시 수정 화면으로 사용될 경우 값을 대입하게 됩니다.

if (childData != nil) {

cell.genderControl.selectedSegmentIndex =

[[childData valueForKey:@"gender"integerValue];

}

return cell;

else {

//두번째 섹션 또는 세번째 섹션은 Row 1, 2인 경우

static NSString *CellIdentifier = @"ChildInfo";

ChildInfoCell *cell = (ChildInfoCell *)[tableView 

dequeueReusableCellWithIdentifier:CellIdentifier];

//ChildInfoCell은 타이틀을 위한 Label 하나와 값을 입력할 TextField 하나로

            //구성된 커스텀 셀입니다.

if (cell == nil) {

NSArray *nib = [[NSBundle mainBundle]

loadNibNamed:@"ChildInfoCell" 

owner:self 

options:nil];

for (NSInteger i = 0; i < [nib count]; i++) {

id oneObject = [nib objectAtIndex:i];

if ([oneObject isKindOfClass:[ChildInfoCell class]]) {

Debug(@"cell = ChildInfoCell");

cell = (ChildInfoCell *) oneObject;

cell.delegate = self;

cell.labelField.text = @"";

cell.valueField.text = @"";

}

}

}

//각각의 셀에 타이틀을 설정합니다. 타이틀은 cellTitle1과 cellTitle2라는

//배열에 저장되어있습니다.

//

//self.cellTitle1 = (NSArray *)[NSArray arrayWithObjects:

// @" : ", @"몸무게 : ", nil];

//self.cellTitle2 = (NSArray *)[NSArray arrayWithObjects:

// @"주민번호 : ", @"출생지 : ",

// @"성별 : ", nil];

if (indexPath.section == BODY_SECTION) {

cell.labelField.text = [self.cellTitle1 

objectAtIndex:indexPath.row];

else {

cell.labelField.text = [self.cellTitle2 

objectAtIndex:indexPath.row];

}

//수정 화면으로 사용될 경우 Core Data에서 불러온 데이터를 설정하게 됩니다.

if (childData != nil) {

if (indexPath.section == BODY_SECTION) {

if (indexPath.row == 0) {

cell.valueField.text = [childData.height 

stringValue];

else if (indexPath.row == 1) {

cell.valueField.text = [childData.weight 

stringValue];

}

else {

if (indexPath.row == 0) {

cell.valueField.text = childData.idnum;

else if (indexPath.row == 1) {

cell.valueField.text = childData.location;

}

}

}


//마지막으로 숫자만 입력받을 셀에 대해서는 키보드 타입을 따로 설정해주었습니다.

if (indexPath.section == BODY_SECTION ||

(indexPath.section == BORN_SECTION && indexPath.row == 0)) {

cell.valueField.keyboardType =

UIKeyboardTypeNumbersAndPunctuation;

else {

cell.valueField.keyboardType = UIKeyboardTypeDefault;

}


return cell;

}


}


이상 실제 소스를 살펴보았지만 번잡스럽게 커스텀 셀이 3종류나 되어 실제 Row 수가 6개인 것을

감안하면 코드가 필요이상으로 복잡해졌습니다. 조금 만 더 다듬으면 코드는 더 짧아질 것입니다.


해결하지 못한 문제점…ㅠ.ㅠ


일단 이렇게 구성을 하고 테이블 뷰가 스크롤 가능하도록 처리를 했습니다.

그런데 좀 이상한 문제가 발생을 하였습니다.


스크롤을 몇 번 하고나면 셀의 위치가 바뀌어 있는 것입니다.

예를들어 섹션 1의 0번째 Row가 주민번호인데 이것이 섹션 2의 2번째 Row에 표시되는

그런 문제였습니다.


우선 데이터 소스 관련 3개의 메서드는 스크롤이 발생할 때마다 계속 수행이 되는 것을 확인해본 바

일단 심증으로는 셀을 계속 재사용하는 과정에서 셀을 불러올 때 처음 정해진 순서대로

셀이 반환지 않는 것이 아닐까 하는 점입니다.


어쨌든 이 부분은 아직도 해결을 하지 못해서 일단 테이블을 스크롤이 되지 않도록

고정시켜놓은 상태입니다. 혹시 제 소스 코드를 보시고 문제가 있을법한 부분이 있다면

수정 부탁드립니다…^^;;;


이번 주는 유독 피곤하기도 하고 또 신규로 개발 중인 앱의 진행이 원활하지 못해서 일단

이번 주는 여기서 글을 맺을까 합니다.

길고 평범한 글 읽어주셔서 감사합니다. 다음 주에 아래 내용으로 다시 찾아뵙겠습니다.


3. 커스텀 셀

4. 셀에 컨트롤 넣기

5. 테이블 뷰의 배경

6. 등록과 수정 화면 공유

7. 테이블 뷰의 스크롤

8. 요약

9. 마무리


반응형