맥 주소록에서 이름의 초성을 별명으로 자동저장하기

애플 스크립트와 자체제작한 명령줄 도구를 사용해서 OSX 주소록의 이름 중 한글의 초성을 따내 이를 자동으로 별명으로 저장해주는 도구입니다.

사용방법

  1. 글 맨 하단의 다운로드 링크를 통해 파일을 내려받는다.
  2. 압축을 해제하고 속에 들어있는 “별명추가” 스크립트를 실행한다.
  3. 이후 아이폰과 동기화하면 아이폰에서도 초성으로 주소록 검색이 가능해진다.

주의사항

  • 스크립트와 명령줄 도구는 공개하지만, 이를 사용한 결과는 순전히 자신의 책임입니다.
  • “이름(first name)” 항목만 변환합니다. “성(last name)” 필드는 검색하지 않아요.
  • 초성변환은 Chosung 앱이 합니다. 터미널에서 Chosung <한글문자열> 을 입력하면 해당 문자열의 초성만 반환하니 따로 쓰셔도 됩니다.

다운로드링크 : http://www.box.com/s/0lcmd3rxl9klfgipltbi

CGLayer를 사용한 핑거 드로잉 구현 (Objective-C)

코어 그래픽(Core Graphics)은 저수준의 드로잉 명령 API들을 통해서 화면이나 비트맵이미지, PDF 등에 시각적 요소를 그릴 수 있게 하는 프레임워크이다. 예전에는 Quartz, CoreGraphics라는 이름으로 분리되어 있었는데 iOS5 부터 UIKit의 일부로 완전히 편입되었다. 간단한 모양의 시각적 오브제를 표현하기 위해 비트맵 이미지를 사용하는 것보다 런타임에 오브제를 빠르게 그리고, 이를 재사용할 수 있게 하는 등의 기능을 제공한다. 실제로 많은 앱들이 현재에도 코어 그래픽을 사용해서 UI를 표현하는 경우가 많이 있다. 이번 글에서는 코어 그래픽 API를 사용해서 손가락으로 화면에 그림을 그리는 간단한 캔버스 앱을 구현하는 방법을 살펴보기로 하겠다.

코어 그래픽을 사용할 때에는 이 프레임워크의 핵심 객체인 그래픽 컨텍스트에 대한 이해가 필요하다. 그래픽 컨텍스트는 개념상, 가상의 캔버스라 생각하면 된다. 우리는 그래픽 컨텍스트라는 이 가상의 캔버스에 그림을 그리게 되고, 쿼츠 엔진은 이 가상의 캔버스에 그려진 그림을 필요한 출력으로 가져다 렌더링한다. 따라서 그래픽 컨텍스트는 장치 독립적인 성격을 가지며, 하나의 그래픽 컨텍스트는 다른 장치를 위한 그래픽으로 쉽게 전환이 가능하다. 따라서 그래픽 컨텍스트에 적용된 그래픽은 아이폰 및 아이패드용 화면 출력 뿐 아니라, 인쇄나 PDF를 만들기도 쉽게 지원된다. 실제로 macOS를 보면 모든 뷰는 PDF로 변환이 가능하고, 화면에 그려지는 모든 것이 PDF인 동시에 PNG일 수 있는데, 이것은 macOS의 드로잉 체계가 컨텍스트라는 개념을 중심으로 장치독립적으로 추상화되어 있기 때문에 가능한 것이다.

모든 화면에 출력되는 모든 뷰는 콘텐츠를 시각적으로 표현하는 도구이며, 그 콘텐츠를 뷰에 제공해주는 주체가 바로 그래픽 컨텍스트이다. 가상의 캔버스인 컨텍스트에 코어 그래픽 API를 사용하여 그림을 그리면 이 데이터가 그래픽 버퍼로 덤프되고, 그 결과 이미지가 화면에 뿌려지게 된다. 보통 특정한 뷰에 이렇게 그림을 그리기 위해서는 해당 뷰에서 “현재 컨텍스트”를 얻고 여기에 그림을 그리면 된다. 물론 컨텍스트는 별도로 생성할 수 있다. 별도로 생성한 컨텍스트에 그림을 그리는 것은 일종의 그래픽 데이터를 메모리 내에 준비하고 있는 것이 된다.

