본문 바로가기

개발/App Developer

UITableView - 델리게이트 구현이론(1)


 * 어플 개발에서 게임이 아닌 이상 테이블 뷰를 제일 많이 사용한다.

 

  * 이는 '맥부기카페 :http://cafe.naver.com/mcbugi.cafe?iframe_url=/ArticleRead.nhn%3Farticleid=6694 '에서 발췌한 내용으로

     IB없이 UITableViewController로 테이블 뷰를 구현하는 것이 아닌 UIViewController를 만들어 그 안에 구현하는 식을 설명한다

 

  * UITableView의 중요성을 인지하고 개념정리를 할려던 중 개념 정리에 좋은 컨텐츠인 것 같아 저장을 하여본다.

 

 

 

1. 새 프로젝트에서 Window Based Application 으로 프로젝트를 만든다.

 

 

2. 밑에 사진이 인터페이스 빌더 없이 만들어진 순수 코드이다.

    모든 어플은 처음에 '프로젝트명AppDelegate'로 만들어져 시작한다. (그전에 main 함수를 포함한다.)

 

 

 - 앱델리게이트만 있는 상태에서 화면에 뜰 뷰는 window라는 변수(UIWindow)로 지정되어 있다.

    그래서 window가 가장 최상위 뷰라 생각하면 된다. 나중에 뜰 뷰들은 전부 window 하위 뷰로 들어간고 생각하자.

 

3. 뷰 컨트롤러를 하나 만든다.

 

 

  - iPhoneOS > Cocoa Touch Classes > UIViewController subclass 선택

 

 

  - 내비게이션 컨트롤러와 이 컨트롤러가 시작 시에 보여줄 루트 뷰 컨트롤러를 생성한다.

     꼭 테이블 뷰를 루트 뷰로 만들 이유는 없지만 드릴다운(drill down)에 가장 적합한 구조이며

     커스터마이즈 하기 쉽기에 루트 뷰를 사용한다.(클래스명은 상관없음)

     * 이는 루트 뷰에 대한 설명이며 위의 class명은 보기 쉽기 위해 작성한 것이다.

 

 

 - RootViewController가 생성되었다. 이 객체를 사용하기 위해 앱 델리게이트를 연결해 보자.

 

4. 기본 제공되는 Delegate의 헤더 파일에 아래와 같이 선언해 주자.

 

 

  - 일단 RootViewController를 참조하기 위해 임포트를 시켜준다.

        #import "RootViewController.h"

  

   @interface testAppDelegate : NSObject <UIApplicationDelegate> {

RootViewController *RVC;  // 추가할 부분
UIWindow *window;

// 이 안에 testAppDelegate객체 안에서 사용될 클래스 변수를 적는곳입니다. 

      }

 

      이미 기본적으로 window 변수가 이미 정의되어 있다.

      이 곳에  RootViewController *RVC; 를 사용할 객체로 지정할 변수를 정의해 준다.

 

 

5. 구현 파일(.m 파일 여기서는 testAppDelegate.m)에서 아래 코드를 추가합니다.

 

 


- (void)applicationDidFinishLaunching:(UIApplication *)application {    

RVC = [[RootViewController alloc] init]; // < 추가

[window addSubview:RVC.view];            // < 추가

    // Override point for customization after application launch

    [window makeKeyAndVisible];

}

 
applicationDidFinishLaunching 함수는 어플이 실행되었을때 호출되는 함수입니다

