[iOS] 계산기를 만들어보자

계산기는 어찌보면 제일 간단한(?) 종류의 앱이라 할 수 있고 대체로 대부분의 컴퓨터나 휴대전화에 기본적으로 들어있는 기능이기도 하다. 하지만, 아이패드에는 없다.(!) 꼭 아이패드에서 없어서 그런 것이 아니라 간단한 앱이다 보니 만들기도 간단하고 해서 오늘은 계산기를 한 번 만들어보면서 MVC 패턴에 대해 살펴보기로 한다.

MVC

MVC는 모델-뷰-컨트롤러의 각각의 머리글자를 따서 만든 용어이다. MVC는 데이터와 사용자 인터페이스를 분리하고 이를 컨트롤러가 중계하는 형태로 프로그래밍의 결과물을 모듈화하는 기본적인 방법론이라 할 수 있다. 모델(데이터)과 뷰(UI)가 분리되어 있어 한 번 만들어 놓은 코드를 나중에 재사용하기 쉽게 한다는 장점이 있다. MVC의 개념에 대한 이야기는 조금 있다 하기로 하고…

계산기 만들기

Xcode의 새 프로젝트를 시작한다. 이름은 적당히 주고, 이번에는 하나의 뷰만 사용하면 되므로 Single View App. 템플릿을 선택하고, 스토리보드를 사용한다. 프로젝트가 생성되면 기본적으로 5개의 파일이 생성되어 있다. 앱델리게이트와 뷰컨트롤러 그리고 스토리보드 파일이다.

뷰컨트롤러 구현파일 (ViewController.m) 파일에 아주 많은 내용들이 이미 들어있는데, 편의상 이 내용들은 모두 삭제하도록 한다. 헤더를 임포트하는 부분과 구현부 선언과 종료 단 3줄만 남기고 모두 삭제한다.

인터페이스 만들기

스토리보드에서 인터페이스를 만든다. 계산기의 사용자 인터페이스는 입력한 숫자가 표시되는 표시창과 이런 저런 버튼 들이다. 여기서는 간단하게 숫자, 사칙연산기호, 계산 값을 구하는 = 버튼 등이 필요할거라고 가정한다. 소수점이나 제곱 등의 기능은 차차 추가해보는 것으로 하자.

표시창 추가

텍스트레이블을 하나 뷰에 추가한다. Label이라고 쓰여져 있는 문구는 계산기 답게 0으로 수정해주고 우측 정렬 속성을 지정해준다.

숫자버튼 추가

총 10개의 숫자 버튼이 필요하다. UIButton을 뷰에 추가하고 적당히 이쁜 비율로 조정하여 위치를 잡아주자. 10개의 버튼을 일일이 끌어 놓기 전에 몇가지 준비를 해야할 것이다. 먼저 방금 만든 버튼에 연계하는 액션 메소드를 추가할 것이다.

우측 상단 에디터에서 가운데 Assistant Editor를 선택하거나 ⌥⌘↩를 눌러 어시스턴트 에디터를 연다. 그러면 오른쪽에는 현재 뷰 컨트롤러의 소스코드(ViewController.h)가 표시된다. ^⌘↓를 눌러 구현부 파일로 이동한 다음, 마우스 오른쪽 버튼으로 방금 추가한 버튼을 끌어서 소스코드가 있는 곳으로 옮겨보자. (헤더 파일로 드래그하면 Outlet이 기본으로 선택되지만, 구현부로 드래그하면 바로 메소드가 만들어진다.)

해당 버튼을 눌렀을 때의 호출될 메소드를 자동으로 추가할 수 있다. 숫자를 눌렀으니까 이름은 digitPressed로 주면 된다.

이제 Assistant Editor를 닫고 ( ) 버튼을 클릭해서 선택한 다음  D를 눌러 해당 버튼을 복제한다. 두 번 눌러서 총 3개를 만든다. 이를 나란히 배치하여 가로로 한 줄을 만들고, 다시 이번에는 3개를 모두 선택하여 키를 누른채로 아래로 끈다. 그럼 + 표시가 생기면서 3개의 버튼이 복사된다. 같은 방법으로 버튼을 복사하여 10개의 버튼을 완성한다.

이렇게 소스코드의 메소드와 연결된 버튼을 복제하면 복사된 버튼들 역시 소스코드와 connect 된 상태를 유지하므로 조금이라도 더 IBOutlet 노가다를 줄일 수 있다.

같은 방법으로 + 버튼을 하나 새로 만들어서 소스코드와 연결, operatorPressed 메소드와 연결한다. 이 버튼을 추가로 복제해서 -, *, / 버튼도 생성한다.