만약 추가적으로 생성한 그래픽 컨텍스트와 CGLayer를 결합하면, 컨텍스트의 데이터가 레이어의 콘텐츠를 제공하게 되고, 다시 이 레이어는 다른 컨텍스트에서 일종의 스탬프처럼 찍어서 사용할 수 있다. 이런식으로 직접 화면에 그리는 것이 아니라, 컨텍스트에 미리 그림을 그려놓고 이것을 재활용하여 반복적인 문양을 그리는 것을 (왜냐면 CGLayer는 재사용할 때 캐시된다!) 오프스크린 드로잉이라고 한다. 특히 오프스크린 드로잉은 백그라운드 스레드에서 처리가 가능하다는 장점이 있다. (iOS에서는 화면에 무언가를 그리기 위해서는 반드시 메인스레드에서 작업해야 한다.) 따라서 워커 스레드에서 미리 콘텐츠를 제작해놓고 메인스레드에서는 최종 결과물만 업데이트하는 식으로 처리하여 화면의 빠른 드로잉이 가능하게 할 수 도 있다.

플로우

핑거 드로잉은 말 그대로 화면에 손가락을 터치하고, 손가락이 터치해서 움직이는 경로를 따라 화면에 선이나 무늬를 그려넣는 것을 말한다. 즉, 가장 기본적인 인터랙티브 드로잉 방법이다. 이 핑거 드로잉을 지원하는 캔버스 뷰를 작성해보도록 하겠다. 기본 개념은 다음과 같다.

┌───CanvasView───┐                  ┌─Offscreen Layer─┐
│  View'sContext │                  │ Layer's Context │  
│                │    touch --->    │                 │
│                │                  │                 │
│                │                  │                 │
│                │       Draw       │                 │
└────────────────┘ <--------------- └─────────────────┘
  1. 뷰와 크기가 똑같은 레이어를 하나 준비한다.
  2. 손가락이 움직이는 궤적은 레이어에 그려진다.
  3. 레이어에 그림이 그려진 후에는 뷰에 레이어를 그린다.

실제로 손가락이 움직이는 궤적은 눈에 보이지 않는 캔버스에 그림을 그리는 것이다. 그리고 이렇게 그려진 데이터를 뷰에 언제 그리느냐에 따라서 손가락을 움직이는 사이사이에 선이 그려지게 할 것인지, 아니면 손가락을 떼는 시점에 그림이 나타나게 할 것인지를 결정할 수도 있다.

프로젝트 시작

새 프로젝트를 하나 만든다. 어차피 UIView 클래스를 새로 하나 만드는 것이 사실 구현의 전부이므로, Single View App으로 시작한다. 프로젝트를 생성하였으면, 새 파일을 추가한다. Objective-C 을 언어로 정하고, 클래스는  UIView를 선택한다. 이름은 CanvasView 정도가 좋을 것 같다.

앱 실행시에 해당 뷰가 전면에 표시되도록 이 캔버스 뷰가 들어갈 뷰 컨트롤러의 파일에서 viewDidLoad를 다음과 같이 수정해서 루트 뷰에 캔버스뷰를 추가한다. (혹은 UI빌더에서 UI뷰를 하나 삽입하고, 그 클래스를 CanvasView로 선택해도 된다.)

viewcontroller.h 수정하기

기본으로 세팅된 메인 뷰가 로드되면 캔버스뷰를 만들어서 자기 위에 얹도록 코드를 작성한다. 만약 스토리보드에서 메인 뷰의 클래스를 Canvas로 변경했다면 이 코드는 작성하지 않는다.

#import "CanvasView"
/* ... */
-(void)viewDidLoad {
    CanvasView *canvas = [[CanvasView alloc] 
                         initWithFrame:self.view.frame];
    [self.view addSubView:canvas];
}

캔버스뷰의 인스턴스 변수 정의

CanvasView의 헤더를 작성하자. 두 개의 인스턴스 변수를 선언한다. 뷰 내에서 오프스크린 드로잉을 담당할 레이어를 위한 CGLayerRef 변수와, 그 레이어에 그림을 그릴 수 있는 CGContext 타입 변수를 선언한다. 이 두 변수는 OpaqueType 이며, Objective-C 클래스가 아니므로 * 를 붙이지 않음에 유의하자.