RVC = [[RootViewController allocinit]; // 처음 사용될 RootViewController를 만든후 초기화 합니다.

[window addSubview:RVC.view];    // RootViewController에 달린 뷰를 window에 추가합니다

    
그리고 alloc하여 retain 된 메모리를 해제하기 위해
- (void)dealloc에서 [RVC release];  를 추가합니다.
이 함수는 해당 객체의 레퍼런팅 카운팅을 -1 해줍니다. 레퍼런스 카운트가 0이 되면 객체의 dealloc 메소드가 호출되고
메모리에서 제거됩니다.
앱 델리케이트의 경우 어플이 종료 될 때 호출되기 때문에 큰 차이는 없지만, 확식히 결자해지의 원칙으로 적어두는 것이 좋습니다.
 
여기까지하면 앱 델리게이트에서의 준비는 마무리 됐습니다.
 
이제 본격적으로 RootViewController에 대해 다뤄보겠습니다.
 
  * 코딩 시작전 이론 설명
 
     루트뷰컨트롤러 객체에서 테이블 뷰 객체를 만들 것입니다.
     이 경우 테이블 뷰 객체는 루트뷰컨트롤러의 하위로 포함되겠지만,
     그렇다고 테이블 뷰 객체의 데이터에 맘대로 접근 할 수 있는 것은 아닙니다.
     테이블 뷰 객체에서 허가된 속성이나 명령어에만 접근 할 수 있습니다.
 
      API에 대략 접근 가능한 속성을 볼 수 있습니다. (<-- 개인적으로 해석...)
      다만 기본적인 것들인 리스트나 그룹, 셀이 선택되었을 때
      이런 것들은 프로토콜 설정을 통해 접근하게 됩니다.
 
      만약 한 객체의 클래스를 자작해서 다루는 거라면 일방적으로 데이터를 뽑아오거나 호출이 가능합니다만,
      API의 경우 테이블 뷰의 소스코드까지 접근이 가능한 것은 아닙니다.
 
      맨 처음 해야 할 일은 헤더에서 프로토콜 설정을 해줍니다. (이건 테이블 뷰와 약속을 하는 겁니다.)
 
      테이블 뷰의 속성을 보시면 delegate와 datasource가 있습니다.
      나중에 코딩 작업에서 보시면 아시겠지만,
 
      delegate = self;
      datasource = self;
      로 설정합니다.
      이건 데이터나 테이블이 어떤 이벤트를 호출할 때 자기 객체 즉 루트뷰컨트롤러에 있는 함수를 호출하라는 뜻입니다.
 
      위 설정을 마친 후 뷰가 로드되게 되면 테이블 뷰는 이 루트뷰컨트롤러에 메세지를 보냅니다.
      그리고 대화 하는 식으로 데이터 설정이 일어납니다.
 
   ex)
      테이블 뷰 : 그룹이 몇개냐?
      루트뷰컨트롤러 : 1개다!
 
      테이블 뷰 : 0번 그룹의 셀 갯수가 몇개냐?
      루트뷰컨트롤러 : 5개다.
        * 테이블은 셀의 집합입니다.
 
      테이블 뷰 : 0번 그룹의 0번 셀의 데이터를 달라?
      루트뷰컨트롤러 : 이거다 (return cell)
 
      이런 식으로 대화(메세지)가 오가며 테이블 뷰에 데이터가 구현됩니다.
 
   기본 개념은 이정도로 해두시고 지금부터 코딩에 들어갑니다.
 
1. 처음 만들어진 뷰 컨트롤러(RootViewcontroller) 안에는 아무것도 없습니다. (기본 선언은 있음..)
 
 
- RootViewController.h 헤더 파일
 
 
- RootViewController.m 구현파일
 
2. 우선 myTable 변수를 클래스 변수로 정의 합니다.
 
 
3. 그리고 UIViewController와 '{' 사이에 ' <UITableViewDelegate, UITableViewDataSource> ' 를 넣습니다.
    이것은 앞서 설명한 테이블 뷰와의 약속으로 이 클래스가 UITableViewDelegate, UITableViewDataSource의 프로토콜에
    따른다는 선언입니다. 통신 프로토콜을 맞춘다고 생각하면 됩니다.
 
 
4. 그리고 RootViewController.m에서 init 함수를 생성해 줍니다.
 
 
 * 참고 공부 (autorelese)
    Object-C 에서 사용하는 객체의 메모리 관리, 정확히는 참조 카운트(reference count) 관리를 해 주어야 한다.
    객체를 새로 생성하거나 추가로 참조할 때는 카운트를 증가(retain) 시키고, 다 쓰고 나면 카운트를 감소(release) 시킨다.
    결국 0이 되면 객체는 메모리에서 사라진다.(dealloc) 참조가 0이 되면 사라지므로 그 이후에 객체를 사용하려 한다면
    예외를 보게된다.
 
    오토릴리즈는 참조 카운트 감소를 나중에 미루기 위한 그러면서도 카운트가 나중에 감소되는 것을 보장받기 위한 pool이다.
    예약 release 정도로 생각하면 될듯 하다.
    
     NSObject에 정의되어 있는 autorelese
        -(id)autorelease;
    객체의 참조 카운트를 감소시킬 때 release 대신 autorelease 를 사용하면 release가 예약된다. 실제 release를 수행하면
    참조 카운트가 바로 줄어들지만, autorelease 직후는 아직 release 된 상황이 아니므로 참조 카운트가 감소되지 않는다.
 
    오토릴리즈의 동작 원리
    오토릴리즈 된 객체는 오토릴리즈 pool에 등록된다. 오토릴리즈 풀은 객체를 관리하는 일종의 컬렉션이다.
    그리고 이 컬렉션이 해제될 때 관리하는 객체를 모두 release 한다.
    따라서 autorelease 된 객체의 release 되는 시점은 pool이 해제될 때이다.
    원하면 각종 컬렉션 객체를 이용하여 pool을 간단히 만들어 쓸 수 도 있지만,
    Foundation Framework에 NSAutoreleasePool이 있으므로 애써 만들 이유는 없다.
 
    왜 오토릴리즈를 사용할까?
    객체를 release 할 주체가 명확하지 않아 제 3자인 오토 릴리즈 풀에 릴리즈를 의뢰하는 것이다.
 
    결론
     오토릴리즈란 릴리즈 예약하고자 하는 객체를 오토릴리즈 풀이라는 컬렉션에 등록시키는 행위이며,
     오토 릴리즈 풀은 자신이 해제될 때 컬렉션에 있던 객체를 모두 릴리즈 한다.
 
   alloc과 init의 차이
     alloc은 클래스 메소드로 메모리를 확보해주고 객체를 하나 생성한다.
     init은 실제 객체의 초기값을 설정해 준다. 
     Class* instance = [[Class alloc] init];
     과 같은 형태로 사용
 
