본문 바로가기

개발도구/iOS - 아이폰 개발

[펌-아이폰-비밀] TableView

링크 펌: http://j2enty.tistory.com/82


 안드로이드 폰이나 아이폰, 그 외에도 블랙베리, 윈모, 바다 ... 그 외에 모든 모바일, 임베디드 기기에서 가장 중요한 컴퍼넌트가 어떤건가 하면 바로 이 테이블뷰(안드로이드에선 리스트뷰)라고 할 수 있을 것입니다. 바로 많은 양의 데이터를 사용자에게 보여줄 때 사용되는 것이 테이블뷰이기 때문입니다. 
 실제로 안드로이드 마켓이나 앱스토어에 등록된 앱, 이들을 떠나 애초에 아이폰에 이미 내장되어 있는 앱들을 보면 대부분 테이블뷰를 사용하게 됩니다. 아이폰의 경우를 살펴보면 기본으로 제공하는 연락처, 메시지 모두 테이블뷰를 이용해서 구현되어 있습니다. 가장 흔하게 사용하는 카카오톡의 경우를 예를 들어도 친구리스트 역시 테이블뷰 이고 대화창 역시 테이블뷰로 구현되었습니다. 이 처럼 임베디드 특히 모바일 기기에서 가장 중요하다고 할 수 있는 테이블뷰가 어떻게 작동하는지에 대해서 살펴보려 합니다.

 그에 앞서 작년 이맘 때 처음 안드로이드를 공부할 때 리스트뷰를 이해하려고 오랜 시간이 걸렸습니다. 안드로이드에서 리스트뷰를 구현하는데는 처음에는 엄청 오랜 시간이 걸렸었습니다. 그리고 그 작업을 이해하는데도 오랜 시간이 걸렸었습니다. 
 그 경험 후에 아이폰을 공부하는 지금은 iOS SDK에 감사하고 있습니다. 그 만큼 이해하기 쉽고 사용하기 쉽게 잘 구성되어 있기 때문입니다.

 잡소리를 다 치우고 이제 '무척이나 쉬운' 테이블뷰에 대해서 살펴보겠습니다. 오늘 포스팅의 근거 자료는 대부분 '스탠포드 iPhone강의'에서 가져왔습니다.

1. UITableView
 -. 테이블뷰는 리스트를 통해 데이터를 보여주는데 매우 중요한 역할을 하는 클래스입니다.  
  : 테이블뷰는 스크롤뷰의 하위 클래스 입니다. 
  : 두 개의 델리게이트를 이용하여 원하는 용도에 맞도록 다양하게 활용이 가능합니다.
    - DataSource : UserInterface를 다루는데 사용
    - Delegate : Data를 불러오는데 사용
 -. 한번에 하나의 Data Column을 보여줄 수 있습니다.
  : 종종 테이블뷰는 다른구조의 데이터를 뷰에 표현 해야 할 때가 있다.
  : 또한 이렇게 컬럼으로 나눔으로써 Section으로 구분 지을 수 있으며 이는 사용자에게 좀 더 쉬운 UserInterface가 된다.
 -. 이런 테이블뷰는 2개의 스타일이 있다.
 : UITableViewStylePlain / UITableViewStyleGrouped 


 : Plain Style TableView의 구조


 : Grouped Style TableView의 구조


 : 그렇다면, 테이블뷰는 어떠한 방식으로 데이터를 가져오는가?
  - 이에 대한 정답은 'Delegate' 이다. 그 중에도 DataSource Delegate를 이용한다. 
 : 데이터를 어떻게 가져오는지 알았다. 그럼 그 데이터를 한꺼번에 다 가져오는 것은 어떠한가?
  - 이는 매우 비효율적인 방법이라고 할 수 있다.
  - 때문에 테이블뷰는 현재 필요한 데이터가 어떤 것인지 알고 그것만을 요청한다. 이는 테이블뷰의 Row가 데이터를 표시하려고 할때, 즉 현재 필요한 데이터에 한에서만 불러온다는 것을 의미한다.
 : 하지만 테이블뷰는 총 데이터의 길이(크기)를 알고 있어야 한다
  - 테이블뷰는 스크롤뷰의 서브클래스이다. 이는 스크롤이 얼마나 되어야 하는지 스크롤뷰는 알고 있어야 한다는 것을 의미하고, 적어도 테이블뷰는 스크롤이 얼만큼 되야 하는지를 알 수 있는 데이터의 크기는 알고 있어야한다.
 : 때문에 테이블뷰를 그리기 위해서 가장 먼저 호출되는 메소드의 역할은 데이터의 크기가 얼마인지를 알아내는 것이다.
  - 실제로 첫번째 호출되는 메소드는 얼마나 많은 Section을 가지고 있는가, 혹은 얼마나 많은 Row를 가지고 있는가를 알아내는 역할을 한다.
 : 그 후에 DataSource는 테이블뷰에게 실제 데이터를 전달하기 시작한다. (단, 데이터가 필요한 Row가 나타날 때)