@import UIKit;

@interface CanvasView: UIView
{
  CGContextRef layerContext;
  CGLayerRef drawingLayer;
} @end

초기화

초기화작업은 통상의 UIView의 초기화 프로세스를 따른 후, 두 개의 인스턴스 변수에 대한 초기화를 수행한다. 이 때의 순서는 다음과 같다.

  1. 비트맵 컨텍스트를 하나 생성한다.
  2. 1의 컨텍스트를 기반으로 CGLayer를 생성한다. 사실, 1에서 컨텍스트를 만들지 않고 참조 컨텍스트로는 NULL을 넣어도 상관없다. 하지만 넣어주는 경우에 조금 더 최적화된다고 한다.
  3. 2에서 생성한 레이어로부터 실제 레이어의 콘텐츠를 담을 컨텍스를 얻어서, 이를 drawingContext 값으로 대입한다.
  4. drawingContext를 설정한다. 선의 색이나, 굵기, 끝모양 등의 정보를 지정할 수 있다.
#import "CanvasView.h"

@implementation CanvasView
-(id) initWithFrame: (CGRect)frame
{
  self = [super initWithFrame: frame];
  if (self) {
    [self initContext];
  }
}

-(id) initWithCoder: (NSCoder*)aDecoder
{
  self = [super initWithCoder: aDecoder];
  [self initContext];
}

-(void) initContext {
  CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB();
  CGContextRef ctx = CGBitmapContextCreate(
                  NULL, 
                  10, 
                  10, 
                  8, 
                  0, 
                  cs, 
                  kCGBitmapAlphaPremulipliedLast);
  // 각각의 파라미터는 다음과 같은 의미이다.
  // 1 : NULL - 비트맵을 저장할 메모리 블럭, NULL을 넘겨주면 이 함수가 자동으로 할당한다.
  // 2, 3 : 10, 10 은 컨텍스트의 픽셀 크기이다. 
  // 4 : 8 - 컴포넌트당 비트. RGBA가 각각 8비트, 총 32비트 트루컬러를 사용할 것이다.
  // 5 : 0 - 한 줄당 바이트. 픽셀당 4바이트를 사용하고 총 10픽셀 폭이니 40이 될 것이다. 이는 1번 파라미터가 NULL인 경우
  //         0을 넘겨서 자동계산하게 할 수 있다. 
  // 6 : cs - 컬러스페이스
  // 7 : 비트맵정보 - 미리 정의된 CGImageAlphaInfo 상수를 사용한다.

  // 레이어와 컨텍스트 생성/초기화.
  drawingLayer = CGLayerCreateWithContext(ctx, self.bounds.size, NULL);
  drawingContext = CGLayerGetContext(drawingLayer);
  // 컨텍스트에 그림을 그리는 방법을 세팅한다.
  CGContextSetStrokeColorWithColor(drawingContext, [[UIColor redColor] CGColor]);
  CGContextSetLineWidth(drawingContext, 4.8f);

  // 참조용으로 생성한 객체들을 정리한다. 
  CGContextRelease(ctx);
  CGColorSpaceRelease(cs);
}

이 코드에서 주목해야 할 점은 맨 처음 비트맵 컨텍스트를 생성할 때, 사이즈를 화면 크기가 아니라 제멋대로 주었다는 점이다. 이 컨텍스트를 기반으로 레이어를 만들 때, 레이어만 뷰의 크기와 일치시켰다. CGLayerCreateWithContext() 함수에서 흔히 잘못알고 있는 점은 이 함수에서 넘겨지는 그래픽 컨텍스트 객체는 레이어에 그려지는 그림과는 실제 무관할 수 있다는 점이다. 실제로 비트맵 컨텍스트를 만들지 않고 NULL을 전달하여도 코드는 정상적으로 동작한다. 그렇다면 이는 왜 필요한 것일까?

