[iOS/OSX] 앱 로컬라이징 하기

nib 파일 로컬라이징

nib 파일을 선택하고 인터페이습 빌더에서 언어를 추가하면 해당 언어로 표시할 nib 파일이 복사된다. 이에 따라 버튼 레이블이나 뷰에 올라가는 이미지와 텍스트를 변경하면 기기에서 설정한 언어에 따라 자동으로 해당 언어의 nib 파일이 선택되어 로컬라이징된 UI를 보여주게 된다.

문자열 테이블

예를 들어 경고창이나 검색 결과 문구에 대해서는 문자열 테이블을 사용하여 로컬라이징한다. 문자열테이블은 .strings 라는 확장자를 가지고 있다. 예를 들어 코코아 앱에서 찾기 패널을 만들었다고 한다면 Find.strings 파일을 각 언어별로 만들어 둘 수 있다. 문자열 테이블은 키-값 쌍의 모음이다. 각각의 키와 값은 겹따옴표로 둘러진다.

이를 코드에서 사용하려면 NSBundle 을 사용해야 한다.

NSBundle *main = [NSBundle mainBundle];
NSString *aString = [main localizedStringForKey:@"Key1" value:@"DefaultValue1" table:@"Find"];

이 코드는 Find.strings 파일에서 Key1 의 키를 찾는다. 만약 값이 발견되지 않았다면 디폴트로 DefaultValue1 이라는 문자열이 된다.

문자열 테이블 만드는 법

Xcode에서 빈 파일을 하나 만든다음, 이름을 Localizable.strings 라고 하고 English.lproj 디렉토리에 저장한다. 파일의 내용은 앞서 설명한 바와 같이 키와 값 쌍으로 입력하며, 각각의 쌍은 세미콜론으로 끝나야 한다.

"DELETE" = "Delete";
"SURE_DELETE" = "Do you really want to delete %d people?";
"CANCEL" = "Cancel";

이제 만들어진 문자열 테이블을 Xcode에서 선택하고, 인스펙터 패널에서 다른 언어 버전을 추가한다. 신규로 추가한 언어를 선택한 다음, 내용을 편집한다.

"DELETE" = "삭제";
"SURE_DELETE" = "%d명의 인원을 삭제하겠습니까?";
"CANCEL" = "취소";

알파벳이 아닌 문자가 있는 경우에는 유니코드로 변환할 것인지 묻는 경우가 있는데 변환을 해야 한다.

이렇게 저장된 문자열은 다음과 같이 사용한다. (문자열 테이블이 하나 밖에 없는 경우) 문자열 테이블이 더 있는 경우에는 테이블 이름도 명시해야 한다.

NSString *deleteString;
deleteString = [[NSBundle mainBundle] localizedStringForKey:@"DELETE" value:@"Delete?" table:nil];

이렇게 쓰는게 상당히 번거롭기 때문에 매크로로 처리할 수도 있다.

#define NSLocalizedString(key, comment) [[NSBundle mainBundle] localizedStringForKey:(key) value:@"" table:nil]

이 때, comment 부분은 그냥 무시된다. 하지만 genstrings 툴을 사용할 때, 코멘트를 보고 활용할 수 있다.

ibtool 을 사용하여 nib 생성하기

ibtool 명령은 터미널을 통해 다국어 적용 문자열을 덤프할 수 있는 명령이다. 아래 예는 MyDocument.nib 파일로부터 Doc.strings 파일을 생성해 내는 명령이다.

$ cd SomeProject/English.lproj
$ ibtool --generate-stringsfile Doc.strings MyDocument.nib

이렇게 생성된 string 파일에서 만약 스페인어 버전을 만들겠다면, 스트링 파일의 값들을 수정해준다. 그런 다음,

$ mkdir ../Spanish.lproj
$ ibtool --strings-file Doc.strings --write ../Spanish.lproj/MyDocument.nib MyDocument.nib

즉, 현재 디렉터리의 MyDoument.nib 파일을 Doc.strings 파일을 참고하여 일부 레이블을 번역한 버전을 자동으로 생성하는 것이다.

문자열 포맷의 토큰 순서 변경하기

예를 들어 페이지 번호를 표시한다고 할 때,

NSString *theFormat = NSLocalizedString(@"PAGE", "%d of %d ");
x = [NSString stringWithFormat:theFormat, currentPage, totalPage];

와 같이 포맷팅할 수 있다. 하지만 언어별로는 순서가 달라질 수 있는데, 이는 다음과 같이 문자열 테이블 작성시에 토큰의 순서를 달러 표시와 함께 써서 변경할 수 있다.

"PAGE" = "%2$d 중 %1$d";

