최초 작성일 : 2011/05/18 09:23 


iPhotoDiary를 2.0으로 업그레이드하면서 일기 작성시 앱이 죽는 버그가 생겼다.

가능한한 빨리 작업하고 아이튠즈 커넥트에 업데이트 등록을 했는데
그 때가 벌써 4월 28일이다.

그리고 5월 7일 첫 reject 메일이 날아왔다. 내용은 다음과 같다.

Hello HYOUNG JUN WOO|1074475225,

Thank you for submitting iPhotoDiary to the App Store.

We've completed the review of your app, but cannot post this version to the App Store because it did not comply with the App Store Review Guidelines, as detailed below:

  • 8.1: Apps must comply with all terms and conditions explained in the Guidelines for using Apple Trademark Names and the Apple Trademark Products List

To reply to this message or to get more information, visit the Resolution Center in iTunes Connect. Do not reply directly to this email.

Regards,

App Review

Converse with fellow developers and Apple engineers on technical topics.

Apple Developer Forums — http://devforums.apple.com 

일단 앱스토어 리뷰 가이드라인의 8.1 조항을 어겼다는 것이고
이 조항은 애플의 트레이드마크로 등록된 이름들과 제품들의 사용에 관한
내용으로 쉽게 말해 애플 제품의 이름을 아무렇게나 사용하면 안된다는 것이다.

사실 이전 버전들이 아무런 문제 없이 등록이 되었기에 별로 심각하게 생각지 않았고
그래서 혹시나 앱을 설명하는 내용중에 문제가 있나 싶어 앱 설명을 대폭 줄여서
핵심적인 내용만 남기고 다 지워버렸다.

그리고 다시 5월 14일에 동일한 내용으로 reject 메일을 받았다.
이번엔 좀 요모조모 살펴보았다.

아하...그런데 아이튠즈 커넥트 사이트에 이전엔 못봤던 링크가 하나 눈에 들어왔다.
리젝 사유에 대해 자세히 설명하는 내용이 들어있는 페이지로 가는 링크였다.
아쉽게 해당 내용을 캡쳐하지는 못했지만 핵심적인 내용은 앱 이름에 애플 제품의
이름이 직접 들어가면 안되다는 것이 핵심이었다.

즉 나의 앱 이름은 iPhotoDiary였고 여기에 iPhoto라는 애플의 제품명이 들어있어서
결국 리젝된 것이었다.

애플 제품 이름을 사용할 경우 애플에서 권장하는 방식은

앱 이름 - for iPhone

이런 형식이다.

어쨌든 황당한 것은 처음 등록부터 2차례의 업데이트까지 모두 통과했었는데
이제와서 이런 사유로 리젝을 당하니 사실 좀 불쾌한 느낌도 들었다.

하지만 가이드라인을 제대로 확인 안한 잘못도 있고 또 갑작스레 애플의 정책이
강화되었겠거니 하고 그냥 넘어가는 수 밖에...
내가 잡스랑 맞짱뜰 군번도 아니고...

암튼 이런 리젝은 좀 짜증나는 것이 사실이다...-.-

저작자 표시
신고

최초 작성일 : 2011/03/25 16:32 






거의 1년이 다되서 두 번째 업데이트를 했습니다.
큰 변화는 없지만 신경을 좀 썼습니다...^^

변경사항

1. 달력의 절기표시를 수정하였습니다. 절기를 구하는 함수를 추가하여
    절기가 표시되도록 하였습니다. 절기를 구하는 함수는 PHPSCHOOL의 산이님이
    PHP로 만든 소스 중에 절기를 구하는 부분만 Objective-C로 변환하여 사용하였습니다.

2. 일기 작성시 날씨와 현재 위치의 주소값이 자동 표시되도록 하였습니다.
    현재 날씨는 구글 날씨API를 사용중인데 국내 대상 앱이므로 조만간 기상청
    데이터로 변경 예정입니다.

3. 디자인을 변경하였습니다. 레티나 디스플레이를 지원하도록 하였습니다.

소스코드 : https://github.com/woohj70/iPhotoDiary 

저작자 표시
신고

최초 작성일 : 2011/02/22 15:52 







AppStore : iPhotoDiary



사실 네이버의 맥부기 카페에 연재하고 있던 [실전 소스 분석] 마무리 짓고 

소스를 공개하려고 했는데 이래저래 바쁘다보니 연재가 언제 재개될지 기약이 없네요.

그래서 일단 소스를 먼저 공개합니다.


염두에 두실 것은 제가 머리털나고 처음 Objective-C 아이폰을 접하면서 독학으로

만든 것이다보니 소스의 수준이 한참 저질입니다...^^;;;


특히 초보분들은 섣불리  코드 따라 쓰지 마세요...^^;;;

물론 어디에 어떤 용도로 쓰든지 그것은 자유입니다.


그리고 고수분들은 혹시라도 제게 조언을 해주실 것이 있으면

아낌없는 조언 부탁드리겠습니다.


뭐 가장 찾으실만한 내용은 음력을 지원하는 달력이 있는데요

이게 주워온 소스를 사용하다보니 메모리 누수가 보이는 부분이 좀 있네요.

그 부분으 주의 하세요.


https://github.com/woohj70/iPhotoDiary

저작자 표시
신고

최초 작성일 : 2010/10/18 02:10 


3. 커스텀 셀


3-1. 커스텀 셀 만들기


커스텀 셀을 사용하는 방법은 생각보다 쉬웠습니다. 사실 커스텀 셀 뿐만 아니라 대부분의 아이폰

개발이 막상 닥쳐보면 생각보다 쉽습니다. 그만큼 애플에서 꼭 필요하다 싶은 것만을 SDK에

잘 담아 놓았기 때문이겠죠…^^(하지만 최근에는 많은 부분 접근 제약이 있음으로해서 보다 편의

적인 내용 구현이 막혀 있는 면을 보게 되어 좀 답답하긴 하더군요.)


본론으로 들어가서…

일단 화면상에 보여지는 대부분의 객체들은 모두 View기반이라고 생각하시면 되겠습니다.

테이블 뷰 셀 역시 UIView로 부터 상속받은 클래스이구요. 따라서 일단 셀 구성을 하는 것은

뷰에다가 객체를 배치하듯이 하면 됩니다.


일단 커스텀 셀로 사용할 클래스를 만들어야겠죠?


우선 New File 창에서 Cocoa Touch Class중 Objective-C class를 선택하시면 중앙에 있는

셀렉트 박스에서 UITableViewCell을 선택하실 수 있습니다. 이 것을 선택하시고 나머지

단계는 그냥 보통 하시던 대로 쭉쭉 나가시면 됩니다…^^;;;






기본적으로 구현되어있는 메서드는 초기화를 위한 initWithStyle:reuseIdentifier: 메서드와

셀의 선택여부를 설정할 수 있는 setSelected:animated: 메서드 그리고 dealloc메서드가

있습니다. 매우 단순합니다.


추가적으로 이 셀에서 발생한 이벤트와 관련하여 이 셀이 사용되는 TableView로 메시지를

전달하기 위해 protocol을 만들어야 하는 경우도 있으나 필수는 아닙니다. 만일 커스텀 셀에서의

이벤트를 테이블 뷰에서 처리할 일이 없다면 굳이 안만들어도 될 것 같습니다.


그리고 앞서 UIView에서 상속받은 클래스라고 하였기 때문에 미리 짐작을 하신 분들도 계시겠지만

viewDidLoad같은 메서드들이 없기 때문에 IB를 통해 셀 화면을 구성해야 하는데 자동으로

XIB 파일을 생성해주지 않습니다. 따라서 생성된 클래스 파일명과 같은 이름으로 별도로

XIB 파일을 만들어 주고 IB를 통해 디자인을 하도록 합니다.


New File 창에서 iOS의 User Interface를 선택하고 Empty XIB를 선택합니다.

다음 단계에서 파일명은 앞서 만든 클래스 파일명과 동일하게 만들어주고 Finish합니다.









다음 XIB파일을 열어서 File's Owner를 선택하고 Object Identify 창에서 Class를 이 커스텀 셀을

사용할 Table View Controller 클래스로 지정해줍니다.






마지막으로 Library Inspector에서 Table View Cell 객체를 메인 Inspector로 가져다 놓습니다.

그리고 Object Identify Inspector에서 Class를 앞서 만든 Table View Cell 클래스로 지정합니다.







마지막으로 자주 실수하는 부분인데요. 반드시 Attribute Inspector창의 맨 위에 있는

Identifier에 이름을 지정해주고 이 이름으로 코드 상에서 사용을 해야 합니다.

[tableView dequeueResusableCellWithIdentifier:identifier]에서 identifier의 값이 바로

여기서 지정한 이름이 되어야 합니다.






이제 준비는 다 끝났네요. 방금 가져다놓은 Table View Cell 객체를 더블클릭하면 작은 뷰

화면 하나가 나타납니다.









여기다가 필요한 컨트롤들을 가져다 놓으시면 됩니다.


이 과정으로 만들어진 제 커스텀 셀입니다. 아이 등록 화면의 첫번째 섹션에 사용되는 셀입니다.

사진 선택 및 출력용의 버튼 하나와 사진 프레임을 위한 이미지 뷰, 이름과 생년월일 입력을 위한

텍스트 필드 2개와 음력 Label 표시를 위한 Label, 그리고 양/음력 선택을 위한 Segment 컨트롤이

들어있습니다.









3-2. 커스텀 셀 사용하기


자, 구슬이 서말이라도 꿰어야 보배라죠…^^ 만든 커스텀 셀을 사용해보도록 하겠습니다.

단도직입! 바로 소스를 보죠.


제가 만든 커스텀 셀은 AddChildViewController라는 클래스에서 사용합니다.


우선 만일 커스텀 셀에서 protocol을 만들었다면 당연히 AddChildViewController 클래스는

delegate메서드를 구현해야겠죠. 저는 ChildBasicInfoCellDelegate를 구현하도록 코딩이 되어

있습니다. 사용하는 delegate 메서드는 생년월일 입력 텍스트 필드 터치시 데이트 피커를

불러오기 위한  movePickerView:(ChildBasicInfoCell *)basicCell 메서드입니다.


이 클래스는 당연히 UITableViewController를 상속 받았구요.

따라서 테이블 뷰를 구성하는 각종 delegate 메서드들이 있습니다.

그 중에 커스텀 셀을 화면에 보여주는 메서드는 바로


tableView:cellForRowAtIndexPath:


이 메서드입니다. 이 메서드는 내부적으로 Row 수만큼 Loop를 돌면서 테이블 뷰 위에

테이블 뷰 셀을 붙여줍니다.


다음은 AddChildViewController메서드에 앞서 보여드린 제 커스텀 셀을 붙이이 위해 구현한

내용만을 가져온 것입니다.


//첫번째 섹션인 경우에만 이 커스텀 셀을 사용합니다.