2. TableView DataSource / Delegate
 이 두개를 설명하기 앞서 설명할 것이 몇가지 있다. (여기부터는 개인적인 지식)
 테이블뷰를 구성하는데는 크게 2개의 Delegate로 나눠집니다. 하나는 UITableViewDataSource이고 다른 하나는 UITableViewDelegate입니다. 이름이 DataSource라고 해서 다른 종류의 것이 아니라 이 역시 Delegate라는 것을 알아야 합니다. 그렇다면 이렇게 귀찮게(?) 2개로 구분지은 이유는 무엇인가 생각해볼 수 있겠죠. 
 위에도 짧게 코멘트가 있지만 DataSource는 테이블뷰에 필요한 실질적인 데이터의 제공을 담당하는 Delegate입니다. 그리고  Delegate는 사용자와의 대화, 즉 UserInterface를 담당하죠. 이렇게 크게 역할에 따라 구분하여 혼란이 없도록 한 것같습니다. 그럼 이제 두 가지의 Delegate를 구분지어서 알아보겠습니다.

 2.1 TableView DataSource
  -. 설명하기에 앞서 테이블뷰와 DataSource가 어떤 대화를 통해 화면을 구성해 가는지 알아보겠습니다.
  Step1. 초기상태(현재 테이블뷰 데이터가 표시되어있지만 아무것도 없다고 생각하자)


  Step2. 테이블뷰가 데이터소스에게 얼마나 많은 Section을 가지고 있는 데이터인지 묻는다. 


  Step3. 데이터소스는 테이블뷰에게 몇개의 Section이 있는지 대답해준다.


Step4. 이번에는 0번 Section에는 몇개의 Row가 있는지 테이블뷰가 데이터 소스에게 묻는다.


Step5. 데이터소스는 0번 Section에는 1개의 Row가 있다고 테이블뷰에게 알려준다.


  Step6. 테이블뷰는 이제 데이터의 크기는 알아냈다. 이제부터는 실제로 어떤 데이터가 필요한지 궁금해져서 데이터소스에게 다시 묻는다. "나 이제 5개 Section이 있는것도 알고 각 Section에 몇개의 Row가 들어가는지 알아. 이제 0번 Section의 0번 Row를 표시할라고 그러는데 여기에 들어있는 데이터가 뭐야" 


  Step7. 데이터 소스는 0번 Section과 0번 Row에는 'John Appleseed'가 들어있다고 테이블뷰에게 알려준다. 그럼 테이블뷰는 그 데이터를 해당 Row(Cell)에 표시하게 된다.


  : 위의 순서대로 데이터소스와 테이블뷰의 대화가 진행되면서 실제 데이터를 테이블뷰에 표현하게 됩니다. 

  -. TableView DataSource 메소드들

 DataSource Methods (9개)

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
 : 표현할 데이터의 총 Section의 갯수. (2차원 배열의 경우)

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
 : 표현할 데이터에서 하나의 Section에 있는 총 Row의 갯수. (1차원배열의 경우엔 Section이 아닌 Array에 들어있는 총 Object의 갯수)

- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section
 : Section의 헤더 

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
 : Section의 풋터 

- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index
 :  

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
 : 실제 하나의 셀마다에 값을 넣어주는 부분 

- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
 : 삽입/삭제를 이용할지 안할지를 정의하는 부분 

- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath
 : Row의 이동을 허용할지 안할지를 정의하는 부분 

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
 : 테이블뷰의 삽입/삭제 작업 후에 호출 됨

- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath
 : 테이블뷰의 이동작업 후에 호출 됨 

 : 위의 메소드들 중 테이블뷰의 삽입/삭제/이동에 대한 설명은 후반부에 다시 다루겠습니다.

 2.2 TableView Delegate
  -. Delegate Methods

Delegate Methods (21개)

- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section
 : Footer 높이 

- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
 : Header 높이 

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
 : Row 높이 
 

- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section
 : DataSource의 String형 헤더가 아닌 커스팀가능한 TableView Footer를 만들 때 사용 

- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section

 : DataSource의 String형 헤더가 아닌 커스팀가능한 TableView Header를 만들 때 사용
 

- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath
 : 테이블뷰의 Row를 클릭했을 때 호출 

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
 : tableView:willSelectRowAtIndexPath가 호출 된 후 호출
 

- (NSIndexPath *)tableView:(UITableView *)tableView willDeselectRowAtIndexPath:(NSIndexPath *)indexPath
 : 테이블뷰의 선택을 해지할 때 호출 

