[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 상으로도 몇 가지 문제점이 있다. 하지만 상당히 ‘정직’한 방식으로 구성된 소스이므로 얼마든지 문제점을 개선하는 것은 가능할 것이다.

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