if (indexPath.section == BASIC_SECTION) {

                      

//바로 이 이름("ChildBasicInfo")이 IB의 Attribute Inspector의 identifier 항목에 입력한

              //그 이름이어야 합니다.                                                  

static NSString *CellIdentifier = @"ChildBasicInfo";

    

//커스텀 셀을 사용하지 않을 경우에는 이 때 만들어지는 셀의 클래스는 그냥

//UITableViewCell입니다. 지금은 제가 만든 커스텀 셀 클래스인 ChildBasicInfoCell의

//인스턴스를 받아옵니다.

ChildBasicInfoCell *cell = (ChildBasicInfoCell *)[tableView 

dequeueReusableCellWithIdentifier:CellIdentifier];

//셀의 재사용을 위한 조건문이죠. 이미 사용된 적이 있는 인스턴가 있으면 그 것을 그냥

// 사용하고 그렇지 않으면 새로운 인스턴스를 만듭니다.

if (cell == nil) {

//일단 메인 번들로부터 커스텀 셀들을 모두 가져와서 배열에 넣고

NSArray *nib = [[NSBundle mainBundle]

loadNibNamed:@"ChildBasicInfoCell" owner:self options:nil];

//가져온 커스텀 셀들의 배열을 Loop로 돌려서

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;

}


마지막으로 역시 빼먹기 쉬운 메서드인데요.

UITableViewDelegate에 있는 메서드인


- (CGFloat)tableView:(UITableView *)tableView 

           heightForRowAtIndexPath:(NSIndexPath *)indexPath


이 메서드입니다. 제가 만든 커스텀 셀은 높이가 120필셀입니다. 그냥 이상태로는 테이블 뷰는

셀의 높이까지는 파악을 하지 못합니다. 그래서 셀의 높이가 기본 높이로 그냥 표시되기 때문에

커스텀 셀이 잘려보입니다. 이렇게 기본 높이와 다르게 만들어진 커스텀 셀의 높이를 테이블 뷰에

알려주기 위해서는 위 메서드를 통해 커스텀 셀의 높이를 알려주어야 합니다.


코드는 아래와 같습니다.


- (CGFloat)tableView:(UITableView *)tableView 

           heightForRowAtIndexPath:(NSIndexPath *)indexPath

if (indexPath.section == BASIC_SECTION) {

return 120.0f;

else {

return 44.0f;

}

}


저는 첫번째 섹션만 셀의 높이가 다르기 때문에 이와 같이 코딩되어 있습니다.



4. 셀에 컨트롤 넣기


3절의 내용에서도 커스텀 셀에 segment 컨트롤이 들어가 있었습니다.

만일 모든 셀에 동일하게 컨트롤을 포함시키고자 한다면 이렇게 사용하면 됩니다.

하지만 같은 셀을 사용하면서 특정 인덱스에 있는 셀에만 컨트롤을 포함시키고자 할 경우도

있을 것입니다.


아래는 제 iPhotoDiary의 기념일을 등록하는 화면입니다.






화면을 보시면 이 테이블 뷰 셀들은 모두 같은 클래스로 만들어져 있는데요.

위의 2개의 셀에는 타이틀과 눈에 보이지는 않지만 값을 입력받을 Lable 그리고

Disclosure Indicator가 있는 반면 아래 2개의 셀은 타이틀과 스위치 컨트롤만 보입니다.






이렇게 유동적으로 컨트롤을 넣기 위해서는 역시 cellForRowAtIndexPath메서드에서 작업을

해야 하구요. 코드는 아래와 같습니다. 셀을 만드는 과정이 이미 잘 알고 계실 듯하여 핵심 코드만

적습니다.


if (indexPath.row == 0) {

cell.titleLabel.text = @"기념일 : ";

else if (indexPath.row == 1) {

cell.titleLabel.text = @"날짜 : ";

else if (indexPath.row == 2) {

cell.titleLabel.text = @"공휴일 : ";

cell.accessoryView =

[[[UISwitch allocinitWithFrame:CGRectZeroautorelease];

else if (indexPath.row == 3) {

cell.titleLabel.text = @"음력 : ";

cell.accessoryView =

[[[UISwitch allocinitWithFrame:CGRectZeroautorelease];

}


아주 간단하죠…^^

즉, cell 인스턴스의 accessoryView 속성에 사용하고자 하는 컨트롤의 인스턴스를 만들어

넣어주기만 하면 됩니다.


그리고 Disclosure Indicator는  IB를 통해 설정해주었기 때문에 코드상에는 설정하는 부분이

없는 것입니다.



5. 테이블 뷰의 배경


이 것은 일종의 팁이라고도 할 수 있겠지만 썩 좋으 방법 같지도 않아 그냥 간단하게

이런 방법도 있다는 것을 말씀드리는 선에서 넘어가겠습니다.


테이블 뷰를 사용하면서 기존에 자주 보이는 회색 스트라이프가 딱히 맘에 들지 않아

테이블 뷰에 배경을 넣고 싶었습니다. 그런데 테이블 뷰에 바로 배경을 넣으니 테이블 뷰

전체에 배경이 깔리는 것이 아니라 각각의 셀에 배경이 들어가게 됩니다.


즉, 셀의 높이만큼 이미지들이 잘려서 셀의 갯수만큼 보여지게 되죠.

배경 이미지가 단순하여 구분이 가실지는 모르겠지만 요렇게 됩니다.



뷰 컨트롤러의 뷰에 배경이 깔린 경우




테이블 뷰 컨트롤러의뷰에 배경이 깔린 경우









그래서 사용하는 방법은 UIViewController에 UITableViewController.view를 얹고 뷰 컨트롤러의

view 배경으로 이미지를 넣은 후 테이블 뷰와 커스텀 셀들의 배경색을 투명으로 지정하는 것입니다.

그럼 테이블 뷰를 통과해 밑에 있는 뷰의 배경 이미지가 전체적으로 보이게 됩니다.


코드는 아래와 같습니다.


anniversaryTableView = [[AddAnniversaryTableViewController alloc]

initWithStyle:UITableViewStylePlain];

//테이블 뷰의 배경색을 투명으로 지정합니다.

anniversaryTableView.view.backgroundColor = [UIColor clearColor];

anniversaryTableView.view.opaque = YES;

anniversaryTableView.delegate = self;

anniversaryTableView.tableView.allowsSelectionDuringEditing = YES;

//뷰 컨트롤러의 뷰에 배경 이미지를 지정합니다.

self.view.backgroundColor =

[UIColor colorWithPatternImage:

[UIImageimageNamed:@"anniversary_background.png"]];

anniversaryTableView.view.frame = CGRectMake(9010230450);


//그리고 뷰 컨트롤러의 뷰 위에 테이블 뷰 컨트롤러의 뷰를 붙입니다.

[self.view addSubview:anniversaryTableView.view];


하지만 이 방법은 테이블 뷰에 배경을 깔기 위해 불필요한 뷰 컨트롤러을 하나 더 사용해야 한다는

점에서 성능면으로나 구현의 난이도 면에서 그리 좋아 보이진 않습니다.


구현의 난이도라고 표현한 것은 테이블 뷰에서 발생한 이벤트를 통해 새로운 뷰를 띄우거나 할 경우

새로운 뷰가 뜨는 위치가 꼬임으로 해서 delegate를 잘 써줘야 한다는 점입니다.


혹시 더 좋은 방법을 알고 계신 분들이 있으시면 정보 부탁드립니다…^^;;;


오늘로 이번 장을 끝내볼까 했는데 또 내용이 길어져 다음주까지 계속해야겠네요.

이번 주는 이것으로 마치겠습니다.

긴 글 읽어주셔서 감사합니다.


6. 셀에 대한 접근과 인덱스 알아내기

7. 등록과 수정 화면 공유

8. 테이블 뷰의 스크롤

9. 요약

10. 마무리

저작자 표시
신고

최초 작성일 : 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. 마무리


저작자 표시
신고

최초 작성일 : 2010/10/04 01:25 




1. 드디어 실제 소스 분석!


앞서의 글들은 소스 분석이라기 보다는 앱 개발 작업 전반에 걸쳐 필요한 사항들을 일반적인

입장에서 적어본 글들이었습니다. 물론 모든 내용이 적절하게 기술되었는지에 대해서는

확신이 서질 않지만 어쨌든 글을 작성하기에는 이런 일반론적인 내용이나 세부적인 기술이

좀 더 편하긴 한 것 같습니다. 적절한 참고 자료를 선택해 인용하고 부연하는 정도로

충분하니까요…^^


하지만 이제부터 작성되는 글 내용은 제가 실제로 개발을 했던 소스를 살펴보는 것이기 때문에

사실상 꽤나 부담이 됩니다.


자바 경력은 어느 정도 있지만 Objective-C라는 생소한 언어와 iOS라는 생소한 플랫폼으로

작업을 하는 것이다보니 효율적인 코드라든지 메모리 관리, 차후의 유지보수에 관한 측면 등은

미처 생각을 할 여유조차 없이 그저 기능 구현에 매달리게 되었고 결국 이런 소스를 남들에게

공객하고 또 감히 그 것으로 뭔가를 설명한다는 것은 저의 (개발자로서의) 모든 치부가 고스란히

드러나는 일이 될 수도 있기 때문이죠…^^;;;


하지만 글을 진행하는 동안 종종 말씀 드렸듯이 제 글은 참고서로써 혹은 따라야 할 모범으로써가

아니라 그저 타산지석으로, 다시말해 '이렇게 개발하는 사람도 있구나'. 혹은

'이사람은 이런 부분을 잘못했구나'라는 부분들을 함께 공유하고 또 그 것을 통해 저도

배우겠다는 목적으로 글을 쓰게 된 것입니다.


사족은 걷어치우고 본론으로 들어가서 7회째의 이야기를 시작해 보도록 하겠습니다.

이번 글에서는 제가 개발한 아이폰 앱인 iPhotoDiary의 메인 화면 작업 과정에 대해 살펴보도록

하겠습니다.


소스 내용 중 CoreData 사용 부분에 대해서는 이전 글들에서 모두 설명을 드렸기 때문에

CoreData에 대해 궁금하신 분들은 저의 이전 글들 중 3~5편을 살펴 보시기 바랍니다.


아울러 다시 한 번 양해 말씀드리지만 제가 머리털 나고 처음으로 접해본 언어를 가지고

처음으로 접해본 플랫폼에서 처음으로 만든 애플리케이션입니다. 이점 감안해주시길

부탁드립니다. 아울러 제 글이 어떤 형태로든 도움이 되었다고 생각되시는 분들은 제가

잘못하고 있는 부분에 대해서도 아낌없는 조언 부탁드립니다…^^



2. 화면 설계


다른 분들도 모두 그렇겠지만 최초의 설계가 개발 완료시점까지 유지되는 경우는 거의 없을

것입니다. 제 경우도 마찬가지여서 최종 결과물의 화면은 최초 설계시의 그것과는 너무나도

딴판이네요. 개발 과정을 기록해두었다가 어떤 변화가 있었는지를 살펴보는 것도 하나의

재미라면 재미겠네요. 참고로 제가 최초 구상한 메인화면은 아래 이미지와 같았습니다.






그리고 최종 결과물은 아래와 같죠






바로 전 글에서 말씀을 드렸지만 Interface Builder(이하 IB)는 UI설계 도구로서도 꽤 훌륭합니다.

사실 첫번째 이미지처럼 러프한 스케치가 선행 되면 더 쉽긴 하겠지만 IB같이 실제 화면상의

모습을 거의 그대로 볼 수 있는 도구가 있다면 더 좋겠죠…^^


변한 것은 UI뿐만 아닙니다. 현재 화면상에 보여지는 정보는 등록된 아이의 이름, 생년월일,

나이, 성별, 띠, 별자리, 탄생석 등이 있습니다만 최초에는 성별, 띠, 별자리, 탄생석 등의

정보가 없고 별 필요없는 태명, 출생지, 담당 의사 등의 정보가 있었습니다. 바꾸길 잘했죠…^^?


그리고 최종적으로 변경된 사항은 이름, 생년월일, 나이…등의 title Label은 이 앞선 버전에서는

배경 이미지에 적혀있었습니다. 좀 더 예쁜 폰트를 사용해보고 싶은데 방법을 몰랐기에

제가 사용할 수 있는 꼼수로 배경 이미지에 Label Text를 넣고 그 위치에 맞게 값들이 보여질

Label 컨트롤드을 배치 했었던 것이죠. 하지만 아이폰 4가 나오고 해상도가 달라졌다는 말이

들리자 '아 이렇게 했다가는 나중에 해상도 차이 때문에 Label과 Value가 어긋나는 경우도

생기겠구나' 하는 생각이 들어 다시 title Label을 Label 컨트롤로 바꾼 것입니다.






지금은 아이폰에서 원하는 폰트를 사용하는 방법을 알았으니 조만간에 업데이트 해서

사용해봐야죠…^^ 폰트 관련 내용에 대서는 링크를 참고하세요. http://www.prapps.net/416


※하단 탭도 변경이 있었는데 이 내용은 이후 글에서 다루도록 하겠습니다.


사족이지만 가장 불만스러운 부분은 아이 사진이 나오는 부분의 사진 프레임입니다.

뭔가 포인트를 주고 싶어 이래저래 만들어봤는데 오히려 시선을 분산시키는 역할만 하고 있네요.

역시 개발자의 디자인 감각이란 한계가 있는가봅니다…^^;;;


그래서 최종적인 화면 구성은 상반부에는 아이에 대한 기본 정보가 출력되고

하반부에는 위쪽에 선택한 아이에 대한 그날의 이벤트를 표시하는 TableView, 아래쪽에는

등록된 아이들을 선택할 수 있는 버튼이 출력될 영역으로 결정이 되었습니다.



3. 실제 소스를 보면…


일단 IB상의 구조를 보시면 다음과 같습니다. 자주 본 이미지죠…^^






Layout상의 가장 하단에 백그라운드 이미지를 보여줄 ImageView가 위치하고 있습니다.

분홍색 그라데이션에 하얀 별 문양이 있는 배경입니다. 이 부분도 진작에 UIView에 배경

이미지를 넣는 방법을 알았더라면 굳이 ImageView를 사용하지 않았을 것입니다만 이 당시에는

그런 방법조차 몰랐네요…^^;;;


참고로 아래와 같은 코드죠.


self.view.backgroundColor = [UIColor colorWithPatternImage:

[UIImage imageNamed:@"cameraControllerBacground.png"]];


이후 다른 화면에서는 위의 방법을 사용하여 배경 이미지를 사용했습니다.


다음으로는 등록한 아이의 사진을 보여줄 ImageView와 별모양의 프레임을 표시해줄 ImageView

그리고 title Label들과 성별, 띠, 별자리, 탄생석 등의 아이콘을 표시해 줄 ImageView, 끝으로

아이의 기본 정보 값을 표시해 줄 Label들이 있네요. 중간에 ImageView하나가 더 있는데

이 것은 테이블 뷰에 표시될 오늘의 이벤트를 둘러싼 프레임인데 이것도 역시 맘에 안듭니다…-.-


이 중에 실제 값이 반영될 컨트롤들은 당연히 코드상에 IBOutlet으로 선언된 객체들과 연결이

되어야 하고 title Label이나 사진 프레임같이 늘 고정된 값만을 가진 것들은 그럴 필요가 없습니다.


일단 이 화면은 탭바 컨트롤러에 연결되어 앱 실행시 내부적으로 호출되어 load되므로

기본 코딩된 initWithNibName…메서드는 주석처리 한 상태입니다. 초기화 코드는 모두

viewDidLoad 메서드에 들어가 있습니다.


나의 실수


저는 viewDidLoad 메서드의 이름만을 생각하고서는 이 메서드가 뷰가 사라졌다가 보여질 때마다

수행되는 메서드라고 생각했었습니다. 그런데 아마도 뷰의 인스턴스가 생성되는 최초 시점에

한번 만 수행이 되나보더군요. 일단 인스턴스가 생성된 뷰에서 뷰가 사라졌다가 다시 보여질

때마다 수행되는 메서드는 - (void)viewWillAppear:(BOOL)animated 메서드를 사용해야

한다는 것을 나중에 알았습니다.


간단하게 내용을 보시면 다음과 같습니다.

NavigationController에 대한 설정 때문에 같은 내용의 코드가 많은 뷰의  viewDidLoad 메서드에

반복적으로 사용되고 있는데 이러한 반복 코드의 공통 함수화도 제가 풀어야 할 숙제네요…ㅠ.ㅠ


CGRect labelFrame = CGRectMake(140.028.0170.040.0);

UILabel *label = [[[UILabel allocinitWithFrame:labelFrame] autorelease];

label.font = [UIFont boldSystemFontOfSize:18];

label.numberOfLines = 2;

label.backgroundColor = [UIColor clearColor];

label.textAlignment = UITextAlignmentRight;

label.textColor = [UIColor blackColor];

label.shadowColor = [UIColor whiteColor];

label.shadowOffset = CGSizeMake(0.0, -1.0);

label.lineBreakMode = UILineBreakModeCharacterWrap;

label.text = @"\nMy Children";

self.navigationItem.titleView = label;


위 내용은 NavigationController의 중앙에 있는 하트모양과 iPhotoDiary가 찍힌 이미지의 우측

밑으로  My Children이라는 텍스트를 출력하는 내용입니다.






다음은 NavigationController의 좌우에 있는 UIBarButtonItem을 설정하는 코드가 나옵니다.


// Configure the add button.

    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];


다들 아시는 것 처럼 만일 UIBarButtonItem의 Text를 직접 지정해주고 싶을 경우에는

아래와 같은 메서드를 사용해야 합니다.


UIBarButtonItem *saveButton = [[UIBarButtonItem allocinitWithTitle:@" "style:UIBarButtonItemStylePlain target:self action:@selector(save)];

self.navigationItem.rightBarButtonItem = saveButton;

[saveButton release]; 


그리고 얼마전 이 연재를 시작하면서 알게 되어 부랴부랴 수정한 managedObjectContext를

가져오는 코드입니다.


iPhotoDiaryAppDelegate *appDelegate = (iPhotoDiaryAppDelegate *)[[UIApplicationsharedApplication] delegate];

self.managedObjectContext = appDelegate.managedObjectContext;


마지막으로 여러 명의 아이가 등록되었을 때 어떤 아이가 선택되었는지 확인하는 용도로 사용될

문자열 변수인 childButtonName을 초기화 하는 내용이 들어있네요.


많은 컨트롤들을 IB에서 배치하고 IBOutlet으로 연결하였기 때문에 코드가 짧습니다.


그리고 이 화면에서는 데이터를 불러와 출력하는 기능만 있고 데이터 변경이 일어나지는 않기

때문에 fetchedResultsController 메서드는 필요가 없고 fetchRequest를 통해 데이터를

불러오는 내용이 있는 사용자 메서드를 만들어서 사용하시면 됩니다.


제 소스에서는 만일 네비게션바의 우측에 있는 신규 등록 버튼을 눌러 등록 화면으로 갔다가

오는 경우나 좌측의 편집 버튼을 통해 편집 화면에서 수정이나 삭제가 이루어진 경우 그 내용을

반영해야 하기 때문에 앞서 말씀드린 현재 뷰를 벗어났다가 다시 돌아올 경우 매번 수행되는

viewWillAppear:(BOOL)animated 메서드에 데이터를 조회하는 메서드를 호출하는 코드가

들어있습니다. 함수명은 fetchedRequestResult:(NSString *)childName입니다.

코드는 아래와 같네요.


- (void)viewWillAppear:(BOOL)animated {

[super viewWillAppear:animated];

if ([self.childDatas count] == 0 ||

[self.childDatas count] != childCount) {

[self fetchRequestResult:childButtonName];

}

}


여기서 childDatas는 조회된 데이터를 담고있는 NSArray 타입의 객체입니다.

그리고 childCount는 아이의 등록/삭제 등으로 변동이 있었을 때 그 결과를 반영하는 데이터 수를

표시해주는 NSInteger타입의 static 변수입니다.


즉 현재 데이터 건수가 0이거나(최초 실행시) 현재 불러온 데이터 건수와 변경사항을 반영하는

수치가 같지 않은 경우(등록이나 삭제가 발생한 경우) 새롭게 fetchRequest를 통해 데이터를

가져오도록 한 것입니다.


다음으로 가장 중요한 fetchedRequestResult:(NSString *)childName인데요.

이 메서드를 호출함으로써 코어 데이터로부터 데이터를 가져와서 그 데이터를 해당 컨트롤에

출력해 줄 수 있는 것입니다. 이 fetchedRequestResult:(NSString *)childName메서드에서

하는 일은 등록된 아이들의 데이터를 모두 가져와서 다음과 같은 작업을 합니다.


1. Core Data에 접근하여 데이터 가져오기

2. 등록된 아이가 없을 경우 아이 등록을 요청하는 Alert창 띄우기

3. 아이들 수와 성별에 맞는 선택 버튼을 동적으로 생성하여 배치.

4. 등록된 아이들 중 오늘이 백일, 돌 혹은 생일인 경우 Message 창 띄우기


그런데 사실상 1번의 작업이 주된 목적이고 2,3,4번의 작업은 주 목적과 직접적인 관계가

없기 때문에 다른 메서드로 분리시키는 것이 좋을 것입니다.


그리고 애초에는 계획이 데이터를 불러오는 시점에서 모두 불러올 것이 아니라 선택된

아이의 데이터만을 불러올 생각으로 인자로 아이 이름 문자열을 넘기도록 했었는데요

나중에 그냥 전체를 다 불러와서 처리하는 것이 더 나을 것 같아 수정을 했습니다. 그래서

메서드 인자인 (NSString *)childName는 의미가 없어졌네요…-.-


이런 부분이 숙제로 남아있습니다…ㅠ.ㅠ


마지막으로 위에서 배치된 버튼을 선택함에 따라 선택된 버튼의 아이 정보를 화면에 출력해주는

setDatatoControlls:(ChildData *)cData라는 메서드가 있네요. 여기서는 인자로 받은 ChildData의

인스턴스를 통해 IB에서 배치한 각각의 컨트롤들에 실 데이터를 출력하고 있습니다.


어려운 부분은 생년월일이라는 기본 정보를 가지고 양/음력 생일, 띠, 탄생석, 별자리 등을

계산하는 부분이고 나머지는 그냥 해당 컨트롤에 값을 대입하기만 하면 됩니다.


고민...


기본 정보로 가공이 필요한 경우, 예를 들면 생년월일을 통해 띠와, 탄생석과 별자리가

정해집니다. 이 때 이러한 가공된 데이터들을 저장시에 미리 계산하여 DB의 별도 필드에 넣고

사용을 할 것인지 아니면 지금처럼 기본 정보만 넣고 가공 데이터는 출력시 가공작업을 거쳐

보여줄 것인지같은 문제가 미묘하게 신경을 건드리는 부분이 있습니다.


전자를 택하자니 DB를 운용하는데 있어 조금이라도 부하를 덜 주고 싶은 마음이 생기고

후자를 선택하자니 아무래도 입력 보다는 조회의 빈도수가 높은데 그런 부분에 데이터 가공이라는

작업 부하를 주고 싶지 않은 마음도 생기고…


다행이 데이터 건수가 몇건 안되기 때문에 어떤 방식을 쓰건 상관이 없겠지만 대량의 데이터를

처래 해야 하는 경우에는 충분한 부하 계산을 거쳐서 방법을 정하는 것이 좋을 것 같습니다.

그게 힘든 것이 우리 개발자들의 현실이지만 말이죠…ㅠ.ㅠ


나머지는 단순히 버튼이 클릭되었을 경우의 이벤트 처리를 위한 메서드들이네요.



4. 요약


본격적으로 실제 소스를 살펴보다 보니 정말 미흡한 부분이 많은 소스코드네요…^^;;;

무식하면 용감하다고…용케도 이런 코드를 남에게 공개하고 또 이 것을 바탕으로 소스 분석을

하겠다는 야무진 계획을 세웠습니다…


실제 소스를 분석한 부분이라 딱히 요약이라기엔 뭐하고 팁이라고 할만한 것을 보면


1. 네비게이션 컨트롤러의 타이틀을 사용자가 직접 지정하는 방법. 아래 코드입니다.

CGRect labelFrame = CGRectMake(140.028.0170.040.0);

UILabel *label = [[[UILabel allocinitWithFrame:labelFrame] autorelease];

label.font = [UIFont boldSystemFontOfSize:18];

label.numberOfLines = 2;

label.backgroundColor = [UIColor clearColor];

label.textAlignment = UITextAlignmentRight;

label.textColor = [UIColor blackColor];

label.shadowColor = [UIColor whiteColor];

label.shadowOffset = CGSizeMake(0.0, -1.0);

label.lineBreakMode = UILineBreakModeCharacterWrap;

label.text = @"\nMy Children";

self.navigationItem.titleView = label;


2. 단순 조회성 화면이므로 fetchedResultsController 메서드는 필요가 없고

    fetchRequest를 통해 데이터를 불러와 화면에 출력하면 된다는 것.


요정도가 되겠네요.



5. 마무리


대부분이 IB에서의 작업이라 실제 코드 분량은 그리 많지 않은 화면이었습니다.

setDatatoControlls:(ChildData *)cData 메서드에서 띠, 탄생석, 별자리 계산하는 내용과

각각의 값에 맞는 아이콘을 구분해서 지정해주는 내용이 좀 길다면 기네요.


다음 시간에는 메인 화면과 연관된 아이 등록 화면과 편집 화면에 대해 살펴보도록 하겠습니다.

혹시 시간이 부족한 경우 둘 중의 한 화면만 진행하겠습니다…^^;;;


새로운 한 주 잘 보내시고 다음에 다시 뵙겠습니다.

긴 글 읽어주셔서 감사합니다.

저작자 표시
신고

최초 작성일 : 2010/09/26 02:03 


1. View와 Control들의 역할


지난 시간까지 PC, 노트북, 모바일 기기 전체를 통틀어 가장 핵심이라고 할 수 있는

'데이터'를 관리하는 내용에 대해 코어 데이터를 이용하는 방법을 통해 알아보았습니다.

데이터의 '입력 -> 가공 -> 출력'은 모든 컴퓨팅 기기의 수행 업무에 대한 고전적인 정의라고도

할 수 있겠죠.


굳이 이와같은 말을 하지 않더라도 정확한 가공을 통한 정확한 정보의 출력은

디지털 기기에서 매우 중요한 일입니다.


하지만 이왕이면 다홍치마라고 정확한 정보가 출력되더라도 가급적이면 보기 좋게 출력되면

더 좋겠죠. 그래서 뷰와 컨트롤들을 잘 다루는 것 또한 중요하게 되는 것이구요.


당근 빠따인 말들은 집어 치우구요. 좀 더 기술적으로 말을 해보도록 하죠.

프로그래밍을 좀 해보신 분들은 MVC라는 말을 많이 들어보셨을 겁니다.

특히 객체 지향 프로그래밍에서는 이제는 너무나도 당연시되는 패턴이기도 하고

또 UI를 구성하는 대부분의 API가 바로 이 MVC 패턴을 기반으로 만들어져 있습니다.

물론 이 API를 이용한 개발자들의 코드도 MVC를 지키도록 노력하여 만들어 지고 있구요.


MVC는 곧 Model, View, Control의 첫 스펠링을 모은 용어입니다.

그러고 보니 3가지 모두 매우 익숙한 단어네요. Model이란 용어가 좀 걷돌기는 하지만

이전 글에서 코어 데이터를 다룰 때 분명 ManagedObjectModel이 있었죠.

그리고 View와 Control는 UI개발을 할 때 너무나 익숙한 용어들이구요. 게다가

이 글의 제목이기도 하네요…^^;;;


바로 그렇습니다. 꾸미지않고 직설적으로 연관을 지어보면 다음과 같습니다.


Model = 데이터

View = 데이터를 사용자에게 보여주는 화면

Control = 데이터의 입력이나 가공, 출력을 지시하는 객체들


가장 단순한 형태의 프로그램인 계산기를 생각해보도록 하죠.

액정 화면, 숫자키와 연산자 키, 그리고 계산을 할 수 있는 반도체 모듈만 있으면 가장 단순한

계산기가 하나 만들어질 것입니다. 이 계산기의 작동은 이렇습니다.


1. 사용자가 숫자키(Control) 10을 누릅니다.

2. 임시 저장공간에 10(Model)이 저장됩니다.

3. 액정 화면(View)에 10이 표시됩니다.

4. 사용자가 +키(Control)을 누릅니다.

5. 계산기는 연산의 종류를 기억합니다.

6. 사용자가 숫자키(Control) 5를 누릅니다.

7. 계산기는 10과 +연산을 할 대상으로 5를 기억합니다.

8. 사용자가 =키(Control)를 누릅니다.

9. 계산기는 연산을 진행하고 그 결과인 15를 액정 화면(View)에 표시합니다.

10. 사용자는 액정 화면(View)을 통해 결과를 확인합니다.


즉 숫자 및 연산자 키는 Control로 계산기에게 뭔가 지시를 하고 그 지시를 통해 변경된

사항을 View에게 알리도록 하여 View에 변경 사항이 반영되도록 합니다. 가장 복잡한

역할을 하네요.(그리고 우리가 흔히 Control이라 부르는 UI객체들은 대부분 View의 기능을

동시에 가지고 있는 객체들입니다. 따라서 원한다면 View없이 Control들만으로 화면을 구성하는

일도 얼마든지 가능합니다.)


Model에 해당하는 10과 5 및 이 둘의 +연산 결과인 15는 입력되고 가공되고 출력되는

데이터입니다. (물론 연산의 종류인 +도 데이터의 일종이긴 하나 좀 모호해서 확실하게

확인 가능한 숫자만 다룹니다.)


그리고 이러한 데이터들을 사용자가 시각적으로 확인할 수 있도록 View에 해당하는 액정 화면에

표시하도록 한 것입니다.



이 글을 작성하다가 문득 알게 된 내용인데요, iOS SDK에는  UIViewController라는 객체가

있습니다. 이름에 Controller가 들어있기지요. 비록 데이터와 연관은 없지만 외적 변화에 대한

내용을 View에 알려주어 View가 그 변화에 맞게 행동할 수 있도록 해줍니다.

다시 말해서 View를 가지고 있는 Control이죠.


위에서 UI객체들 중 Control들은 대부분 View를 가지고 있다('View 클래스를 상속받았다'가 좀더 정확한 표현이겠죠)고도 말했었는데요 이와같이 실제로 View와 Contro은 그 영역을 구분하기가

어려운 경우가 많습니다.



우리가  Interface Builder를 사용하든 혹은 사용하지 않고 코딩으로만 처리하든 화면 구성과

어떤 기능 수행을 위해 다양한 View들과 Control 등을 사용하게 됩니다.

위의 내용을 염두에 두신다면 이후 View와 Control들을 사용하고 배치하는데 어느 정도

도움이 되실 것입니다.



2. Interface Builder(이하 IB로 표기하겠습니다.) - 언제 쓰나?


최근 개발 툴들이 제공하는 기능 중에 편한 것 중 하나가 바로 UI를 WysWyg 방식으로

구성할 수 있도록 도구들을 제공한다는 점입니다.


필요한 객체들을 마우스를 통해 드래그 앤 드롭하여 개발자가 원하는 위치에 놓기만 하면

별다른 코딩 없이 화면을 멋지게 구성할 수 있습니다. Xcode에서는 그런 기능을 하는 툴이 바로

IB입니다. (IB에 대한 세부적인 내용이나 사용법은 본 글의 주제에서

벗어나므로 생략합니다.)






IB를 사용하게 되면 일일히 View나 Control의 좌표를 지정해준다든지 size를

설정한다든지 하는 번거로운 코딩을 하지 않아도 되기 때문에 작업 속도를 매우 빠르게 해줍니다.


하지만 언제든지 IB가 도움이 되는 것은 아닙니다.


예를들어 날짜별로 찍은 사진을 화면에 보여준다고 할 때 모든 날짜에 찍은 사진의 수가 일정한

것이 아니기 때문에 IB를 통해 ImageView를 배치하는 것이 불가능합니다.

이럴 경우에는 어쩔 수 없이 코딩을 통해 동적으로 ImageView를 생성해주는 수 밖에 없겠죠.


제가 IB를 사용하는 기준은 바로 이런 것입니다.

즉 화면상에 고정적인 객체들이 많이 필요하면 IB를 통해 화면을 먼저 설계를 한 후 동적으로

생성해야 할 객체들을 코딩을 통해 구현합니다.






또 이렇게도 씁니다. 전혀 .xib 파일이 필요 없고 모두 코딩을 통해 구현하게 되는 화면이지만

코딩을 하는 동안에는 화면 구성이 어떨지 실제로 볼 수도 없고 또 좌표를 확인하기도 쉽지

않기 때문에 우선 IB를 통해 화면 구성을 합니다. 그리고 각 객체들의 좌표를 확인한 후 그 값을

코딩에 이용합니다. 코딩이 끝나면 과감하게  .xib 파일을 지워버리죠…^^ 일종의 화면 설계 또는

프리젠테이션(키노트나 PPT같은) 용도로 사용을 하는 것이죠.


그러나 이런 경우에는 잘 사용을 안하게 됩니다.


1장에서도 말씀드린 바와 같이 Control이란 객체들은 상당히 복잡한 매커니즘을 가지고 있습니다.

때문에 개발자들의 코딩 대부분이 바로 이러한 Control들의 행동을 정의하는 것이라고 봐도

과언이 아닙니다.


이렇게 어차피 코딩이 많은 Control들을 화면에 구현하는 경우 그 갯수가

많아지면 많아질 수록 IBOutlet 설정하고 IBAction 설정하고 IB에서 코드와 객체 연결해주고…

이러한 과정이 오히려 더 번거로워질 수 있습니다. 그냥 컨트롤 생성하고 addTarget…메서드로

이벤트 지정해주는 것이 더 깔끔할 수 있죠. 이런 경우에도 IB를 사용하지 않고 작업을 하는 것이

더 좋을 수도 있을 것입니다. 운이 좋다면 for문을 통해 주루룩 생성할 수도 있죠…^^;;;


물론 이 것은 어디까지나 저의 경우입니다. 실제 IB를 어떻게 활용할 것인가 하는 것은

각 개발자분들이 경험을 통해 판단하는 것이 제일 정확할 것이라고 생각되네요.



3. View와 Control - 어떻게 배치할 것인가?


IB를 쓰건 안쓰건 UI를 구성하기 위해서는 각종 View들과 Control들을 배치를 해야 합니다.

하지만 여기서 중요한 것은 각종 View와 Control들의 수평적인 배치가 아니라

수직적인 배치입니다.


대다수의 프로그래밍 언어에서  UI 객체들은 Layer의 계층 구조를 이루고 있습니다.

포토샾에서의 Layer와 같은 개념으로 보시면 될 것입니다. 필요한 객체 이외의 부분은

투명 처리를 하여 하나 하나의 UI 구성 요소를 한겹 한겹 쌓아 올린 것이라고 보면 됩니다.

아래 그림은 UINavigationController Class Reference항목에 삽입되어있는 navigation controlle의

view 구성에 대한 그림입니다.






하나의 클래스 이렇게 중첩 구조로 되어있음은 물론 개발자들이 배치한 UI객체들도 모두

수직적인 위치를 갖게 되는 것입니다. HTML을 잘 아시는 분들은 z-index 속성을 아실 것입니다.

아쉽게도 IB에서는  z-index 처럼 수직적인 위치를 세세하게 지정할 수는 없습니다. 다만

가장 부모가 되는 View에서 sendSubviewToBack이나 bringSubviewToFront를 사용하여

가장 뒤로 혹은 가장 앞으로 가져오는 정도의 조절만을 할 수 있습니다.


만일 IB를 사용하였다면 이 작업을 보다 직관적으로 하실 수 있습니다.

두 번째 글 쯤에서 IB의 메인창에 대한 설명을 한 그림을 기억하시는지요?

다음과 같은 그림이었습니다.






위의 그림을 보시면 View에 배치된 컨트롤들의 목록이 보이실 것입니다. 바로 이 목록의

위치를 드래그 앤 드롭으로 이동하여 위치를 조절하시면 됩니다. 가장 위에 있는 컨트롤이

가장 아래(뒤쪽)에 있는 컨트롤이고 가장 아래 있는 컨트롤이 가장 위(앞쪽)에 있는 컨트롤입니다.


이 위치가 중요한 이유는 자칫 순서가 뒤바뀌어 단지 사진을 보여주는 이미지 뷰가 버튼 컨트롤을

가리면서 앞쪽에 위치하게 되면 버튼 컨트롤의 터치 다운 이벤트가 이미지 뷰에 막혀서 기능하지

않게 되기 때문입니다. 심한 경우는 그 모습도 보이지 않게 되죠…^^


같은 맥락에서 어떤 UI객체들을 서로 대등한 위치에 놓을 것인지 아니면 한 객체를 다른 객체의

하위에 놓을 것인지도 잘 고려해야 합니다.


예를들어 아래 그림과 같이 어떤 버튼을 눌렀을 경우 TextView와 관련된 영역이 확장되는 기능이

있다고 했을 때 만일 확장되는 영역의 모든 UI객체들(타이틀 바, 확장 버튼(▲, ▼), 날씨 버튼,

저장 버튼, UITextView 등)이 수평적으로 대등한 위치에 있게되면 확장/축소시 각각의 모든

객체들에 대해 좌표를 다시 설정을 해주어야 합니다. 하지만 만약 최 상위에 View를 하나 두고

UI객체들을 그 하위에 배치를 하게 되면 View, TextView, 타이틀 바 정도만 좌표와 사이즈를

수정하여 처리할 수 있게 되는 것입니다.







4. 요약


오늘은 화면 구서요소에 대한 원론적인 이야기와 함깨 지극히 개인적이지만 IB를 사용하면

좋은 경우와 안좋은 경우 그리고 UI 컨트롤들을 배치할 때 주의해야 할 사항들을 알아보았습니다.

워낙에 일반적이거나 개인적인 내용이다보니 몇줄로 요약하기도 쉽지 않네요. 주의점 하나만

더 언급하는 것으로 요약 부분을 마무리 하죠…^^;;;



일단  IBOutlet으로 설정하여 IB에서 배치한 객체와 연결을 하게 된 경우 이 객체에 대해서는

별도의 생성 처리를 해 주지 않아도 됩니다. IB에서 만들어진 객체에 대해 다시 코드상에서

 alloc과 init관련 메서드를 통해 초기화를 하게 될 경우 IB에서 설정한 내용 정상적으로 반영이

안되는 경우가 생길 수 있습니다.


따라서 IB에서 만든 객체라면 단지 그 변화시에 반영해야 할 내용들, 예를들면 위치 변화, 크기

변화 혹은 일부 속성의 변화 등과 관련된 코딩만 해주시면 됩니다. 물론  dealloc에서 release는

해주시구요.


그리고 흔히 하시는 실수…IB에서 객체 만들어 놓고 코드상에서 IBOutlet 만들어놓고…

정작 연결 안시키는 실수…^^;;; 주의하세요~



5. 마무리


실제 소스를 분석하기 전에 우선 UI관련 객체들과 관련된 내용들의 일반적인 부분을 다루어

보았습니다. 여전히 시행착오를 거치고 있는 과정이다보니 실수가 적지 않을 것 같네요.

많은 조언 부탁드립니다.


다음 시간에는 위 내용을 바탕으로 제 앱인 iPhotoDiary의 메인 화면의 제작 과정을

분석해보도록 하겠습니다.


명절 후유증 잘 극복들 하시고 다음 시간에 뵙겠습니다.

저작자 표시
신고

최초 작성일 : 2010/09/20 00:33 



애초의 계획은 일정 내용이 진행되면 해당 내용에 관련된 소스 코드 전체를

공개하고 이후 다음 진행하고 또 관련 소스 공개하고...이런 식으로 하려 했는데

소스를 부분부분 자르려니 쉽지도 않고 또 보시는 입장에서도 단편적인

소스는 이해하기가 어려울 것 같아 소스 공개는 이 [실전 소스 분석]이 모두

끝난 후 전체 소스를 한꺼번에 공개하는 것으로 방향을 잡았습니다.

이 점 참고하시기 바랍니다.


1. Predicate 사용하기


지난 시간까지 코어 데이터의 개념, 필요한 메서드들, 테이블 뷰 컨트롤러를 통한 코어 데이터의

사용 등에 대해 알아보았습니다. 사실 기본 개념이나 원리를 몰라도  API 내용만을 가지고도

충분히 사용할 수 있을만큼 코어 데이터의 사용법은 간단합니다. 더불어 테이블 뷰와의 관계도

잘 만들어져 있어 NSFetchedResultsControllerDelegate에 선언된 메서드를 구현하면 별다른

코딩 없이 코어 데이터의 변경사항(입력, 수정, 삭제)이 즉시 테이블 뷰에 반영이 됩니다.

개발자에게는 정말로 편한 방법이라고 할 수 있을 것입니다.


하지만 항상 테이블의 모든 데이터를 화면에 출력하는 것은 아닙니다. 특정 날짜에 등록된

데이터, 혹은 특정 성씨를 가진 사람들의 집합. 또는 득정 지역별 데이터 등 어떤 조건에 따라

전체 데이터 중 일부만을 필요로 하는 경우가 대다수일 것입니다. 이 때 사용하는 것이

Predicate이고 이 것은 RDBMS에서 SQL문을 작성할 경우 where 조건절에 해당하는 내용을

담게 됩니다.


Predicate 역시 클래스를 사용하는 것 자체는 어렵지 않습니다. 다만 인자로 넘기는 Prediccate의

포맷을 잘 알아두어야 합니다. 가끔 언급을 했듯이 제가 진행하는 이 내용은 '강좌'라기보다는

제 소스를 같이 분석해보는 '소스 분석'에 가깝기 때문에 이러한 세세한 부분까지는 다루지

않겠습니다. 단지 Predicate 클래스를 어떤 식으로 사용하는지만 간단히 언급하도록 하겠습니다.


이전 시간까지 언급한 클래스 중에 NSFetchRequest는 'SQL문'에 해당한다고 앞서

말씀드렸습니다. 그러므로 where 조건절에 해당하는 predicate는 당연히 fetchRequest에

포함되어야 합니다. 다음과 같은 코드를 작성하시면 끝입니다.


다음은 이전 내용에서 보여드린 fetchedResultsController 메서드의 일부분입니다.


// Attribute의 이름을 설정

NSString *attributeName = @"sectionDate";

// predicate 생성

NSPredicate *predicate = [NSPredicate 

                   predicateWithFormat:@"%K LIKE[cd] %@"// where 조건문

                   attributeName,

                   [conditionString stringByAppendingString:@"*"]];

NSFetchRequest *fetchRequest = [[NSFetchRequest allocinit];


// fetchRequest에 predicate을 설정함

[fetchRequest setPredicate:predicate];

.

.

.


위 내용 중 눈여겨보실 것은 predicate를 생성하는 부분인데요. 그 중에서도 인자로 넘어가는

문자열을 구성하는 부분을 잘 보셔야 합니다. 먼저 위 predicate의 인자로 넘어가는 문자열을 SQL문으로

바꿔보면 다음과 같습니다.


where sectionDate like conditionString || '%'


만일 conditionString의 값이 '2010년 8월'이라면


where sectionDate like '2010년 8월%'


이렇게 되는 것입니다.


물론 이 밖에도 많은 표현이 가능하지만 앞서 말씀드린 대로 자세한 내용은 API 문서를 참고해주세요…^^;;;

해당 내용은 'Predicate Programming Guide'의 Predicate Format String Syntax절에 있습니다.



2. 테이블 뷰를 사용하지 않는 화면에서의 데이터 사용

(NSManagedObjectContext클래스의

                     executeFetchRequest메서드)


이전 시간에는 테이블 뷰를 통해 데이터를 화면에 보여주는 내용에 대해 살펴보았었습니다.

하지만 항상 테이블 뷰를 통해 테이터를 보여주기만 하는 것은 아니죠.


때론 UIView를 통해 데이터를 표현해 줄 수도 있습니다.


테이블 뷰를 통해 데이터를 표현하는 경우에는 section정보와 row정보등이 필요하며 이러한 경로를

통해 원하는 데이터를 불러오기 위해 indexPath라는 객체를 사용한다는 것을 말씀드린 바가 있습니다.

또한 데이터의 변경사항을 테이블 뷰에 반영하기 위해 NSFetchedResultsControllerDelegate에

선언된 4개의 메서드들이 필요하게 됩니다.


하지만 테이블 뷰가 아닌 곳에 데이터를 표현하는 경우는 위와 같은 정보들이 필요 없는 상황이

대부분이며 따라서 몇가지 구현은 생략을 하여도 됩니다.


즉, 단순히 필요한 여러건의 데이터만을 가져와 사용하는 경우에는 단지 NSFetchRequest

클래스만을 사용하여 처리 할 수 있게 되는 것입니다.


제가 만든 애플리케이션인  iPhotoDiary의 첫 화면은 등록된 아이들의 데이터를 가져와

그 중 한 아이의 신상 정보를 UIView 기반의 화면에 보여주는 것입니다.






제가 필요한 것은 단지 등록된 아이들의 데이터와 각각의 아이들을 선택할 경우 키워드가 될

아이들의 이름 뿐입니다. 섹션 정보도 필요 없고 또 이 화면에서는 데이터의 변경이 이루어지지도

않습니다. 따라서 직접 만든 다음의 메서드 하나에서 데이터의 사용이 완결됩니다.


- (void)fetchRequestResult:(NSString *)cName


인자로 넘어가는 것은 등록된 아이의 이름입니다. 어차피 많아야 3-4명의 아이들을 등록하여 사용하는

경우가 대부분일 것이라 판단했고 그렇다면 각각의 아이에 대해 코어 데이터에 접근하는 것은

낭비라고 생각하여 일단 이 함수에서 모든 데이터를 가져 온 후 데이터 건수만큼 아이들을 선택할 수

있는 버튼을 만들고 버튼을 클릭할 경우 이 함수에서 불러온 데이터 배열(ManagedObject의 배열)에서

이름을 비교하여 해당 데이터를 가져오도록 처리하였습니다.


따라서 이 함수에는 아주간단한 내용만으로 데이터를 가져오고 있습니다.

(그리고 사실상 인자로 넘어오는 cname이 필요가 없죠...^^;;;)

코드를 보시면


NSFetchRequest *fetchRequest = [[NSFetchRequest allocinit];

[fetchRequest setEntity:[NSEntityDescription 

  entityForName:@"ChildData" 

              inManagedObjectContext:managedObjectContext]];

NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc

  initWithKey:@"birthday" ascending:YES];

NSArray *sortDescriptors = [[NSArray alloc]

  initWithObjects:&sortDescriptor count:1];

[fetchRequest setSortDescriptors:sortDescriptors];

NSError *error;

self.childDatas =

[managedObjectContext executeFetchRequest:fetchRequest error:&error];



[fetchRequest setSortDescriptors:sortDescriptors]; 이전까지의 내용은

잘 아실 것입니다. fetchRequest를 생성하고 대상 테이블을 ChildData로 지정하고 정렬 기준을

birthday로 하여 생일이 빠른 아이가 먼저 나오도록 하였습니다.


그리고 마지막으로 managedObjectContext의 executeFetchRequest 메서드를 통해 쿼리를 실행하고

이 메서드의 return값으로 데이터의 배열을 얻어오게 된 것입니다.

참고로 결과 값을 받은 self.childDatas는 NSArray객체입니다.


메서드의 이후 내용은 데이터의 값을 가지고 여러가지 가공을 하여 화면의 각 요소에 출력하는

내용입니다.


이와같이 데이터의 변경이 없는 단순 조회성 데이터의 사용은 managedObjectContext 객체만으로

충분합니다.



3. 정리


코어 데이터 사용의 끝자락이다보니 오늘은 좀 일찌감치 마무리를 합니다…^^;;;


초반에도 말씀을 드렸다시피 이 내용들은 전문적인 내용에 대한 설명이 아니라 초보 개발자의

시행착오를 그대로 보여드리는 것이 목적이기에 결과는 동일하지만 과정이 나쁜 그런 코드들이

많이 보이실 것으로 생각됩니다.


하지만 어느 정도 프로그래밍의 경험이 있으신 분 들은 과정이 얼마나 중요한지 잘 아실 것입니다.

그래서 이미 완성된 코드를 다시 Refactoring하기도 하는 것이죠.


안타깝게도 저는 Objective-C를 처음 접하는지라 어찌어찌 결과물은 만들었지만 잘못된 부분들을

수정하기는 쉽지가 않네요. 그나마 이 실전 소스 분석을 진행하면서 몇군데 어설픈 코드를 바로잡을

수는 있었습니다. 이런 부분에 대해서는 이 글을 보시는 많은 분들이 도움이 필요합니다…^^;;;


간단하게 코어 데이터를 구성하는 객체들을 다시 한 번 짚어보며 마무리를 하도록 하겠습니다.


ApplicationDelegate에서 한 번만 설정

1. Persistent store : 영구저장소 RDBMS의 데이터파일(.dbf파일 같은…)

2. Data Object Model : 데이터베이스의 전체적인 모양, Schema, ERD

3. Persistent store Coordinator : 영구 저장소에 접근하기 위한 객체

4. Managed Object Context : 객체 관리 컨텍스트. 코어 데이터를 사용하기 위한 핵심 객체


필요한 위치에서 수시로 사용되는 객체들

5. Entity Description : Entity 즉, 테이블과 관련된 정보를 갖고 있는 객체

6. Managed Object : 기본적으로 Entity의 구조(Schema)를 표현하나 쿼리의 결과로서 받아지는

                            데이터의 Row를 표현하기도 함

7. Fetch Request : 데이터를 불러오기 위한 쿼리를 표현하는 객체

8. Predicate : 쿼리문 중 where 조건절을 표현하는 객체

9. Fetched Results Controller : 코어 데이터로부터 가져온 데이터들을 계층적으로 관리하는 객체

                   테이블 뷰를 통한 데이터 표현에 사용된다.



4. Tip - Data Migration


코어 데이터로 작업을 하면서 가장 난감했던 상황이 .xcdatamodel 파일을 통해 Entity들과 각 Entity의

relationship 및 property들을 만들어놓고 나중에 이 것을 수정하여 빌드하고 실행시키면 데이터 모델이

서로 다르다고 에러를 토해내는 상황이었습니다.

즉, 수정된 데이터 모델을 적용시키는 방법을 몰랐던 것입니다.


그래서 많은 분들이 알고 계시겠지만 데이터 모델의 변경을 반영하는 방법을 팁으로 추가합니다.


우선 xcode 화면에서의 설정입니다.


1. xcode의 Groups & Files 창에서 .xcdatamodel 파일을 선택합니다.

2. xcode의 메뉴바에서 Design -> Data Model -> Add Model Version을 선택합니다.






이렇게 하고 Groups & Files 창에서 보시면 기존에 .xcdatamodel 파일이 하나 있던 것이

.xcdatamodeld라는 디렉토리로 바뀌어있고 그 디렉토리 아래 .xcdatamodel 파일이 존재할 것입니다.

현재 사용중인 데이터 모델은 파일 아이콘에 초록 바탕에 체크 표시가 된 그림이 추가됩니다.

그리고 이전 버전의 데이터 모델들은 파일명 끝에 일련 번호가 붙습니다. 이 때 번호가 클 수록

가장 오래된 버전입니다.


이런 과정을 거친 실제 데이터 모델 화면입니다.






다음은 ApplicationDelegate의 persistentStoreCoordinator 메서드에서 다음 코드를 추가합니다.


NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:

[NSNumber numberWithBool:YES], SMigratePersistentStoresAutomaticallyOption,

[NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption,

nil];


이 Dictionary객체는 persistentStoreCoordinator를 생성할 때 인자로 들어가게 됩니다.


전체 코드는 다음과 같습니다.


- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {

    if (persistentStoreCoordinator != nil) {

        return persistentStoreCoordinator;

    }

NSString *storePath =

[[self applicationDocumentsDirectory]

stringByAppendingPathComponent:@"NewIPhotoDiary.sqlite"];


NSURL *storeUrl = [NSURL fileURLWithPath:storePath];

NSDictionary *options =

[NSDictionary dictionaryWithObjectsAndKeys:

[NSNumber numberWithBool:YES], 

 NSMigratePersistentStoresAutomaticallyOption,

[NSNumber numberWithBool:YES],

 NSInferMappingModelAutomaticallyOptionnil];

    persistentStoreCoordinator =

[[NSPersistentStoreCoordinator alloc]

initWithManagedObjectModel: [selfmanagedObjectModel]];

NSError *error;

if (![persistentStoreCoordinator 

addPersistentStoreWithType:NSSQLiteStoreType 

configuration:nil URL:storeUrl

options:options error:&error]) {

// Update to handle the error appropriately.

NSLog(@"Unresolved error %@, %@", error, [error userInfo]);

exit(-1);  // Fail

    }    

    return persistentStoreCoordinator;

}


만일 managedObjectModel 메서드도 아래 코드와 다르다면 아래 코드처럼 바꿔주세요.


- (NSManagedObjectModel *)managedObjectModel {

    if (managedObjectModel != nil) {

        return managedObjectModel;

    }

NSString *path = [[NSBundle mainBundle]

pathForResource:@"NewIPhotoDiary"ofType:@"momd"];

NSURL *momURL = [NSURL fileURLWithPath:path];

managedObjectModel = [[NSManagedObjectModel alloc]

initWithContentsOfURL:momURL];

    return managedObjectModel;

}


이제 빌드하고 실행시키시면 잘 돌아갈 것입니다…^^



5. 마무리


처음으로 아이폰 앱을 만들 때만큼 좌충우돌, 시행착오의 연속이네요.

어렵사리 하나의 항목에 대한 글을 마쳤는데 부족한 부분이 너무 많이 느껴집니다.

이 글을 읽는 많은 분들의 양해를 바랄 뿐입니다…^^;;;


본격적으로 시작된 내용이 코어 데이터라서 더 많이 어려웠는지 모르겠습니다.

일단 이 부분에 대해서는 겸허하게 여러분들의 비판과 내용 수정을 기다리도록 하겠습니다.


이제 데이터를 처리하는 부분이 완성되었으니 다음시간부터는 iPhotoDiary의 화면을 순서대로

따라가면서 어떤 식으로 작업을 하였는지에 대해 설명을 드리도록 하겠습니다.

바로 다음 시간에는 첫 화면인 등록된 아이의 신상정보와 아이 선택 버튼이 있는 화면을

소스를 보면서 분석해보도록 하겠습니다.

앞으로는 그리 어려운 내용은 없을 것 같네요…^^


긴 글 읽어주셔서 감사하고 다음 시간에 뵙겠습니다.

저작자 표시
신고

최초 작성일 : 2010/09/10 18:24 


0. 정정


지난 시간에 코어 데이터 관련 객체들 설명 중 ManagedObject에 대한 설명을 다음과 같이 하였습니다.


쉽게 말하면 Entity를 클래스 파일로 만들어놓은 것이라고

생각하시면 됩니다. 이후 실제 코딩 작업 설명에 자세히 말씀드리겠지만 xcdatamodel 파일을 통해

구성된 Entity들은 클래스 파일로 만들 수 있습니다.

다만 이 클래스 파일이 항상 필요한 것은 아니고 개발자가 별도의 메서드를 추가시키고자 할 때나

사용을 하게 됩니다.


한데 이 설명이 적절치 못한 것 같아 정정합니다.


우선 .xcdatamodel 파일을 통해 Entity를 실제 클래스 파일로 만들 수 있게 된다는 부분까지는

맞습니다. 그리고 이 클래스파일은 NSManagedObject 클래스를 상속받아 만들어지는데요.

하지만 이 이렇게 생성된 클래스 파일은 같은 이름의 Entity의 구조(스키마, schema)를 표현하긴

하나 실제로 인스턴스화 되어 사용될 때는 같은 이름의 Entity에 저장된 데이터 Row를 의미한다고

보셔야 할 것 같습니다. 이 내용은 아래 본문에 다시 언급하고 있습니다.


Tip Entity 클래스 파일 만드는 방법


1. 우선 .xcdatamodel을 선택하여 데이터 모델 설계 화면을 엽니다.

2. 좌측 상단의 Entity 목록 화면에서 Entity 하나를 선택합니다.




3. Groups & Files창의 필요한 위치에서 오른쪽 마우스 클릭 후 Add -> New File… 을 선택합니다.

4. New File 팝업창에서 iOS->Cocoa Touch Class를 선택하면 우측 화면에 Managed Object Class라는

   새로운 항목이 보입니다. 이것을 선택합니다.




5. 진행하다보면 현재까지 만든 모든 Entity목록을 보여주는 화면이 보이고 이 화면에서 클래스를 만들

   Entity를 선택해서 클래스 파일을 만들면 됩니다.





1. 복습


먼저 지난 시간 분석한 내용을 요점 정리 해보면 다음 4 개의 메서드로 집약될 것입니다.


managedObjectContext

persistentStoreCoordinator

managedObjectModel

applicationDocumentsDirectory


이 메서드들 중에서도 가장 중요하다고 할 수 있는 것은  managedObjectContext입니다.

물론 나머지 3개의 메서드들도 당연히 중요하지만 일단 자주 등장하고 자주 사용하게 된다는

면에서 managedObjectContext가 중요한 것입니다. 간단하게 그 흐름을 되집어보면


1. managedObjectContext의 인스턴스 를 구하기 위해 같은 이름의 메서드를 호출합니다.

2. managedObjectContext를 구하기 위해서는 영구 저장소에 접근해야 하기 때문에 영구

   저장소에 접근하기 위해 사용되는 persistentStoreCoordinator의 인스턴스를 얻게됩니다.

3. persistentStoreCoordinator를 얻기 위해 필요한 정보인 영구 저장소가 존재하는 경로와

   영구 저장소에 저장되어있는 데이터의 논리적인 구현을 표현하는 managedObjectModel

   을 구하게 됩니다.


   이를 위해서 applicationDocumentsDirectory와 managedObjectModel 메서드가 호출됩니다.


이런 과정을 거쳐 얻게 되는 managedObjectContext에는 우리가 사용하게 될 Entity(테이블)와

property(필드 또는 컬럼), 그리고 relationship(Entity간의 관계)에 대한 정보들이 들어있습니다.

그래서 본격적인 데이터 관련 작업은 managedObjectContext에서 시작된다고 보시면 됩니다.


위의 4가지 메서드는 프로젝트 생성시 'Use Core Data for storage'옵션을 체크하면 자동으로

코드가 생성된다는 것도 지난시간에 말씀드렸습니다. 그리고 특별한 이유가 없는한 이렇게

자동 생성된 4개의 메서드는 전혀 수정하실 필요도 없이 그냥 사용하면 됩니다.


2. managedObjectContext로부터 시작하기


지난 시간에 언급한 내용중에 제 실수를 하나 말씀드렸습니다.

그것은 managedObjectContext를 사용하는데 managedObjectContext가 필요한 뷰 컨트롤러에서

ApplicationDelegate로부터 managedObjectContext를 가져오는 것이 아니라 ApplicationDelegate에서

각각의 뷰 컨트롤러에 managedObjectContext를 할당해 주었던 것입니다. 이렇게 될 경우 아무래도

managedObjectContext를 사용하지 않는 시점에도 managedObjectContext 객체의 retain count가

올라간다든지 하는 등의 문제가 발생할 소지가 있을 것입니다.


즉, 특정 뷰 컨트롤러에서 managedObjectContext가 필요하다면 viewDidLoad 정도의 위치에서

ApplicationDelegate에 접근하여 managedObjectContext를 얻어와야 한다는 말입니다.

다음 코드처럼 말이죠.


iPhotoDiaryAppDelegate *appDelegate =

     (iPhotoDiaryAppDelegate *)[[UIApplication sharedApplication] delegate];

self.managedObjectContext = appDelegate.managedObjectContext;


이제부터 이 뷰 컨트롤러에서는 managedObjectContext의 인스턴스를 사용할 수 있게 된 것입니다.


그럼 이 managedObjectContext를 이용해서 어떤 작업을 하게되느냐…


우선 잠시 말을 돌려서 일반적인 RDBMS에서의 작업 과정을 살펴보도록 하죠.

우선 DBMS에 Connection을 해야 합니다. 그리고나서는 적절한 쿼리를 DBMS로 전송을 하게되고

DBMS에서는 쿼리를 파싱한 후 쿼리가 요청하는 내용에 대해 Resultset을 돌려주게 됩니다.

이후에는 개발자가 Resultset을 가지고 적절히 화면을 구성하여 데이터를 화면에 뿌려주게 되는

것이죠.


이 과정을 코어 데이터에 적용을 시켜보면

우선 managedObjectContext를 DBMS에 대한 연결 정보라고 생각하셔도 될 것입니다.

이 managedObjectContext의 인스턴스를 얻었다는 것은 DB에 연결하여 데이터를 가져올 준비가

되었다는 의미입니다.


그러면 이제 쿼리를 전송하고 쿼리에서 요청한 결과셋을 가지고 와야 하는데요.

이러한 작업을 수행하는 객체가 바로 NSFetchedResultsController클래스의 객체입니다.

NSFetchedResultsController클래스의 인스턴스를 통해 다음의 메서드를 수행함으로써

영구 저장소로부터 필요한 데이터들의 결과값을 가져오게 됩니다.


- (BOOL)performFetch:(NSError **)error


실제 사용 예는 다음과 같습니다.


NSError *error;

if (![[self fetchedResultsController] performFetch:&error]) {

// Update to handle the error appropriately.

NSLog(@"Unresolved error %@, %@", error, [error userInfo]);

exit(-1);  // Fail

}


여기서 fetchedResultsController인스턴스는 fetchedResultsController라는 메서드를 통해서

얻게 되는데요. 이 때 fetchedResultsController 서드 내에서 managedObjectContext가 인자로

쓰이게 됩니다.


fetchedResultsController의 내용은 다음과 같습니다.


- (NSFetchedResultsController *)fetchedResultsController {

    if (fetchedResultsController != nil) {

        return fetchedResultsController;

    }

    

// Create and configure a fetch request with the Book entity.

NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];

NSEntityDescription *entity =

            [NSEntityDescription entityForName:@"DiaryData" 

                                 inManagedObjectContext:managedObjectContext];

[fetchRequest setEntity:entity];

// Create the sort descriptors array.

NSSortDescriptor *birthdayDescriptor =

            [[NSSortDescriptor alloc] initWithKey:@"writedate" ascending:YES];

NSArray *sortDescriptors =

            [[NSArray alloc] initWithObjects:birthdayDescriptor, nil];

[fetchRequest setSortDescriptors:sortDescriptors];

// Create and initialize the fetch results controller.

NSFetchedResultsController *aFetchedResultsController =

         [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest  

          managedObjectContext:managedObjectContext sectionNameKeyPath:nil  

          cacheName:@"Root"];

self.fetchedResultsController = aFetchedResultsController;

fetchedResultsController.delegate = self;

// Memory management.

[aFetchedResultsController release];

[fetchRequest release];

[birthdayDescriptor release];

[sortDescriptors release];

return fetchedResultsController;

}


한 줄 한 줄 분석을 해보면 먼저 fetchedResultsController도 지연로딩을 사용하고 있습니다.

fetchedResultsController 인스턴스가 존재하면 기존 인스턴스를 반환하고 그렇지 않으면

새로 생성을 하여 반환합니다.


if (fetchedResultsController != nil) {

return fetchedResultsController;

}


다음에는 쿼리문을 준비하는 과정입니다.


NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];

NSEntityDescription *entity =

            [NSEntityDescription entityForName:@"DiaryData" 

                                 inManagedObjectContext:managedObjectContext];

[fetchRequest setEntity:entity];


fetchRequest 인스턴스를 하나 만들고 managedObjectContext내에 있는 EntityDescription으로부터

'ChildData'라는 이름의 Entity(테이블)를 가져와서 쿼리에 사용하겠다고 설정하는 것입니다.

즉, 'ChildData'라는 테이블을 쿼리의 대상으로 하겠다고 선언한 것입니다.


※ 주의할 것은 이 때의 ChildData는 테이블을 의미하고 있지만 나중에 나오게될 ChildData의 인스턴스는

ChildData라는 테이블에 저장되어있는 Row에 해당한다고 보셔야 한다는 것입니다. 이 내용은 이후 다시 한 번

반복하도록 하겠습니다.


다음의 내용은 SQL문의 order by절에 해당한다고 보시면 됩니다.

정렬 기준 property(컬럼)와 정렬 방법(오름차순 혹은 내림차순)을 지정하게 됩니다.


NSSortDescriptor *birthdayDescriptor =

            [[NSSortDescriptor alloc] initWithKey:@"writedate" ascending:YES];

NSArray *sortDescriptors =

            [[NSArray alloc] initWithObjects:birthdayDescriptor, nil];

[fetchRequest setSortDescriptors:sortDescriptors];


두 번째 줄을 보시고 짐작들 하셨겠지만 NSSortDescriptor는 여러개를 지정할 수 있습니다.

NSSortDescriptor의 배열이 최종적으로 fetchRequest에 지정되는 것이죠.


여기까지 해서 쿼리(fetchRequest)에는 2가지 정보가 설정되었습니다.

데이터를 불러올 대상 테이블과 데이터를 불러올 때의 정렬에 대한 정보입니다.


마지막으로 이렇게 설정된 쿼리문을 가지고 managedObjectCContext로부터 Resultset을 가져오게 되는

문장이 바로 다음 내용입니다.


NSFetchedResultsController *aFetchedResultsController =

         [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest  

          managedObjectContext:managedObjectContext sectionNameKeyPath:nil  

          cacheName:@"Root"];

self.fetchedResultsController = aFetchedResultsController;

fetchedResultsController.delegate = self;


세번째 인자는 테이블 뷰를 사용하게 되는 경우 섹션 이름으로 사용하게 될 property(컬럼)를 지정하는 것이고

마지막 인자는 캐시로 사용할 파일 이름입니다.


여기까지 하면 일단 기본 기능은 처리가 됩니다. 해당 Entity의 모든 데이터들을 NSSortDescriptor들의

기준에 따라 불러오게 되는 것이죠.


하지만 이 것만으로는 당연히 부족합니다. 단지 1건의 데이터를 사용하기 위해서 100건 1000건의 데이터를

불러와야 한다면 엄청난 낭비겠죠. 물론 100건 1000건 불러온다고 해서 엄청난 부하가 걸리거나 하진

않겠지만 말이죠…^^;;; 물론 이를 해결할 옵션 메서드들이 있지만 말입니다. 자세한 내용은 API 문서를

참고하세요.


어찌되었건 여기까지 정상적으로 데이터를 불러와서 이제 사용하기만 하면 되는 시점에 왔네요.


3. TableViewController에서 사용하기


UITableViewController는 UITableViewDataSource 프로토콜을 구현하고 있는데요.

여기에는 테이블 뷰 이용시 필수라고 할 수 있는 몇가지 메서드가 구현되어야 합니다. 다음과 같은 메서드들이죠.


- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView

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

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

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section


프로토콜 이름에서도 알 수 있듯이 이 메서드들은 모드 데이터 소스와 관련된 내용이고 따라서 필요한

중요 정보들을 코어 데이터 처리를 통해 가져온 fetchedResultsController 인스턴스를 통해 가져오게 됩니다.

주요 사용 정보는 section 정보입니다. 물론 fetchedResultsController 생성시  sectionNameKeyPath:nil

코딩한 경우에는 섹션이 나누어지지 않기 때문에 섹션 수는 1로 나오겠죠.


섹션 수를 가져오는 코딩입니다.


- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {

NSInteger count = [[fetchedResultsController sections] count];

.

.

.

}


리고 각 섹션에 포함된 데이터 수를 가져오는 코딩입니다.


- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {

NSInteger numberOfRows = 0;

if ([[fetchedResultsController sections] count] > 0) {

id <NSFetchedResultsSectionInfo> sectionInfo = [[fetchedResultsController sections] objectAtIndex:section];

     numberOfRows = [sectionInfo numberOfObjects];

}

.

.

.

}


다음으로 사용하는 정보는 특정 인덱스에 있는 실 데이터의 클래스(ManagedObject 타입의 클래스)를

가져오게 됩니다.


데이터 클래스를 가져오는 코딩입니다.

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

diaryData =

(DiaryData *)[fetchedResultsControllerobjectAtIndexPath:indexPath];

.

.

.

}


마지막으로 섹션 이름을 가져오는 코딩입니다. 섹션이름은 fetchedResultsController 생성시 sectionNameKeyPath:에 넣어준 값입니다.

물론 sectionNameKeyPath:nil로 한 경우 이 값은 없죠.


- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {

return [[[fetchedResultsController sections] objectAtIndex:section] name];

}



※ 참고로 이 때 사용되는 인덱스 정보는 index가 아니라 indexPath입니다. 이름에서도 알 수 있듯이 특정 위치의

인덱스 값 하나만을 의미하는 것이 아니라 여러층으로 중복된 배열 구조의 인덱스 경로를 모두 표시하는

객체라고 볼 수 있겠죠. 이 것을 통해 fetchedResultsController의 구조나 fetchedResultsController로부터

실 데이터를 불러오는 과정을 유추해볼 수 있습니다.


즉, fetchedResultsController는 중첩된 배열 구조로 section이라는 배열이  있고 이 배열은 그 요소로

데이터  Row의 배열을 가지고 있는 2차원 배열 구조로 보시면 된다는 것입니다. 아래 그림과 같겠죠.






만일 'Row12'라는 데이터를 찾아야 한다면 우선 section 인덱스인 2를 찾아야 하고 다음 데이터의 인덱스인

1을 찾아가야 해서 2 -> 1의 경로를 가져야 합니다. 바로 이러한 인덱스의 경로를 indexPath가 표현하고

있는 것이죠.


두말할 것 없이 자세한 내용은 API 문서를 참고하세요…^^;;;




※ 앞서도 언급했지만 Entity와 데이터 Row를 잘 구분하셔야 합니다.

일단 .xcdatamodel을 통해 생성된 Entity들은 기본적으로 NSManagedObject타입의 클래스입니다.

위의 예제 코드에서는 'DiaryData'라는 Entity에서 데이터를 가져온 것입니다. 하지만 실제로


(DiaryData *)[fetchedResultsController objectAtIndexPath:indexPath];


이 코드를 통해 가져온 DiaryData 클래스의 인스턴스는 Entity를 표현한 것이 아니라 Entity

저장된 Data Row, 즉, 한 건 한 건의 데이터를 의미한다고 보시는 것이 옳을 것입니다.



여기까지 진행되었다면 일단 화면상의 테이블 뷰에 데이터 리스를 뿌리는 것 까지는 문제없이 실행될 것입니다.


4. 요약


오늘도 쓸데없이 말만 길어졌고 내용은 여전히 혼란스럽네요.

그렇기 때문에 저 역시 여러분의 도움이 많이 필요합니다…^^;;; 제가 틀렸거나 잘못 이해하고 있는 부분은

언제든지 말씀을 해주세요.


일단 지난 시간에 managedObjectContext까지 만들었고 오늘은 이 managedObjectContext를 이용하여 실제 데이터를

가지고 있다고 볼 수 있는 fetchedResultsController를 생성한 후 테이블 뷰 컨트롤러를 이용하는 화면에서 이

fetchedResultsController를 이용하여 UITableViewController(UITableViewDataSource 프로토콜)의 몇몇 메서드를

이용하여 section정보와 실 데이터 row의 정보를 가져오는 방법을 알아보았습니다.


하지만 아직 predicate는 사용하지 않았네요…^^;;;


5. 마무리


여기까지 일단 코어 데이터의 여러 객체들을 이용하여 테이블 뷰에 데이터를 출력하는 내용까지는

정리를 해보았습니다. 하지만 꼭 데이터를 테이블 뷰에만 출력하는 것은 아니죠.

그래서 다음시간에는 FetchedRequest 객체와 Predicate 객체를 이용하여 데이터 한 건만 불러와 화면에

보여주는 내용으로 Core Data에 관한 내용을 마무리하도록 하겠습니다.

저작자 표시
신고

최초 작성일 : 2010/09/03 01:47 


우선 먼저 양해 말씀 드릴 것은 제가 이 실전 소스 분석을 진행하는 목적은

애플이 제공하는 API에 대한 자세한 설명이 아니라 이제 시작하는 개발자로서

일종의 시행착오 경험을 공유하고자 하는데 있습니다. 따라서 기본 API에 대한

설명은 과감하게 생략을 하고 넘어갑니다. 이점 양해 부탁드립니다.


==============================================


1. 아이폰에서의 데이터 관리


이번 분석에서는 일반적인 진도상으로는 좀 이른 감이 있지만 소스의 흐름상 먼저 등장을 하고 있으므로

코어 데이터에 대해 다뤄보도록 하겠습니다.


잘 알고들 계시듯이 아이폰에서는 여러가지 방식의 데이터 저장 공간을 사용할 수 있습니다.

SQLite를 직접 사용하는 방법, 바이너리 파일을 사용하는 방법

(여기에는 다시 property list를 사용하는 방법과 객체 archiving을 이용하는 방법 2가지가 있습니다.),

그리고 마지막으로 코어 데이터를 이용하는 방법.


각각의 특징을 보자면 다음과 같습니다.


1.1 SQLite

우선 가장 접근하기 쉬운 것은 SQLite를 직접 사용하는 방법이라고 생각됩니다.

기존에 웹이나 기타 RDBMS 관련 개발을 해보신 분들은 쿼리를 직접 작성하여 entity간의 관계를 지정하고

검색 조건을 위한 where절을 만들고 하면서 익숙하게 데이터 처리를 할 수 있을 것입니다.


1.2 Binary

다음으로 바이너리를 이용한 방법은 기존 언어에서 객체 직렬화라는 부분을 생각하시면 될 것입니다.

객체 직렬화(serialization)란 데이터를 저장하고 있는 클래스의 인스턴스를 그대로 바이너리 파일로

저장하는 기법입니다. JAVA의 예를 들면 HashMap을 상속받은 클래스에 나라 이름을 key로 하고

그 나라 수도를 value로 하여 저장을 한 뒤 이 클래스의 인스턴스를 파일로 저장시키는 방법입니다.


괜히 말이 복잡해졌는데 쉽게 말씀드려 텍스트 에디터로 열었을 때 이상한 문자로 보여지는 파일이

특정 애플리케이션에서는 정상적인 문자로 보여지는 파일들은 모두 이러한 객체 직렬화를 통해

저장되었다고 보시면 됩니다…^^


예를 들어 아래 한글의 hwp 파일은 텍스트 에디터에서는 이상한 문자로 보이지만 아래한글 프로그램에서는

정상적인 문서로 보이는 것이 그런 경우입니다.


1.3 Core Data

마지막으로 애플에서 구현해놓은 Core Data를 이용하는 방법입니다.

사실 제가 이 방식을 선택한 이유는 애플에서 구현해 놓은 것이기 때문에 반드시 익혀놓아야 할

필요성이 있을 것 같아서였는데 익숙하지 않은 구조로 인해 엄청 애먹었습니다…ㅠ.ㅠ

일종의 ORMapping 역할을 하는 API입니다. JAVA로 보자면 iBatis나 Hibernate에 해당하는 건데요.

원칙적으로 따지자면 개발자는 무지 편한 구조입니다. 일일이 쿼리를 작성하지 않아도

(물론 iBatis나 Hibernate에서는 최초 쿼리는 작성을 해 놓아야 합니다. 단지 재사용이 가능할 뿐이죠)

데이터를 관리할 수 있다는 것이죠.


필요한 메서드에 필요한 인자만을 넘겨서 호출하면 Core Data가 다 알아서 쿼리도 날려주고

결과 값도 돌려주고 한다는 것이죠. 하지만 이 캡슐화(객체 지향의 주용 개념 중 하나로 핵심 로직을

인터페이스 뒤로 숨기고 개발자는 외부로 드러난 인터페이스만을 통해 API를 다룰 수 있게 하는 것)는

저처럼 실력이 떨어지는 개발자에게는 무지 헷갈리는겁니다. 자꾸 몰라도 되는 부분이 궁금해져서 진도가

안나가는거죠…-.-


어쨌든 익숙해지면 가장 편한 방법이 되겠지만 세밀한 컨트롤을 원하는 개발자들에게는

다소 답답한 면이 있을 것입니다.


사실 아직도 제가 잘 모르겠는 부분은 Entity간의 relationship을 어떻게 사용하느냐 하는 것입니다.

기존 RDBMS같은 경우 쿼리를 통해 다양한 join을 만들어 데이터를 불러오는데 코어 데이터에서는

어떤 부분이 그런 기능을 하게 되는지 잘 모르겠습니다. 이 부분. 즉, 테이블간의 조인이

코어 데이터에서는 어떤 식으로 이루어지고 또 개발자들은 어떤 작업을 해주어야 하는지에

대해 아시는 분은 부연 설명 부탁드립니다...^^;;;


2. Core Data의 구조


시작이 너무 장황했네요.

암튼 결론적으로 저는 Core Data를 선택했고 생소한 개념과 객체들로 인해 상당히 헤맸고

아직도 헤매는 중입니다…^^;;;


사실 이글을 작성하는 시점에서도 이 부분을 어떻게 풀어나가야 하는 고민에 손을 못대고 있었는데

다행히 한눈에 딱 들어오는 개념도가 보였습니다.


아래 그림은 '위키북스'에서 나온 'More 아이폰 3 프로그래밍'이라는 책에 실린 그림입니다.






책에는 한글로 표시되어 있었는데 오히려 더 헷갈리는 것 같아서 제가 API용어로 다 바꿔봤습니다.

각각의 의미와 역할은 다음과 같습니다.


1. Persistent store : 영구 저장소라고 해석되고 가장 최종 데이터가 저장되는 영역입니다.

                          물리적으로는 하드디스크(메모리)에 저장된 데이터를 가지고 있는

                          바이너리 파일이라고 생각하시면 됩니다.

2. Data Object Model : Persistent store에 저장된 내용들을 논리적으로 보여주는 객체라고 생각하시면 됩니다.

                          xcode상에 보여지는 xcdatamodel 파일을 생각하시면 될 것 같습니다.

3. Persistent store Coordinator : 영구 저장소와 Managed Object Context를 이어주는 다리 역할을 한다고

                          보시면 됩니다. 항상 영구 저장소에서 뭔가 실제적인 것을 가지고 오려고 하면 직접

                          영구 저장소에 접근하는 것이 아니라 이 Persistent store Coordinator를 거쳐야 하는 것입니다.

                          '왜 그렇게 해야 하는가?'라는 질문은 잡스형님께…^^;;;

                          (이런 것도 사실 객체 지향의 일환은로 핵심적인 내용들은 보다 안정적인 위치에 감춰두고

                           개발자들에게는 제한된 인터페이스만을 제공하여 불필요한 변경으로 인한 문제를 줄이는 동시에

                           내부 API가 변경이 되더라도 개발자들은 딱히 수정을 하지 않아도 되도록 하는데 그 목적이 있겠죠.)

4. Managed Object Context : 객체 관리 컨텍스트인데요 개발자의 작업은 여기서부터 시작된다고 보셔도 됩니다.

                          위의 3가지는  최초 애플리케이션이 실행되는 시점에 한 번 코딩을 해주시면 되고 

                          이 Managed Object Context부터는 수시로 사용을 하게 됩니다.

                          물론 인스턴스를 계속 만드는 것은 아니구요.

                          자세한 것은 코딩을 보시면서…^^

5. Entity Description : Entity는 쉽게말해 RDBMS의 테이블을 생각하시면 됩니다. 특정 Entity와 관련된 각종 정보를

                          가져올 수 있는데요 Managed Object Model이나 property(RDBMS의 필드 혹은

                         컬럼이라고 생각하시면 됩니다.)

                          또는 타 Entity와의 관계(relationship) 등의 정보를 가져오거나 설정할 수 있습니다.

6. Managed Object : Managed Object Context에서 관리되는 객체 중 하나입니다.

                          쉽게 말하면 Entity를 클래스 파일로 만들어놓은 것이라고

                          생각하시면 됩니다. 이후 실제 코딩 작업 설명에 자세히 말씀드리겠지만 xcdatamodel 파일을 통해

                          구성된 Entity들은 클래스 파일로 만들 수 있습니다.

                          다만 이 클래스 파일이 항상 필요한 것은 아니고 개발자가 별도의 메서드를 추가시키고자 할 때나

                          사용을 하게 됩니다.

                          전 아무생각없이 다 만들었네요…-.-

7. Fetch Request : 속칭 '쿼리를 날린다'라는 것입니다…^^ 실제 이 과정을 통해 결과값을 받아오게 되죠.

8. Predicate : '술어'라고 해석을 하더군요. where 조건에 해당하는 객체입니다.

                  이런 부분의 사용이 아이폰 개발을 처음 접하는 개발자들을 울리게 되죠…ㅠ.ㅠ


3. 실제 소스 보기


우선 디버깅을 위해 찍은 로그를 좀 보시죠.

최초 실행되는 iPhotoDiaryAppDelegate에서 단순히 실행되는 순서대로 메서드명을 찍은 것입니다.


2010-09-02 22:43:14.742 iPhotoDiary[2610:307] DEBUG: applicationDidFinishLaunching

2010-09-02 22:43:14.753 iPhotoDiary[2610:307] DEBUG: managedObjectContext

2010-09-02 22:43:14.759 iPhotoDiary[2610:307] DEBUG: persistentStoreCoordinator

2010-09-02 22:43:14.764 iPhotoDiary[2610:307] DEBUG: applicationDocumentsDirectory

2010-09-02 22:43:14.778 iPhotoDiary[2610:307] DEBUG: managedObjectModel

2010-09-02 22:43:14.925 iPhotoDiary[2610:307] DEBUG: managedObjectContext

2010-09-02 22:43:14.931 iPhotoDiary[2610:307] DEBUG: managedObjectContext

2010-09-02 22:43:14.936 iPhotoDiary[2610:307] DEBUG: managedObjectContext

2010-09-02 22:43:14.941 iPhotoDiary[2610:307] DEBUG: applicationDocumentsDirectory


이제는 익숙해진 단어들이 보이네요.


다음은 소스입니다.


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

Debug(@"applicationDidFinishLaunching");

// 이 부분에서 앞으로 데이터를 사용할 필요가 있는 컨트롤러들에게 ManagedObjectContext들을 할당해주고 있습니다.

//    처음에는 Core Data의 구조를 몰라서 이러한 순서를 통해 ManagedObjectContext를 가져온 것이 아니라

//    각 화면 컨트롤러에서 별도로 ManagedObjectContext의 인스턴스를 만들어서 사용했더니 계속 Entity를

//    찾을 수 없다는 오류가 뜨더군요.

appMainViewController.managedObjectContext = [self managedObjectContext];

calendarView.managedObjectContext = [self managedObjectContext];

diaryListController.managedObjectContext = [self managedObjectContext];

eventListController.managedObjectContext = [self managedObjectContext];

// 요거는 다음번에 설명을 하겠지만 binary형태의 저장중 property list를 이용한 저장 관련 내용입니다.

[self writeToPlist];