- (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath
 : tableView:WillDeselectRowAtIndexPath가 호출 된 후 호출
 

- (void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath
 : TableView Cell의 오른쪽에 붙는 AccessoryButton을 클릭했을 때 호출 

- (UITableViewCellAccessoryType)tableView:(UITableView *)tableView accessoryTypeForRowWithIndexPath:(NSIndexPath *)indexPath
 : Accessory의 종류를 변경할 때 사용
 

- (void)tableView:(UITableView *)tableView willBeginEditingRowAtIndexPath:(NSIndexPath *)indexPath
 : 테이블뷰의 수정을 시작할 때 호출 

- (void)tableView:(UITableView *)tableView didEndEditingRowAtIndexPath:(NSIndexPath *)indexPath
 : 테이블뷰의 수정을 종료할 때 호출
 
- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath
 : 테이블뷰의 EditStyle(삽입/삭제)에 대해서 정의할 때 사용 

- (BOOL)tableView:(UITableView *)tableView shouldIndentWhileEditingRowAtIndexPath:(NSIndexPath *)indexPath
 : 테이블뷰가 edit상태 일 때 Cell의 앞쪽에 들여쓰기를 사용할지 안할지를 정의할 때 사용 

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
 : 테이블뷰가 다 구성되고 사용자에게 보여지기 직전에 마지막으로 호출되는 메소드 


- (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender

- (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender


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

- (NSIndexPath *)tableView:(UITableView *)tableView targetIndexPathForMoveFromRowAtIndexPath:(NSIndexPath *)sourceIndexPath toProposedIndexPath:(NSIndexPath *)proposedDestinationIndexPath

- (NSString *)tableView:(UITableView *)tableView titleForDeleteConfirmationButtonForRowAtIndexPath:(NSIndexPath *)indexPath 


 위의 Delegate메소드는 21개가 될 만큼 다양한 상황에 맞게 사용할 수 있도록 제공되고 있습니다. 다만 여기서 의문이  DataSource 메소드 중에 NSString 을 리턴타입으로 갖는 헤더/풋터의 타이틀을 지정하는 메소드가 있었습니다. 근데 Delegate 메소드 중에도 헤더/풋터를 지정하는 메소드가 또 있습니다. 단 이 메소드의 리턴 타입은 UIView 입니다. 리턴타입이 다를 뿐 결국은 같은 역할을 하는 메소드가 왜 각기 다른 Delegate에 정의되어있는지는 이번에 저도 처음 알았고 의문스럽네요;; 
 또한 Delegate 메소드가 21개나 되는지 몰랐습니다;; 그래서 아직 모르는 기능들이 있는데 코멘트를 달지 못한 메소드들은 추후에 업데이트 하도록 하겠습니다.

3. TableViewCell
 -. TableViewCell은 무엇인가?
  : UIView를 상속받는 클래스로 TableView의 Row를 그리는데 사용되는 클래스입니다.
 -. iOS SDK에서 제공하는 기본적인 4가지 TableViewCell Style은 아래와 같습니다.

 
 -. 이 외에도 필요에 따라서 얼마든지 Cell의 Item들을 구성할 수 있습니다. (이 내용은 다음에 다루도록 하겠습니다. - TableViewCell Custom)
 -. 'reuse Identifier'는 무엇인가?
  : 아이폰을 개발하다가 보면 테이블뷰에서 셀을 그릴 때나 맵뷰에서 핀을 그릴 때 보면 reuse identifier라는 것이 있습니다.
  : 이것이 무엇인가에 대한 결론을 먼저 말하면 '퍼포먼스' 를 위한 것입니다.
  : 예를 들어 표시해야할 데이터가 10000개라고 했을 때 10000개의 셀을 다 만든다는 것은 굉장히 비효율적입니다.    10000개가 아닌 사용자에게 보여지는 Cell의 갯수인 7~8개 (경우에 따라 다름)만 만들고 다시 사용하는 것이 효율적이며 이  를 위해서 구분짓는 것이 reuse identifier라는 것입니다. 
  : reuse identifier로 정의된 것들은 보여지는 화면에서 사라지면 Pile이라는 곳에 저장됩니다. (일종의 Queue) 그리고 다시 필   요할 때 Dequeue를 통해 꺼내와서 사용합니다. 이런 식으로 매번 필요할 때마다 메모리에 올리는 것이 아니라 재사용을 통     해서 퍼포먼스의 향상을 가져옵니다.

4. UITableView의 Editing and Reordering
 -. 아이폰을 사용하다보면 Swipe를 통해 해당 셀을 삭제하거나 혹은 상단의 네비게이션바에서 편집버튼을 통해 셀의 순서를 변경 할 수 있습니다. 혹은 새로운 셀을 추가할 수도 있죠. 아이폰에서는 이러한 것들을 Edit라는 큰 틀 하나에 넣는 것이아니라 두가지로 구분을 지어두었습니다. 바로 Editing과 Reordering입니다. Edit에 포함되는 작업은 삽입/삭제 이며 Reordering은 순서변경이라고 할 수 있습니다. 그럼 이렇게 구분을 지어둔 이유가 또 있겠죠. 그 명확한 이유는 모르겠으나 구현하다 보면 삽입/삭제의 경우에 필요한 작업은 2가지로 나눠집니다.
 1. 실 데이터인 Array에 원하는 값을 삽입/삭제
 2. TableView에 원하는 값을 삽입/삭제 
하지만 Reordering작업은 한가지 작업만 필요합니다.
 1. 실 데이터인 Array에 데이터 순서 변경
재정렬의 경우 사용자가 손으로 끌어다가 놓는 그 작업 자체가 삽입/삭제의 2번경우를 대신하는 일이기 때문에 개발하는 입장에서는 실데이터의 순서와 테이블뷰의 싱크만 맞춰주는 작업을 하면 됩니다.

그럼 각각 구현에 있어 어떠한 점을 유의해야하는지 알아보겠습니다.

 4-1. Editing
  -. 삽입/삭제를 관장하는 메소드는 tableview:commitEditingStyle:forRowAtIndexPath: 입니다. 이와 함께 작동하는 메소드로 BOOL형태의 리턴타입을 갖고 있는 tableview:canEditRowAtIndexPath: 가 있습니다. 
만약에 tableview:canEditRowAtIndexPath를 NO로 한다면 어떠한 경우에도 삽입/삭제가 되지 않습니다. 하지만 이 메소드를 아예 구현하지 않고 단지 tableview:commitEditingStyle:forRowAtIndexPath만 구현한다면 어떻게 될까요? 정답은 항상 삽입/삭제 작업이 가능해 집니다. 그 이유는 editing작업의 default는 YES이기 때문에 edit작업이 완료된 후에 어떠한 일을 할지를 정의하는 tableview:commitEditingStyle:forRowAtIndexPath: 메소드가 구현되어 있다면 삽입/삭제가 가능해 지는 것 입니다.
  -. 다른 주의할 점은 작업의 순서인데. 위에서 말한 순서를 지켜야 합니다. 여기서 말하는 순서는 테이블뷰에 먼저 데이터를 삽입/삭제하고 Array에 데이터를 삽입/삭제할 것인지 아니면 Array에 먼저 삽입/삭제를 하고 테이블뷰에 삽입/삭제를 할 것인가 입니다. 정답은 Array에 데이터를 삽입/삭제 하고 그 후에 TableView에 데이터를 삽입/삭제 하는 것입니다. 그 이유는 테이블뷰의 삽입/삭제를 관장하는 메소드인 deleteRowsAtIndexPaths:withRowAnimation: 와 insertRowsAtIndexPats:WithRowAnimation:은 이 메소드가 끝날 때 테이블뷰를 Reload합니다. 때문에 테이블뷰를 다시 불러오면서 현재 테이블뷰와 Array가 싱크가 맞지 않는 문제가 발생하고 바로 익셉션에러가 발생합니다. 때문에 이 순서를 꼭 기억해 두어야 할 것입니다.

 4-2. Reordering
  -. Reordering은 edit와 반대로 생각하면 쉽습니다. 먼저 reordering을 하기 위해서는 tableView:canMoveRowAtIndexPath: 에서 YES를 리턴해주어야 합니다. 왜냐하면 tableview:moveRowAtIndexPath:toIndexPath:의 default는 NO이기 때문입니다. 

----------------------------------------------------------------------------------------------------------------
  지금까지 해서 테이블뷰에 대해서 전반적으로 다루었습니다. 테이블뷰는 가장 중요한 컴퍼넌트 중에 하나인 만큼 사용하는데 있어서 불편함 없도록 적응하는데 중요하다고 생각됩니다. 테이블뷰만 잘 만들어서 넣어두어도 하나의 어플리케이션이 될 정도니깐요.. 
 다음에는 이 테이블뷰에 들어가는 Cell을 어떻게 내가 원하는 모양으로 커스텀 할 수 있는지에 대해서 알아보겠습니다.
 
 첨부하는 프로젝트에는 테이블뷰를 사용한 간단한 예제를 포함했습니다.
 1~50까지를 갖는 배열 10개를 테이블뷰에 표시하는 예제입니다.
 참고하시고 궁금한 내용이 있으시거나 잘못된 내용이 포함되어 있다면 댓글로 남겨주세요~


예제파일(다운로드)

TableViewExample.zip