끝으로 새 버튼을 하나 더 추가하여 = 버튼을 만든다음, 이 때 메소드의 arguement를 None으로 설정하면 sender가 추가되지 않는 메소드가 만들어진다. (있어도 상관은 없다.) 이름은 excute 로 하면 되겠다.

맨 처음에 추가한 텍스트레이블에도 아울렛을 만들어야 하는데, 이는 private property로 선언할 것이니까 조금 있다가 하자.

Private Interface

스토리보드의 숫자 디스플레이부분은 사실 다른 클래스에서 직접 접근할 필요는 없다. (만약 필요하다고 하면 이를 위한 메소드를 인터페이스에 추가해주면 된다.) 따라서 이 디스플레이는 private한 프로퍼티로 설정되어야 한다.

구현부 파일의 @implemantation 위에서 private 인터페이스를 정의하고 여기에 숫자 디스플레이에 대한 아웃렛을 선언한다.

@interface ViewController()
    @property (weak, nonatomic) IBOutlet UILabel *display;
@end

클래스 이름은 동일하나 뒤에 빈 괄호가 있음에 주목하자. 여기서 선언한 프로퍼티는 마찬가지로 접근자메서드를 작성해주어야 한다.

@synthesize display = _display;

보통 코드 작성 시, 등호 뒤의 내용은 잘 적지 않는데, 이는 해당 프로퍼티를 저장할 인스턴스 변수명을 지정하는 것이다. 물론, 보통은 인스턴스 변수로 프로퍼티의 이름과 같은 변수를 선언하기도 하기때문에 필요가 없는데, 모든 프로퍼티가 대응하는 인스턴스 변수를 가져야 하는 것은 아니다.

어쨌든 이 방법으로 프로퍼티를 선언하고 합성(@synthesize)하면 이미 프로퍼티 선언에서 변수의 타입은 지정하였으므로 중복해서 같은 이름의 인스턴스 변수를 또 선언할 필요는 없다.

숫자를 누를 때

숫자를 누르면 입력된 숫자를 계속해서 뒤에 붙여서 display에 표시해주면 된다. 단, 최초로 입력하는 경우에는 이미 0이 표시되고 있기에 최초 입력 시에는 입력한 숫자만 표시되도록 해야 한다. 또한 연산기호 버튼을 눌렀거나 등호 버튼을 누른 후에도 누르는 숫자는 최초 입력과 같은 형태가 되어야 한다.

숫자 입력의 초기화 여부를 구분하기 위해 isEditing 이라는 프로퍼티를 추가하도록 한다. 이는 파일 상단의 private interface  부분에서 선언한다.

    
@interface ViewController()
    @property (weak, nonatomic) IBOutlet UILabel *display;
    @property (nonatomic) BOOL isEditing;
@end

마찬가지로 @synthesize도 빼먹지 말자

@synthesize isEditing = _isEditing;

이제 숫자를 누를 때의 동작은 다음과 같이 구현한다.

-(IBAction)digitPressed:(UIButton *)sender
{
    if(self.isEditing)
        self.display.text = [self.display.text stringByAppendingString:sender.currentTitle];
    else {
        self.display.text = sender.currentTitle;
        self.isEditing = YES;
    }
}

그런데 위의 코드는 isEditing을 초기화하는 부분이 없다. 하지만 큰 무리없이 처음 누르는 숫자가 0을 대체하는데,  iOS5에서는 새로 선언한 인스턴스 변수를 0 나 nil 로 초기화해주기 때문에 가능한 일이다. 만약 iOS4 이하에서 동작하는 앱이라면 -viewDidLoad: 를 통해 NO 값으로 초기화하면 된다. 하지만 처음에 0을 누르면 “00000012”등이 입력이 가능한 상황이되므로, if문을 다시 다음과 같이 수정을 해야 한다.

if(self.idEditing || self.display.text isEqualToString:@"0" )

계산을 담당하는 엔진

자, 숫자는 이렇게 입력한다 치더라도 실제 계산은 어떻게 할까? 물론 뷰 컨트롤러에 입력한 값들을 저장했다가 계산하는 부분을 구현할 수도 있지만 실제 계산하는 데이터는 모델로 보는 것이 가깝다. (물론, MVC라고 해서 반드시 모델이 있어야 하는 것은 아니다. 컨트러롤러 파일자체가 컨트롤러와 모델을 얼마든지 겸할 수 있다.)

계산기 엔진의 기능

