[Cocoa] NSImage를 파일로 저장하기

NSImage를 파일로 저장하기

이미지를 파일로 저장하기 위해서는 ‘표현형식'(representation)에 대해 알아야 한다. 즉 이미지 자체는 일련의 픽셀을 모아둔 데이터인데, 우리가 흔히 알고 있는 JPG, PNG 등의 파일 포맷은 이 이미지를 각각 정해진 방식으로 ‘정리’하여 파일에 저장하기 때문이다. 표현형식은 이런 파일에 저장된 비트맵 데이터를 다시 이미지로 렌더링하는 역할을 하고, 그 반대의 역할도 수행할 수 있다.

NSImage는 1) TIFFRepresentation 메소드를 사용하여 표현형식을 구성하는 데이터로 만든다. 이 데이터를 사용하여 2) NSBitmapImageRep 객체를 생성하고, 이 객체에서부터 3) 실제 저장이 가능한 그래픽 파일 포맷의 데이터를 생성해서 ( -representationUsingType: ), 그래픽 파일 포맷으로 만들어진 데이터를 저장해야 우리가 알고 있는 ‘그림 파일’이 만들어진다.

NSImage *currentImage; // 이 이미지는 이미 존재하는 이미지로 가정한다.  
NSString *pathToSave = ... // 파일을 저장할 경로  
NSData *imageData = [currentImage TIFFRepresentation];  
NSBitmapImageRep *rep = [NSBitmapImageRep imageRepWithData:imageData];  
NSData *dataToWrite = [rep representationUsingType:NSPNGFileType  
properties:nil];  
[dataToWrite writeToFile:pathToSave atomically:NO];

이 TIFF표현형으로부터 각 파일 포맷에 대한 저장용 데이터를 생성할 수 있다. 지원하는 포맷은 다음과 같다.

  • NSTIFFFileType : TIFF
  • NSBMPFileType : BMP
  • NSPNGFileType : PNG
  • NSJPEGFileType : JPG
  • NSGIFFileType : GIF
  • NSJPEG2000FileType : JPEG2000

이 때 전달하는 프로퍼티는 사전의 형태로 파일을 저장할 때 필요한 옵션을 지정한다. PNG로 저장하는 경우에는 인터레이스 여부만 필요하다. 그외 JPG포맷의 압축 정도나 애니메이션 GIF를 저장하는 설정 등을 다룬다. 자세한 내용은 개발자 문서를 참고.

이미지의 표현형에 대해 조금 익숙해진다면, 뷰 자체를 캡쳐하여 이를 저장하는 것도 손쉽게 가능하다. NSBitmapImageRep는 initWithFocusedViewRect: 메소드가 있어서 현재 포커스 뷰의 일부 혹은 전체 내용을 잘라, 이를 데이터로 변환한다. 역시 동일한 방식으로 뷰의 내용을 그대로 저장하는 것이 가능하다.

화면을 캡쳐할 때 iOS의 경우에는 좀 다른 방식으로 접근해야 한다. 비트맵 그래픽 컨텍스트를 만들어서 이 곳에 뷰의 CALayer를 렌더링해 넣는 방식으로 CGImage를 얻을 수 있다. 그리고 이 이미지를 다시 저장하면 된다. 이에 대해서는 별도의 포스팅으로 알아보도록 하겠다.

[Cocoa] 코코아의 이름 규칙

코코아 프레임워크에서 이름 짓는 규칙에 익숙해지는 것은 단순히 코드의 가독성을 높일 뿐만아니라 부분적으로 키-밸류 코딩의 규칙을 따르는 것과도 관련이 있다. 애플의 이름 짓기 규칙은 특히나 약어를 거의 사용하지 않고 단어나 문장을 통째로 쓰는 경향이 있는데 이러면 메소드나 변수의 이름이 길어질 수는 있지만 그 의미를 명확히 이해할 수 있을 뿐만아니라, Xcode의 멋진 자동완성기능이 있어 그리 많은 타이핑이 필요하지도 않다. 이름 짓기가 잘 된 소스는 나중에 본인이 재사용할 때도 매우 명확하고 쉽게 접근할 수 있으며, 그 자체로 소스에 대한 주석이 될 수도 있다.