그래픽 컨텍스트는 장치독립적인 페이지이며, 이 페이지는 앱의 윈도에 그려지거나, 프린터로 출력되거나 혹은 비트맵 이미지로 고정될 수 있다. 이 때 각각의 출력 디바이스에 따라서 다른 정보들이 사용되고, 이는 내부적인 타입이 구분되어 사용되는 것으로 이해할 수 있다. CGLayerCreateWinContext()함수에서 전달받는 컨텍스트 인자는 CGLayer를 생성할 때 캔버스로 사용할 컨텍스트를 받는 것이 아니다. 레이어를 생성할 때, 레이어의 컨텍스트는 별도로 생성되는데, 이 때 인자로 전달받은 컨텍스트의 속성을 참조하여 생성되는 컨텍스트를 최적화한다. (실제로 이를 NULL로 전달하는 것보다, 이렇게 간단하게 만들어서 전달하는 경우, 드로잉 성능이 더 좋다.) 즉, ctx != CGLayerGetContext(drawingLayer) 이다.

 

터치 동작 구현

UIView에서 터치가 움직일 때, 뷰는 touchesMoved:withEvent 메시지를 받는다. 이 메소드를 오버라이딩하여 움직인 만큼 부분에 선을 그려넣도록 한다. 선을 그려넣는 작업은 현재 뷰의 컨텍스트가 아닌 drawingContext이다. 여기에 콘텐츠를 그려넣은 다음에 뷰에는 레이어를 그려넣으면 된다.

- (void)touchesMoved:(NSSet<NSTouch *>*)touches withEvent:(UIEvent *)event
{
  CGPoint lastTouch, currentTouch;
  UITouch *touch = [touches anyObject];
  lastTouch = [touch previousLocationInView: self];
  currentTount = [touch locatioinInView: self];

  // 선을 그리자. 
  // 이전 위치로 이동후 현재 위치로 선을 추가한다. 
  CGContextBeginPath(drawingContext);
  CGContextMoveToPoint(drawingContext, lastTouch.x, lastTouch.y);
  CGContextAddLineToPoint(drawingContext, currentTouch.x, currentTouch.y);
  CGContextStrokePath(layerContext);

  // 그려진 레이어를 뷰에 반영하기 위해 뷰 업데이트를 스케줄링한다.
  [self setNeedsDisplay];
} 

이상의 구현에서 특별한 점은 없다. 여기서는 터치가 조금씩 움직이는 주기마다 컨텍스트에 선을 추가하고, 뷰를 업데이트하도록 한다. 만약 손가락을 뗐을 때만 뷰가 업데이트되도록 하려면 [self setNeedsDisplay];를 touchesEnded:withEvent:에서 호출하도록 한다.

뷰 업데이트

setNeedsDisplay 메시지를 받으면 뷰는 자신의 상태가 유효하지 않다는 것을 감지하고 뷰 영역을 새로 그리려고 시도한다. 이는 drawRect: 메시지를 호출하여 그리게 된다. 이미 지금까지 그려놓은 모든 페인팅은 drawingContext에 남아있고, 이를 뷰에 찍기 위해서는 drawingLayer를 그려주면 된다. 여기서는 뷰의 현재 컨텍스트를 이용해서 레이어를 그리면 된다.

- (void)drawRect: (CGRect) rect
{
  [super drawRect:rect];
  CGContext ctx = UIGraphicsGetCurrentContext();
  CGContextDrawLayerInRect(ctx, self.bounds, drawingLayer);
}

정리

이제 모든 소스 내용을 검토해보자.

  1. 앱이 시작되면 메인 뷰에 캔버스 뷰 인스턴스를 만들어서 붙인다.
  2. 캔버스 뷰는 생성되면 자신의 크기와 같은 오프스크린 레이어와 레이어에 그림을 그릴 컨텍스트를 생성한다.
  3. 뷰가 터치를 받고, 터치가 움직이면 이 궤적을 따라서 선을 만들 수 있고, 이 선은 레이어의 컨텍스트에 추가된다.
  4. 선을 그릴 때마다 뷰는 업데이트 요청을 받고, 컨텍스트의 비트맵 데이터가 투영된 레이어가 뷰에 찍힌다.

조금 더 깊이