말 나온 김에 추가
 
 
1. Object Initialization
Object 생성을 위해서는 [SomeClass new] 를 이용하거나 [[SomeClass alloc] init] 을 사용한다. Cocoa에서는 new보다는 alloc -> init을 사용하기를 권장한다.

[SomeClass alloc] 을 하게 되면 운영체제로 부터 그 object의 instance 변수들을 유지하기에 충분한 메모리를 할당 받는다. 메모리를 할당 받으며 0 으로 초기화 된다. (BOOL은 NO로, float, int는 0으로, pointer는 nil로)

SomeClass *classPtr = [[SomeClass alloc] init]; 형태로 사용되며 다음과 같이 사용 되지 않는다

SomeClass *classPtr=[SomeClass alloc];
[classPtr init];

그 이유는 init에 의해서 리턴되는 Object는 alloc 된것과는 다를수 있기 때문이다. init 에 의해서 리턴되는 Object가 alloc 된것과 다른 이유는 init 메소드에 파라미터를 받아서 적절한 객체를 선택하기 위해서다. 

init 메소드를 override 하기 위해서는 다음과 같은 코드를 사용한다.

-(id) init
{
if (self = [super init]) {
Some codes for initialization...
}
return (self);
}

여기에서 [super init]은 super class를 initialization 하기 위해서 사용되며 앞에서 이야기 한 것과 탁이 init 시에 리턴되는 object는 달라질수 있기 때문에 self 포인터를 update 하였다. (self = [super init])

init 이 실패하게 되면 nil 을 리턴한다.

2. Convenience initializers
initializer는 확장이 가능하다. 이를 "Convenience initializers"라고 한다. 예를 들어  NSString의 경우

-(id) initWithFormat: (NSString *) format, ...;
-(id) initWithContentsOfFile: (NSString *) path;

등의 확장된 initializers들을 가지고 있다.

initializer에서 객체를 할당(alloc->init)하고 초기화 할수 있다. 이때 initializer에서 생성한 객체들은 앞에서 설명한 cocoa의 memory management 룰에 따라 dealloc 메소드에서 clean-up 해주어야 한다. 물론 이때 garbage collection 기능을 사용한다면 객체를 가르키는 pointer를 제거해 주면 된다. GC에서는 dealloc는 의미가 없으며 object가 collect 될때 호출되는 -finalize 를 오버라이드 해야 한다.

3. Designated Initializer
여러 Convenience intializer들을 가지고 어느 클래스를 상속하는 Subclass에서 부모 클래스를 초기화 할 경우에 어떤 initializer를 사용해야 할지 결정을 해야 한다. "Designated Initializer"는 initialier중에서 가장 많은 argument를 받는 initializer이다. Cocoa에서는 Desinated initializer를 subclass에서 부모 클래스를 초기화 할때 사용하도록 권하고 있다.
 