[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: 을 사용한다. 이는 지연 후에 일을 하게되는 객체가 이 메시지를 받고 예약된 작업을 실행하지 않게 된다.

[스터디] 후위식을 사용하지 않는 공학용 계산기 로직

공학용 계산기 엔진 만들기

예전에 미진하게 만들다가 말았던 계산기 만들기와 관련해서. 지난 번 글은 사실 스탠포드의 iOS 앱 개발 강의 내용을 바탕으로 만들었다. 물론 일부 내용을 좀 수정하기는 했는데. 그 강의에서는 스택에 숫자와 연산자를 넣는 형태로 동작했다. 예를 들면 3 + 5 를 계산하기 위해서는 3, enter, 5, enter, plus 의 순서로 값을 먼저 입력하고 연산자를 뒤에 입력하는 형태의 약간 이상한 방식으로 동작했다. 이는 사실 그 계산기 프로젝트를 계속 발전시켜 공학용 계산기로 만들기 위해서이다.

보통의 공학용 계산기는 ‘후위식’을 이용한 계산 방법을 사용한다. 우리가 흔히 사용하는 1 + 2와 같은 수식의 형태를 연산자가 가운데에 들어간다고 해서 중위식이라고 하는데 이를 12+ 라 쓰고 뒤에서부터 계산하는 방식을 후위식이라 한다. 중위식을 후위식으로 변환할 때 연산의 우선 순위를 고려하게 되므로, 곱하기/나누기가 더하기/빼기와 섞여있는 식이나 괄호가 들어있는 식도 후위식으로 전환하고 나면 뒤쪽에서부터 차근차근 계산해서 한 번에 계산할 수 있는 장점이 있다. 물론 후위식 자체는 사람이 계산하기에는 무척 불편한 형태의 수식 표현이지만 컴퓨터는 스택에서 연산자와 피연산자를 꺼내서 쉽게 처리할 수 있다.

문제는… 이 후위식으로 변환하는 과정이 꽤나 골치아프다는 게 문제. (연산 우선순위를 정의한 기준과 별도의 스택이 추가로 필요) 그래서 생각한게 사람이 계산하는 방식과 마찬가지로 (단순 무식하게) 각 항별로 버퍼에서 우선 계산하고, 다음 항을 계산해야 할 때 버퍼에 있는 값을 결과값에 더해 버리면 되지 않을까? 하는 것이었다.

즉 이를 정리하면 다음과 같다.

  1. 버퍼의 값은 1로 초기화한다. 버퍼에는 숫자값과 현재 항이 음수인지, 양수인지를 구분하는 플래그, 그리고 새로 버퍼에 넣는 값을 곱할 것인지, 나눌 것인지를 결정할 플래그. 이렇게 3개의 값이 필요하다.
  2. 곱하기 연산자가 수식에서 나오면 다음에 나오는 숫자는 버퍼에 들어가 버퍼값과 곱해진다. 버퍼의 계산방식을 곱하기로 변경한다.
  3. 나누기 연산자가 수식에서 나오면 다음에 나오는 숫자는 버퍼에 들어가면서 기존 버퍼값을 나눈다. 버퍼의 계산 방식을 나누기로 변경한다.
  4. 더하기 연산자가 수식에서 나오면 이는 하나의 항에 대한 처리가 끝나고 다음 항이 시작되기 때문에 버퍼에 계산된 값을 꺼내 결과 값에 더해준다. 다음 버퍼는 “더해지는” 항이므로 버퍼의 부호를 양수로 설정해놓고, 값을 초기화한다.
  5. 빼기 연산자가 수식에서 나오면 이 역시 하나의 항에 대한 처리가 끝났다. 더하기와 같이 처리하고 다음 항의 부호는 음수이므로 버퍼의 부호를 음수로 설정한다.
  6. 숫자값이 나올 때는 이 값을 버퍼에 집어 넣는다. 버퍼의 곱하기/나누기 플래그에 따라 이값이 버퍼에 들어갈 때 기존의 버퍼의 값에 곱하거나 나눈다.

이 규칙에 따라 곱하기나 나누기로 연결된 식은 하나의 항으로 취급되어 버퍼에서 계산되고, 더하기나 빼기가 나타날 때 결과 값에 더해진다. 이 과정을 수식의 전체에 대해 진행하면 자동으로 식이 계산된다.

이 방식을 사용하면 괄호처리 역시 어렵지 않게 할 수 있다. 여는 괄호가 나타나면 그 괄호를 닫을 때까지 (따라서 열린 괄호의 수를 카운트하며 추적해야 한다) 수식에 들어있는 식을 별도의 수식으로 만든다. 그런 다음 이 수식을 수식 처리 함수 (즉 그 함수 자신)에게 넘겨 재귀적으로 처리하고 그 결과값을 버퍼에 집어넣으면 된다.

기타 파이, 로그, 제곱, 제곱근 등의 처리도 버퍼에서 별도의 필터를 통해 기능을 추가해 나갈 수 있을 것이다.

계산기엔진 소스코드 : http://www.box.com/s/2neu29qfmblaeoiaj5a8

iOS에서 SQLite 사용방법

관련하여 새롭게 작성된 글이 있으니, 이 글을 읽어주세요: [새로 작성된] iOS에서 SQLite3 사용방법

자주 쓰이지는 않지만, 현재로서는 미리 만들어 놓은 데이터를 검색하여 사용하는 가장 단순한(?) 방법이다. (코어데이터는 데이터 셋을 미리 만들어 사용하기가 까다롭다) 대신, 애플은 영구저장소를 활용하는 방법으로 코어데이터를 밀고 있기 때문에 SQL과 관련한 내용을 애플 개발자 문서에서 친절하게 소개하고 있는 자료는 좀 드물다. 대신 SQL의 C인터페이스를 설명하는 글은 인터넷에서 많이 있으므로 적절하게 찾아보면 된다.
iOS에서 SQLite 사용방법 더보기