코어 그래픽 컨텍스트는 장치 독립적인 가상 캔버스라고 했다. 그리고 어떤 장치를 통해서 표현되느냐에 따라서 비트맵 형식일 수도 있고, PDF 형식일수도 있다. CGLayerCreateWithContext() 에서 넘겨지는 컨텍스트는 실제로 생성된 레이어의 컨텍스트를 특정 타입으로 최적화하기 위해 필요하며, 생성된 레이어에 대해 CGLayerGetContext()로 얻게되는 컨텍스트와는 동일할 수도, 그렇지 않을 수도 있다. 따라서 이 둘이 같은 것이라는 어떠한 가정도 해서는 안된다. 실제로 drawingContext를 비트맵 컨텍스트로 생성한 후, 이 컨텍스트를 참조로 레이어를 만든다면 컨텍스트에 그린 그림이 레이어에 전혀 반영되지 않을 것이다.

 

20120103 :: [팁] 프로토타입 셀을 서브클래싱하기

프로토타입 셀에 라벨 등의 서브 뷰를 삽입하면 각각의 서브뷰에 tag 값을 주어 -viewWithTag: 메소드를 통해 접근이 가능하다. 하지만 태그를 일일이 외우는 것은 직관적이지 못하므로 (물론 편리하긴하지만) 셀의 하위 구조가 복잡한 경우에는 그닥 즐겨 쓸만한 일이 아니다. 이를 아울렛을 통해 접근하고 제어할 수 있다면 좀 더 편리할 것이다.

이런 경우에는 프로토타입 셀을 서브 클래싱할 수 있다. 즉 UITableViewCell 클래스의 하위 클래스를 만들고 이를 IB에서 명시해주면 된다. 먼저 스토리보드에서 테이블 뷰 안에 있는 프로토타입 셀에 이것 저것 넣고 싶은 레이블이나 이미지뷰등을 넣는다.

원하는 서브 뷰에 대해서는 아울렛 이름을 생각해 본 다음, 다음과 같이 클래스를 하나 추가한다.

//SampleCell.h

@interface SampleCell : UITableViewCell

@property (nonatomic, strong) IBOutlet UILabel *nameLabel;
@property (nonatomic, strong) IBOutlet UILabel *gameLabel;
@property (nonatomic, strong) IBOutlet UIImageView *ratingImageView;
@end

//SampleCell.m

#import “SampleCell.h”
@implemetation SampleCell
@synthesize nameLabel, gameLabel, ratingImageView;
@end

스토리보드(혹은 xib)에서 프로토타입 셀을 선택해 클래스 속성을 SampleCell로 변경한다. 왼쪽 객체 트리구조 리스트에서 셀의 각 하위뷰로 드래그하여 아울렛을 연결해준다. 이 작업이 완료되면 테이블 뷰는 reuse identifier에 대해 sampleCell 의 인스턴스를 반환하게 된다. (이 때, 중요한 것은 테이블뷰 셀에 아울렛을 끌어와야 한다는 점이다. 테이블 뷰나 뷰 컨트롤러에 이를 끌어와서는 곤란하다!!!)

프로토타입 셀의 클래스 이름과 Reuse Identifier 간에는 어떤 상관 관계도 없으므로 같은 이름을 주지 않아도 상관없다.

20120102 :: [iOS] 저장이 가능한 간단 메모장 3 (코어데이터)

해당 포스트의 코드가 너무 부끄러운 수준으로 디자인이 잘못돼 있어서 새롭게 작성한 글이 있으니 이 글을 참고해주세요.

이미 세 개의 포스팅(관련글 1, 관련글 2, 관련글 3)을 통해 간단한 메모장 앱을 만드는 방법을 살펴보았는데, 이번에는 완전히 똑같은 앱을 코어데이터를 사용하여 생성하는 방법에 대해 살펴보도록 하겠다. 이 시리즈의 맨 처음에 코어데이터에 대해 언급하면서 초보자에게는 좀 많이 어렵다고 이야기한 바 있는데, 이는 실제로 코어데이터가 사용하기 어려운 프레임워크라는 의미라기보다는 코어데이터를 실제로 사용하기 위해서 알고 있어야 하는 배경 지식이 상당히 많다는 의미라고 보는 것이 정확할 듯 하다. 20120102 :: [iOS] 저장이 가능한 간단 메모장 3 (코어데이터) 더보기

20111222 :: [iOS] 저장이 가능한 간단 메모장 2 (2/2)