그래서 계산을 담당하는 클래스를 하나 새로 작성해보도록 한다. 이름은 CalcEngine 정도로 한다. 이 클래스는 다음과 같은 기능을 수행한다.

  1. 엔진에는 큐가 하나 있어서 입력된 계산값들을 하나씩 들어간 순서대로 뽑아올 수 있다.
  2. 계산기에서 숫자입력이 끝나면 (연산자나 등호 버튼을 누르면) 숫자를 이 큐에 집어 넣는다. 또한 연산의 종류를 엔진에 알려준다.
  3. 그리고 또 두 번째 숫자를 입력하고..
  4. 다시 등호나 다른 연산자를 누르면 앞 선 2개의 숫자가 저장된다.  특히 등호를 눌렀다면 엔진은 저장된 2개의 숫자를 순서대로 하나씩 꺼내어 엔진이 알고 있는 연산의 종류에 맞게 계산해서 그 결과를 뱉어낸다.

그렇다면 엔진은 다음 3개의 메소드가 필요할 것이다.

  • -(void)pushOperand:(NSString *)anOperand // 숫자값을 집어넣는다.
  • -(void)pushOperator:(NSString *)anOperator // 연산자를 집어넣어 어떤 연산을 할 것인지 설정한다.
  • -(double)performOperation //들어있는 숫자값들과 연산자로 계산하고 그 결과를 출력한다.

자 이제 만들어보자.

계산기 엔진 클래스 만들기

⌘N을 눌러 새 파일을 만든다. NSObject의 서브클래스를 선택해서 이름은 CalcEngine으로 한다.

헤더

먼저 헤더파일에서는 위에서 정한 메소드들을 선언한다.

#import <Foundation/Foundation.h>

@interface CalcEngine : NSObject
-(void)pushOperand:(NSString *)anOperand;
-(void)pushOperator:(NSString *)anOperator;
-(void)performOperation;
@end

구현부

큐를 담을 배열이나 연산의 종류를 기억하는 프로퍼티를 선언하기 전에 사칙연산을 구분하기 위한 값을 미리 따로 정의하자.

import “CalcEngine.h” 아래 줄에 다음 한 줄을 추가한다. (헤더에 추가해도 사실 상관없다)

enum {plus, minus, multiply, divide};

이는 각 단어를 0,1,2,3에 매칭하기 위해 쓴다. 숫자값으로 0 일 때는 덧셈… 이런 식으로 쓸 수도 있지만 나중에 헷갈리기 때문에 열거형으로 이를 지정해둔다.

Private Interface

    
@interface CalcEngine()
    @property (strong, nonatomic) NSMutableArray *operandQueue;
    @property (assign) int operatioin;
@end

익숙하게 synthesize

@synthesize operandQueue = _operandQueue, operation = _operation;
-(NSMutableArray *)operandQueue
{
    if(!_operandQueue) _operandQueue = [NSMutableArray array];
    return _operandQueue;
}

숫자값을 밀어넣을 때의 동작은 다음과 같이 구현하면 된다.

-(void)pushOperand:(NSString *)anOperand
{
    [self.operandQueue addObject:anOperand];
}

연산자를 지정하는 동작은 다음과 같이 if… else if… else… 패턴을 사용한다.

-(void)pushOperator:(NSString *)anOperator 
{
    if ( [anOperator isEqualToString:@"+"]) self.operator = plus;
    else if ([anOperator isEqualToString:@"-"]) self.operater = minus;
    else if ([anOperator isEqualToString:@"*"]) self.operater = minus;
    else self.operater = divide; 
}

이제 큐로부터 하나씩 값을 빼 오는 부분이다. 큐에는 NSString으로 숫자들이 들어있으니 이를 하나씩 빼내 온다. 빼내 온다는 말은 빼고 원래 들어있던 녀석을 없앤다는 뜻이다.

-(double)popOutOfQueue
{
    double result;
    id front =  [self.operandQueue objectAtIndex:0];
    if(front) [self.operandQueue removeObjectAtIndex:0]; 
    if( [front respondToSelector:@selector(doubleValue)]) 
        result = [front doubleValue];
    return result;
}

이 메소드는 -performOperation 내에서 호출될 것이기 때문에 반드시 이보다 앞서 정의되어 있어야 한다. [1. 추가 : Xcode 4.3 부터는 모든 함수 및 메서드의 선언을 먼저 다 읽은 다음 몸체를 해석하므로 순서와 무관하다.]

마지막으로 계산을 수행해주는 부분은 아래와 같으면 된다.