* self 와 super는 뭔가요?
   self 는 사용시 자신에게 없는 메소드의 경우 부모를 호출 (엄마 데려와~! ㅋ)
   super 사용시 부모 호출 (상위 클래스라고 생각하자!)
   좀 더 자세히 설명하자면 super는 단순한 flag이다. 컴파일러에게 실행할 함수를 찾을 때 어디서 시작해야하는 지 알려주는 것.
   이것은 메세지의 리시버로서 사용된다.
    self는 지역변수이다. 함수내부의 지역변수. 이것은 다양한 방식으로 사용될 수 있으며 심지어 값을 할당할 수도 있다.
 
  나름 소스 풀이
 
    - (id)init {  // 초기화-  생성자 정도  init 함수는 자신의 self를 반환한다. -- 컨트롤 생성... 이게 맞을 듯
         self = [super init];
// self는 [super init]에서 반환된 값과 동일한 값을 가지고 있습니다. 반환값을 확인하는 이유는 슈퍼 클래스에서 초기화에 실패하면 nil을 반환하기 때문에, 올바르게 초기화 되었는지 확인하고 실패했으면 자신도 nil을 반환하기 위함.
self = [super init]; 부분은 새로운 인스턴스를 생성해서 self에 대입하겠다는 의미가 아닙니다
 
선언해 놓은 클래스는 alloc등의 방법으로 인스턴스가 생성되었고 init이 호출될때 super의 init을 호출함으로서 super 클래스에서 필요한 init을 처리하는 것.

만약 'A' 클래스를 상속받아서 'B' 클래스에서 init을 만들고 거기서 self = [super init];을 했다면 'B' 클래스의 인스턴스를 생성하고 init을 했을때 [super init]이 호출되면서 'A' 클래스의 init이 처리됩니다. 이때 인스턴스는 하나입니다.
 

상위 클래스와 하위 클래스의 self는 동일합니다. 그렇기 때문에 self = [super init];과 같이 반환값을 받지 않아도 되지만 의미를 명확하게 하기위해 저런 방법으로 사용하는 것 같음.

 
* 참고
self = [super init]; 사용하는 이유는 객체가 제대로 만들어 졌는지 확인하는 위해서입니다
보통 외부에서 객체를 만들때 a = [[객체이름 alloc] init]; 을 사용합니다
[객체이름 alloc] 명령을 통해 객체가 만들어지고 init을 호출해서 초기화를 하게 됩니다
그렇다면 
a = [객체이름 alloc] ;
[a init];
이런 방법도 가능합니다만 이 방법은 안 좋습니다.
그 이유가 a = [객체이름 alloc] ;이 코드를 실행 할때 객체를 만드는것을 실패 할수도 있다는 겁니다
실패하면 a 에 nil이 들어가게 되는데요 이상태에서 init함수를 호출하면 런타임에러로 튕기게 됩니다
그래서 두줄짜리 코드 대신에 a = [[객체이름 alloc] init]; 이 코드를 사용합니다
self = [super init];
if (self != nil) {
객체 만드는것을 실패 하면 self에 nil이 들어가면서 if (self != nil) { 조건문을 거쳐서 초기화 작업을 스킵하고 nil을 돌려주게 됩니다.

객체만들기를 실패하는 예를 들자면

NSArray *array = [[NSArray alloc] initWithContentsOfFile:path];

같은 코드가 있습니다 이 코드를 간단한 xml파일을 배열로 읽어서 메모리에 저장해줍니다만 해당 주소에 파일이 없을 경우 객체가 만들어 지지 않고 nil을 가지게 됩니다
   
그러나 특별한 일 없이는 객체 만드는 작업이 실패할일은  없기 때문에 위 코딩은 거의 관례(?)로 하시면 됩니다.
이어서 계속
    
      if (self !=nil) { // 객체의 초기화 성공이면 이란 뜻 실패시 위에 설명처럼 nil을 반환
           myTable = [[UITableView alloc]initWithFrame:self.view.bouns style:UITableViewStylePlain];
            // myTable에 UITableView 을 UITableViewStylePlain 스타일로 프레임으로 생성해준다.
           myTable.delegate = self;
           // myTable의 델리게이트를 자신으로 선언
           myTable.dataSource = self;
           // myTable.dataSource를 자신으로 선언
           [self.view addSubview:myTable];
           // 뷰에 myTable를 보여준다
      }
      return self;
        // 앞서 설명한 듯이 init 매소드는 자신을 반환한다.
  }
     
 
후아 길었습니다.
이걸로 myTable에 새로 만들어진 테이블뷰 객체의 주소가 들어갔습니다.
하지만 이걸로 빌드를 하면 델리게이트가 구현되지 않았다고 경고가 뜹니다.
테이블 뷰와의 통신용 함수가 없는데 당연합니다.
 
자 또다시 코딩을 시작하기 전에 잠시 봐둡시다.
myTable에 새 객체 넣는 코드에서 UITableView 부분을 오른쪽 클릭하여 뜬 메뉴에 Jump to Definition을 클릭하면
테이블 뷰 헤더가 뜹니다.
 
- Jump to Definition을 클릭하면..
 
   
- 짠 이런 헤더 파일이 뜹니다. 여기서 스크롤을 내리시다가
  @protocol UITableViewDataSource<NSObject> 부분을 찾아 보면
  @required와 @optional이 있습니다.
  @required 밑에 정의된 함수는 델리게이트 구현에 반드시 필요한 함수입니다.
   @optional 밑에 정의된 함수는 필요하면 쓰고 필요 없으면 쓰지 않는 말그대로 옵션 입니다.
 
2편에 계속
 
 
 
이어서 할 것
참고 사이트 :