이번 시간에는 지난 글에 이어 앱을 작성된 메모를 테이블뷰에 보여주고, 이를 편집하고 삭제하는 기능을 추가해 보도록 하겠다. 사실 메모장 만들기의 핵심 파일에 데이터를 저장하는 것이고 맨 처음 글과의 차이점은 여러 개의 메모를 객체로 만들어서 파일에 저장하는 방법을 서술했다. 마지막 글은 앱의 모양새를 조금 다듬는 정도가 되겠지만, 테이블뷰 사용에 익숙하지 않은 초보자에게는 좋은 읽을 거리가 될 수 있도록 하겠다.

테이블 뷰

테이블 뷰는 iOS에서 주로 목록을 만들 때 많이 사용하게 된다. 가장 쉬운 예로는 연락처 앱의 메인 UI를 생각할 수 있으며, 조금 다른 모양새이기는 하나 설정 앱도 테이블 뷰를 기반으로 하고 있다. 테이블 뷰는 여러 개의 셀을 나열하는 조금 독특한 형태의 뷰로, 이 테이블뷰를 구현하기 위해 테이블 뷰 컨트롤러는 2가지의 프로토콜을 따라야 한다. 그것은 각각 UITableViewDelegateUITableViewDataSource이다.

데이터소스는 테이블뷰의 각 셀에 표시되는 내용을 만들어주는 역할을 한다. 즉 테이블 뷰에 섹션이 몇개있고, 각 섹션에는 몇 개의 셀이 있으며, 각 셀에는 어떤 내용이 들어가야 하는지를 알려주는 것이다. 테이블 뷰는 자신을 그려야할 때 데이터소스로 지정된 객체에 일련의 메시지를 보내어 자신이 렌더링 되어야 하는 모양을 알게 된다. (그리고 이 때 보내는 메시지들은 UITableViewDataSource에 선언되어 있다.)

테이블뷰 델리게이트는 테이블 뷰의 각 셀을 탭하거나 지우거나 편집하는 등의 액션과 관련이 있다. 단지 테이블뷰가 내용만을 보여주고 별다른 사용자 상호작용을 하지 않는 경우에는 특별히 구현해야 하는 필요가 없다.

이제, 테이블 뷰 컨트롤러로 만들어진 RootViewController를 수정하여 저장된 내용을 보여주고, 셀을 탭하여 기존 내용을 편집하는 방법에 대해 살펴보도록 하겠다.

인터페이스

실제 데이터는 앱 델리게이트가 가지고 있으며, 테이블 뷰 컨트롤러에 필요한 것은 각 셀의 내용을 담는 배열하나 뿐이다. 따라서 인터페이스 부분은 다음과 같이 만들어진다.

#import <UIKit/UIKit.h>
#import "AppDelegate.h"
@interface RootViewController : UITableViewController
{
    NSArray *itemListArray;
    AppDelegate *appDelegate;
}
@end

구현부