-(double)performOperation
{
    double result;
    switch(self.operator){
        case plus:
            result = [self popOutOfQueue] + [self popOutOfQueue];
            break;
        case minus:
            result = [self popOutOfQueue] - [self popOutOfQueue];
            break;

        case multiply:
            result = [self popOutOfQueue] * [self popOutOfQueue];
            break; 

        default:
            double divided = [self popOutOfQueue];
            double divisor = [self popOutOfQueue];
            if (!divisor) {
                result = 1;
                NSLog(@"can't divide by zero.");
            }
            return result;
    }

나누기의 경우에는 나누는 값이 0이면 계산할 수 없으므로 두 개의 변수를 사용해서 큐로부터 값을 뽑고, 나누는 수가 0인 경우에는 그냥 1로 나눈 셈치고 결과를 그대로 반환하도록 했다. (0으로 해도 상관은 없을 것이다.)

컨트롤러

이제 다시 컨트롤러로 돌아와서 = 버튼을 누르면 입력한 식에 대해서 계산이 수행되어 그 결과가 표시되도록 해보자. 계산을 담당하는 CalcEngine에 연산자와 숫자값들을 밀어 넣고, 밀어넣은 다음에는 그 결과값을 CalcEngine의 인스턴스로부터 받아오면 된다. 계산 로직이 어떻게 변하든 간에 이는 CalcEngine에서 변경될 뿐이고 컨트롤러는 UI가 변경되지 않는 이상, 계산 로직과는 무관하기 때문에 추후에 CalcEngine을 변경하더라도 거의 변경할 필요가 없을 것이다.

먼저, CalcEngine의 인스턴스하나를 새 프로퍼티로 추가하자.

#import "CalcBrain.h"

private interface 에서 인스턴스를 추가한다.

@property (strong, nonatomic) CalcBrain *brain;

프로퍼티로 추가했으면 @synthesize 구문도 추가해준다.

@synthesize brain = _brain;

연산자를 눌렀을 때

연산자를 누르면 brain에 현재 연산이 무엇인지 알려주어야 한다. 그리고 그 전에 입력된 숫자를 brain에 밀어넣어줄 필요가 있다. 숫자를 밀어넣고 나서는 입력 여부의 flag인 isEditing 을 NO로 다시 초기화한다. 이 작업은 각 연산자를 누를 때 뿐만아니라 = 버튼을 누를 때도 발생하므로 따로 함수로 정의한다. 이 함수는 아래 언급할 operatorPressed, excute 보다 먼저 쓰여져야 한다.

-(void)enterDigit {
    [self.brain pushOperand:self.display.text];
    self.isEditing = NO;
}

연산자를 누를 때는 다음과 같이 구현된다.

-(void)operatorPressed:(UIButton *)sender { //sender의 타입을 지정해버린다.
    [self enterDigit];
    [self.brain pushOperator:sender.currentTitle];
}

= 버튼을 누를 때는 계산을 수행하고 그 결과를 다시 표시하도록 하면 된다.

-(void)excute {
    [self enterDigit];
    self.display.text = [NSString stringWithFormat:@"%f",
                            [self.brain performOperation]];
}

여기까지 작성하고 앱을 빌드/실행하면 기본적인 사칙 연산을 수행할 수 있는 계산기가 만들어진다. 다음 시간에는 연속된 연산을 한 번에 수행할 수 있는 계산기로 발전시켜보도록 하자. (공학용 계산기에서 사용하는 후위식을 사용하는 방법은 아니다.)

또한 지금껏 만든 계산기는 그저 “샘플” 수준일 뿐이고 UI 상으로도 몇 가지 문제점이 있다. 하지만 상당히 ‘정직’한 방식으로 구성된 소스이므로 얼마든지 문제점을 개선하는 것은 가능할 것이다.

오늘도 쓸데없이 길어진 글 읽어주어서 감사하다.

[Objective-C] 프로퍼티의 atomic / nonatomic 속성

Objective-C에서 사용하는 프로퍼티(@property)는 알고보면 엄청나게 중요하더라. 이 프로퍼티를 선언할 때 속성을 지정하는데, 그 중에 nonatomic 이라고 거의 대부분의 객체 타입의 프로퍼티에는 명시해주는데, 이에 대해서 속시원히 설명해주는 글을 찾기가 힘들었다. 물론 멀티스레드 처리에서 해당 값을 안전하게 접근할 수 있도록 해주는 내용이고, 그게 별로 필요가 없으니 보통은 명시적으로 nonatomic으로 쓴다고는 하지만… 암튼 나름대로의 설명은 아래와 같다(…고 본다.) [Objective-C] 프로퍼티의 atomic / nonatomic 속성 더보기

[Objective-C] 한글의 초성, 중성, 종성 분리하기

유니코드 한글의 코드 값 구성 원리와 이를 토대로 각 음절의 초/중/종성을 분리하는 방법은 강호경님의 블로그에 자세하게 나와있으니, 여기서는 자세한 설명을 생략한다.

위 블로그의 내용을 토대로 클래스를 하나 만들어 초성, 중성, 종성을 추출하는 클래스 메소드를 가진 클래스를 하나 작성하고 이를 사용하여 입력된 파라미터로부터 한글의 초성을 뽑아 뱉어주는 명령줄 도구를 작성한 것이다.

소스코드 다운로드 : http://www.box.com/s/mxmoq5vc3ii5z5di5op2

이전 글에서 사용한 애플 스크립트는 이 명령줄 도구를 애플 스크립트에서 호출하여 이름 필드 값의 초성을 추출하여 이를 별명 필드에 입력하고 저장하는 내용이다. 아이폰은 spotlight 검색이나 주소록 앱에서 검색 시 별명 필드를 검색하므로, 이를 통해 초성 검색 기능을 이용할 수 있다.

유니코드 한글의 각 음소를 분리하기

유니코드에서 한글은 0xAC00에서 0xD7A3 사이의 코드 값을 갖는다. 각 16진수값은 10진수로 표시하면 44032와 55203으로 총 11,172개이다. 유니코드 내 한글은 초/중/종성의 각 음소의 조합으로 표현된다. 즉 초성 19개, 중성 21개, 종성 28개를 조합하여 하나의 글자가 되는 것이다. 따라서 각 초,중,종성에 해당하는 한글자모의 위치값을 계산하여 최종적으로 만들어지는 글자의 코드를 생성할 수 있다. 이 때 들어가는 값은 위치 값으로 0~해당 음소의 개수-1 만큼의 인덱스를 의미한다. 따라서 각 자모의 인덱스를 사용하여 조합된 문자의 코드값은 다음 식으로 계산할 수 있다.

((초성 * 21) + 중성) * 28 + 종성 + 0xAC00

이를 역산하면 어떤 문자의 코드값으로부터 각 자모의 인덱스를 구할 수 있다.  즉 각 음소 중에서 몇 번 째 글자인지를 알 수 있게 된다. 각 자모의 인덱스를 구하는 계산식은 위 공식으로부터 다음과 같이 유도된다.

초성 = ((문자코드 – 0xAC00) / 28) / 21
중성 = ((문자코드 – 0xAC00) / 28) % 21
종성 = (문자코드 – 0xAC00) % 28

초성의 자모 코드 시작값은 0x1100, 중성은 0x1161, 종성은 0x11A8 이므로 이를 각각 더한다. 특히 종성의 경우, 받침이 없는 문자의 경우가 있기 때문에 종성에는 1을 뺀다.

초성의 자모코드 = 초성인덱스 + 0x1100
중성의 자모코드 = 중성인덱스 + 0x1161
종성의 자모코드 = 종성인덱스 + 0x11A8 – 1

구현

NSString에서 문자열 내 특정 글자를 뽑아오는 일은 characterAtIndex: 메소드를 사용하고 이 때 반환되는 값은 unichar 포맷이 된다. (unichar 는 unsigned short 타입의 변수형이다.)

unichar oneCode = [hangul characterAtIndex:i];

이를 위 과정을 통해 계산해서 초성 (및 중/종성)을 추출한 다음, 이를 다시 NSString으로 만들기 위해서는 stringWithFormat: 메소드를 사용한다. 이 때 포맷팅 파라미터는 %C (대문자)를 사용한다. 소문자 %c를 쓰는 것은 char 타입일 때 이다. (바꿔써도 무리는 없는 것 같더라)

코드

-(NSString *)getFirstCodeWithString:(NSString *)hangul
{
    NSString *result = @"";
    for ( int i=0; i<[hangul length];i++) {
        unichar oneCode = [hangul characterAtIndex:i];
        // 한글일 때만 처리한다.
        if ( oneCode >= 0xAC00 && oneCode <= 0xD7A3 ) {
            unichar firstCode = ((oneCode -0xAC00) / 28)/21;
            firstCode += 0x1100;
            result = [result stringAppendingString:[NSString stringWithFormat:@"%C",firstCode]];
        }
    }
    return result;
}

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

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

사용방법

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

주의사항

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

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