이름을 정할 때의 규칙 일반

  1. 간결하되 명확성을 해치지 않아야 한다.
  2. 군더더기를 빼라. 예를 들어 my나 object와 같은 접두어는 불필요하다.
  3. 축약형을 쓰지 마라. 축약할 때는 쉽지만 원래 의미는 나중에 아무도 알아보지 못한다.
  4. 이중적인 의미의 단어를 피하라. 예를 들어 displayText는 문자를 표시하는 메소드인가, 그냥 표시되는 텍스트의 이름을 나타내는 변수명인가?
  5. 일관성은 중요하다. 서로 다른 클래스에서 같은 이름의 메소드를 만들었다면, 이들 메소드는 같은 기능을 수행해야 한다.
  6. 접두어를 사용하는 것은 추천할만하다. 대부분의 파운데이션 클래스나 함수는 NS로 시작한다. (이는 NextSTEP에서 왔다는 의미이다.) 또한 아이폰 개발에 사용되는 UIKit에서는 UI로 시작하는 클래스들이 있다. 비슷한 기능을 하는 객체가 접두어를 통해 쓰임새나 출신을 파악할 수 있다.
  7. 클래스 이름에는 연관되는 명사가 있어야 한다. 또한 이들과 관련된 이름은 클래스 이름과 닮으면 된다. 예를 들어 UITableViewCell 은 클래스이고, 셀의 스타일을 지정하는 상수는 UITableViewCellStyleDefault 와 같은 식으로 정의되어 있다.
  8. 카테고리 이름은 클래스+카테고리 로 표현한다. 예를 들면 NSString+HTMLFormatting 과 같은 식이다. 여기서 HTML은 축약어로 들어갔지만, HTML은 널리 사용되고 있기에 허용할 수 있다. URL도 같은 맥락에서 허용한다.
  9. 델리게이트나 데이터소스 프로토콜은 이름이 곧 설명이다. UITableViewDataSource, UITableViewDelegate를 보라.
  10. 그외 프로토콜 이름은 형용사를 쓸 수 있다. 예를 들어 NSCoding 프로토콜은 NSCoder와 NSDecoder 의 두 클래스와 관련이 있음을 알 수 있다.

메소드 이름들

  1. 메소드 이름은 소문자로 시작하고, 단어 단위로 대문자를 첫 글자로 쓴다. 축약어를 쓰는 것은 명확하지 않으므로 권장하지 않는다. 또한 메소드 이름은 접두어를 쓰지 않는다.
  2. 메소드 이름을 언더스코어를 시작하는 것은 가능하나, 애플은 이를 private method로 간주하게 될 것이다.
  3. 속성을 리턴하는 메소드이름은 그 속성과 같은 이름을 쓴다. get-, calculate-와 같은 접두어를 사용하지 않는다. 이는 키-밸류 코딩 규칙과도 관련이 있으므로 중요하다.
  4. 파라미터의 이름은 그 파라미터를 설명하는 이름을 쓴다.
  5. 동사를 형용사처럼 만들지 않는다. calculationPerformed 보다는 performsCalculation이라고 한다.

델리게이트와 데이터 소스

  1. 첫번째 인자는 이 메소드를 호출하는 객체의 이름이다. 예를 들어 테이블 뷰의 데이터 소스에서 -(NSInteger)tableView:(UITableView*) numberOfRows… 와 같다. 왜냐하면 델리게이트나 데이터 소스가 되는 객체는 동시에 여러 객체의 델리게이트나, 데이터소스가 될 수 있기 때문이다.
  2. 델리게이트 메소드 이름에는 did나 will이 종종 들어갈 수 있다. 이는 어떤 작업 이전에 호출되는지, 이벤트가 일어난 이후에 호출되는지를 명시적으로 드러내는 좋은 방법이다.
  3. 델리게이트에게 어떤 ‘허가’를 요청하는 메소드는 -(BOOL)shouldCloseWindow…와 같이 should로 시작한다.

기타

  1. 상수값을 사용하는 경우 정수보다는 열거형을 선언하여 단어로 인식할 수 있도록 하라.UITableViewCellStyleDefault는 그냥 0일 뿐이지만, 이름이 붙어 훨씬 명확해진다.
  2. 전처리기가 처리하는 이름은 전체를 대문자로 사용하고 공백을 언더스코어로 대치한다. LIKE_THIS, DEFAULT_NAME_VALUE 등이 이러한 예다.
  3. 오류 이름은 접두어+고유이름+Exception으로 붙인다. NSIllegalSelectorException 이나 NSRangeException 등이 이런 이름이다.

(Cocoa) 날짜와 시간을 다루기

updated

Swift를 사용해서 Date를 다루는 방법은 새로 작성된 글을 참고하세요.

 

들어가며

날짜와 시간을 위한 프로그래밍을 위해서는 기본적으로 NSDate를 사용한다. NSDate는 2001년 1월 1일 자정을 기점으로 현재시간 (혹은 특정 시점)까지의 초단위로 경과한 시간을 저장하고 있는 객체이다.(Epoch가 아니라 21세기의 시작시점을 기준으로 하고 있다!) 이렇게 단순히 누적된 초 시간으로는 두 시점의 선/후 관계를 파악하는 등의 단순 비교 작업은 가능하지만, 구체적인 날짜나 요일에 연관된 작업을 하기는 매우 어렵다. 예를 들어 올해 크리스마스가 무슨 요일인지를 구하는 일은 NSDate 객체 만으로는 사실상 매우 힘들다.

(Cocoa) 날짜와 시간을 다루기 더보기

[iOS/OSX] Timer 사용하기

특정한 시간 이후에 작업을 시행하는 타이머를 사용하는 방법에는 몇 가지 방법이 있을 수 있는데, 코코아에서 타이머는 NSTimer 객체를 통해 구현된다.