테이블 뷰 컨트롤러의 구현 부 중 데이터 소스를 먼저 살펴보자. 데이터 소스는 테이블 뷰를 새로 그릴 때마다 “섹션은 몇 개”이며 “각 섹션에는 셀이 몇 개” 있고, “각 셀은 이러이러하다”는 내용을 테이블 뷰에게 알려주는 메소드들이다. 따라서 이 세가지 메소드는 필수적으로 구현해야 하며 각 메소드의 이름은 다음과 같다.

  • – (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
  • – (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
  • – (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath

이 중 마지막 tableView:cellForRowAtIndexPath: 가 가장 중요한데, 테이블 뷰는 알려진 셀의 개수만큼 루프를 돌면서 이 메소드를 호출하게 되고, 매 호출시 마다 idnexPath에 담긴 정보에 따라 해당 셀을 반환받게 된다.  또 한가지 중요한 것은 테이블 뷰는 아주아주 큰 크기를 가질 수 있는데 예를 들어 5000개 정도의 길이를 갖는 테이블 뷰가 있다고 하면 이 뷰를 채울 테이블 뷰 셀 역시 5000개가 필요하나, 메모리에 큰 부담을 주게 되므로 테이블 뷰는 이미 만들어진 셀을 재활용하게 된다. 따라서 각 셀은 Reuse Identifier를 설정해 주어 이에 맞는 셀을 사용하도록 한다.

스토리보드에서 테이블 뷰의 셀을 선택하고 Reuse Identifier에 Cell 이라고 입력한다. 또한 셀의 스타일은 기본적으로 Custom으로 되어 있는데 따로 디자인을 변경할 것이 아니므로 Basic으로 변경한다. (Basic 은 텍스트라벨 하나만을 표시하는 기본 셀이다.)

다시 RootViewController.m 파일로 돌아와서 각 부를 구현한다. 테이블 뷰 컨트롤러 템플릿으로부터 만들게 되니 아마 필요한 메서드들은 기본적으로 타이핑이 되어 있다. 먼저 뷰가 로딩되었을 때이다.

-(void)viewDidLoad
{
    [super viewDidLoad];
    appDelegate = [[UIApplication sharedApplication] delegate];
    itemListArray = [[NSArray alloc] init];
}

우선 섹션은 1개만 사용하고, 셀의 개수는 메모의 개수와 같다.

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    itemListArray = [[appDelegate memoListArray] copy];
    return [itemListArray count];
}

각 셀을 표시하도록 한다.

- (UITableViewCell *)tableView:(UITableView *)tableView 
         cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *cellIdentifier = @"Cell"; //스토리보드에서 입력한 reuse identifier와 동일한 문자열
    UITableViewCell *cell = [tableView deque.... :cellIdentifier];
    // Cell의 내용을 정의
    [[cell textLabel] setText:[[itemListArray objectAtIndex:indexPath.row] title]];

    return cell;
}

상세 뷰에서 새 메모를 추가한 경우, 이를 테이블 뷰에 반영하기 위해서는 뷰가 보여질 때 테이블 뷰의 데이터를 갱신해야 한다.

-(void)viewWillAppear:(BOOL)animated
{
    [self.tableView reloadData];
}

이제 앱을 빌드하고 실행하면 기존에 생성했던 메모들이 표시되고, 새 메모를 작성하고 돌아오면 신규 메모가 리스트의 맨 상단에 표시되는 것을 볼 수 있다.

이번에는 기존 메모를 편집하는 부분이다.  먼저 스토리보드에서 테이블뷰의 셀을 선택해, 이를 디테일뷰와 연결한다. 연결 유형은 push로 한다. 이제 셀을 탭하면 디테일뷰로 이동하게 된다. 그런데 셀을 탭해도 신규 메모를 만드는 화면이 뜨게 되는데 이는 앱 델리게이트의 메모인덱스가 계속 -1 이기 때문이다. 이를 변경하는 절차가 있어야 한다.

기본적으로 테이블 뷰 델리게이트에는 셀을 선택했을 때 동작을 정의하는 메소드가 있지만, 우리는 스토리보드에서 segue를 통해 이동하므로 선택이 되기 직전에 인덱스를 바꾸는 작업을 해야 한다. 따라서 기존에 타이핑이 되어있는 메서드인 tableView:didSelectRowAtIndexPath: 를 지우고 새로운 메소드를 정의한다. 즉 선택이 되기 직전에 호출되는 메소드이다.

-(NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    [appDelegate setMemoIndex:indexPath.row];
    return indexPath;
}

이제 기존 메모를 선택하면 해당 메모의 내용이 표시되는 것을 앱을 실행하여 확인할 수 있다.

다음은 셀을 지우는 부분이다. 이 중 많은 내용은 이미 타이핑이 되어 있을 것이다.

-(void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
    if(editingStyle == UITableViewCellEditingStyleDelete){
        [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
        [[appDelegate memoListArray] removeObjectAtIndex:indexPath.row];
        [appDelegate saveData];
    }
}

여기까지 우리는 간단한 메모를 만들고 이를 편집/삭제하여 외부파일로 저장하는 앱을 만드는 방법을 살펴보았다. 언제가 될지는 모르겠지만 지금까지 만들어본 내용을 코어데이터를 사용하여 만드는 방법도 한 번 포스팅할 생각인데, 역시나 연재라는 건 부담이 크고 나는 너무 게으르기 때문에 그 언제가 언제가 될지는 모르겠다. 긴 글 읽어주셔서 감사하다.