타이머와 런루프

타이머는 런루프(Run Loop)와 밀접한 관련을 맺고 있다. 타이머는 스스로 동작기한을 가지고 있고, 그 상태로 런루프에 등록된다. 런루프는 그 기한이 지나는 시점에 타이머를 지켜보고 있다가, 타이머가 실행하기로 한 액션을 지정된 객체로 보내게 된다.

런루프는 (역시나 런루프와 스레드를 혼동하는 불편한 진실 때문에 이에 대한 별도의 포스팅도 필요할 듯) 사용자의 입력 등에 적절히 반응하기 위해서 계속해서 “기다리는” 일종의 루프이다. 예를 들면 키보드는 풀링이라는 방식으로 일종의 런루프를 가지고 있다. 즉 사용자가 키보드를 누르지 않는 상황에서 키보드는 ‘최선을, 총력을 다해서’ 사용자 입력을 반복해서 기다린다. 런루프는 이와 유사하게 스레드 내에서 사용자의 UI 입력을 최선을 다해 기다린다.

타이머가 지정한 액션은 런루프에 의해서 확인된다. 타이머를 사용할 때 가장 유의해야 하는 점은 NSTimer는 실시간 타이머가 아니라는 점이다. 만약 반복되는 주기를 가진 타이머의 특정 작업이 있다고 할 때, 런루프는 단지 그 타이머만을 감시하는 것이 아니라 아주 많은 종류의 입력을 처리해야 하기 때문에 이를 놓치는 경우가 있을 수 있다. 따라서 NSTimer를 통해 이뤄지는 지연된 작업이나, 반복작업은 실제 시간과 다소 차이가 있을 수 있으며, 경우에 따라서는 “상당히 큰” 차이가 발생할 수 있다.

타이머 사용하기

스케줄된 타이머 사용하기

타이머를 사용하는 가장 일반적인 방법은 NSTImer의 scheduledTimerWithTimeInterval:target:selector:userInfo:repeats: 를 사용하는 방법이다. 이는 현재 메인 런루프에 타이머를 추가한다.

-(void)startOneOffTimer {
    [NSTimer scheduledTimerWithTimeInterval:1.0
        target:self
        selector:@selector(timerFired:)
        userInfo:[self userInfo]
        repeat:NO];
}

 

-(void)startRepeatingTimer {
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:0.5
        target:self selector:@selector(timerFired:)
        userInfo:[self userInfo] repeat:YES];
    self.repeatedTimer = timer;
}

타이머의 반복이 없는 경우에는 타이머가 종료된 후 해당 액션을 호출해주고 난 후 타이머는 자체적으로 해제된다. 그렇지 않은 경우에는 추후에 반복되는 타이머를 해제해주기 위해서 따로 참조점을 갖고 있어야 한다.

스케줄링 된 타이머는 생성 즉시 런루프에 추가되어 카운트다운을 시작하게 된다. 이와 별도로 특정 시점부터 동작을 시작하는 타이머는 다음과 같이 만들 수 있다.

-(void)startFireDateTimer {
    NSDate *fireDate = [NSDate dateWithTimeIntervalSinceNow:3600];
    NSTimer *timer = [[NSTimer alloc]
        initWithFireDate:fireDate
        interval:0.5
        target:self
        selector:@selector(timerFiredMethod:)
        userInfo:[self uesrInfo]
        repeat:YES];
    NSRunLoop *theRunLoop = [NSRunLoop currentRunLoop];
    [theRunLoop addTimer:timer forMode:NSDefaultRunLoopMode];
}

시작된 타이머를 멈추기

위와 같이 만들어지고 시작된 타이머는 반복되지 않는다면 종료시 호출해야 하는 액션을 호출하고, 정지된다. 하지만 아직 정해진 시간을 다 채우지 않았거나, 반복해서 실행되는 타이머는 멈출 필요도 있다. 타이머를 멈추기 위해서는 해당 타이머에 invalidate 메시지를 보내면 된다.

-(void)stopRepatedTImer
{
    [self.repeatedTimer invalidate];
    self.repeatedTimer = nil;
}

타이머를 사용하지 않은 예약된 작업 호출

타이머를 사용하지 않고도 특정한 시간 지연 후에 간단히 어떤 기능을 실행할 수 있다. 이 기능은 NSObject에 이미 정의된 메소드이다. (역시 코코아의 루트 클래스가 NSObject라는 사실은 매우 다행스럽다!) 이는 -performSelector: withObject : afterDelay: 이다. 이는 다음과 같이 사용할 수 있다.

    
[self.tableView performSelector:@selector(reloadData)
    withObject:nil
    afterDelay:10.0];

이미 예약된 작업 취소하기

NSObject의 클래스 메소드인 +cancelPreviousPerformRequestsWithTarget:selector:object:+cancelPreviousPerformRequestsWithTarget: 을 사용한다. 이는 지연 후에 일을 하게되는 객체가 이 메시지를 받고 예약된 작업을 실행하지 않게 된다.

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

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

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