[iOS/OSX] 특정 작업을 병렬로 처리하기

“동시에 진행되는 작업”을 처리하기 위해서는 iOS 및 OSX 환경에서는 크게 두 가지 방법을 (흔히) 사용한다. GCD (dispatch queue)와 Operation Queue가 그것이다. 오퍼레이션 큐는 GCD의 Objective-C 버전이라 할 만큼 비슷한데 (사실 좀 다르기는 다르다) 어쨌거나 이 두 가지 방법은 스레드의 생성과 관리를 시스템이 알아서 처리해주는 레벨로 가지고 내려가기 때문에 실제로 프로그래머가 신경써야 할 부분을 “동시에 진행되는 작업을 처리”하는 부분에만 집중하면 되도록 해준다.

예를 들면 네트워크를 통해 데이터를 로드해야 하는 경우나 그 반대로 네트워크를 통해 데이터를 저장해야 하는 경우에 응답이 느리다면 (이는 디스크 같은 영구 저장소를 액세스할 때도 일어날 수 있다. 아주 미묘한 수준이기는 하나 이런 작업은 앱에 blocking을 가져오고 UI에 대한 반응을 느리게 만든다) 이 작업의 처리를 기다리는 동안 앱은 사용자의 터치에 반응하지 못하고 계속 대기하게 될 것이다. 따라서 사용자 경험의 품질이 매우 나빠질 수 있다. 이런 경우에는 “동시작업 처리”를 하도록 해주는 것이 좋다. 동시작업 처리를 사용하면 멀티 코어 프로세서를 효율적으로 사용할 수 있고, 시스템을 보다 “바삐” 움직이게 할 수 있기 때문이다.

(*여기서 주목해야 할 부분은 “동시작업 처리”를 “멀티 스레드”로 기재하지 않은 것이다. GCD에서 동시작업을 처리하는 것은 “디스패치 큐”를 분리하여 동시에 2개 이상의 작업을 진행시키는 것인데, 놀라운 점은 GCD를 사용한 동시작업은 해당 작업에서 다시 스레드를 생성하지 않는 이상, 모두 메인 스레드에서 돌아간다. 따라서 멀티스레드가 아닌 경우가 있을 수 있다.)

NSOperationQueue를 통한 동시작업

먼저 NSOperationQueue를 사용하는 경우를 살펴보도록 하자.

만약 Block 객체를 사용하는 코딩 문법에 조금 익숙하다면 (특히 이는 애니메이션과 관련한 새로운 메소드에서 자주 등장한다. 우리가 익힌 바 있는 UIDocument 관련 글에서도 본 적이 있을 것이다.) 상당히 쉽게 익숙해질 수 있다. 즉 NSOperation은 코드 블럭과 같이 “일련의 작업을 지시하는 코드”를 객체로 만들어 이를 별도의 큐에서 실행하도록 하는 방식이다. 이 때 스레드의 생성과 관리는 큐가 알아서 하게 되므로 여전히 스레드 관리에 대한 크나큰 부담을 덜 수 있게 되는 것이다.

작업 객체 생성

NSOperation은 우리가 작업해야 하는 코드를 담는 객체인데, 이를 활용하는 방법에는 다음 세 가지가 있다.

  • NSInvocationOperation
  • NSBlockOperation
  • subclassing NSOperation
먼저 NSInvocation은 특정 객체의 메소드를 작업 객체로 만들어버리는 방법이다. 즉, 다른 스레드에서 동시 처리를 해야할 메소드를 가진 객체가 있다면, 그 객체의 메소드를 동시 처리 작업으로 만들 수 있다.
NSInvocationOperation *theOp = [[NSInvocationOperation alloc] 
                                      initWithTarget:self 
                                            selector:@selector(doMyTask:) 
                                              object:withData];

NSBlockOperation 객체는 코드 블럭을 사용해서 작업 객체를 만들 수 있다.

NSBlockOperation *theBlockOp = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"Block has started");
}];

// 아래와 같이 블럭을 계속 추가해 나갈 수 있음
[theBlockOp addBlock:^{
    // do something...
}];

혹은 NSOperation 객체를 새로 생성할 수도 있다. (이에 대한 자세한 내용은 다른 글에서 다뤄볼까 한다.)

작업 객체의 실행

작업 객체는 물론 그대로도 실행이 가능하다. 하지만 특별히 멀티 스레드로 동작하도록 작업 객체를 커스터마이징 하지 않은 경우라면 이런 작업들은 메인 스레드에서 돌아가는 함수와 동일하다. (즉, 동시작업으로 처리되지 않는다.) 별도의 스레드에서 동시 작업으로 처리되도록 하려면 NSOperationQueue 객체를 생성하여, 이 곳에 앞서 말한 방법으로 생성된 작업객체를 추가해주면 된다.

NSOperationQueue *aQueue = [[NSOperationQueue alloc] init];
[aQueue addOperation:anOp];

작업이 추가되면 큐 객체는 자동으로 스레드를 만들고 먼저 큐에 추가된 순서대로 작업 객체에 start 메시지를 보내어 각각의 작업을 시작하게 된다.

단순한 예제를 만들 때 유의할 점은, 큐가 스레드를 새로 생성할 때는 약간의 시간이 걸리는 데, 그 사이에 메인 스레드가 종료되어 버린다면 큐에 담긴 작업이 아예 처리되지 못하고 프로그램이 종료될 수도 있다.

이런 예제와 같은 경우에는 큐를 처리하는 동안 큐를 생성했던 현재 스레드를 잠깐 멈추게 하여 큐가 처리된 이후에 그 다음 작업을 실행해주는 방법도 있다.

[aQueue waitUntilAllOperationsAreFinished];

하지만 이렇게 큐의 작업이 처리되는 것을 기다리는 것은 성능에도 좋지 않은 영향을 미치고 (왜냐면 그만큼 메인 스레드가 블럭킹을 당하고 잠기기 때문에) 되려 동시 작업성을 저해하는 결과를 가져오기 때문에 가능하면 쓰지 말 것을 권한다.

큐에서 작업을 시작할 때는 작업 객체에 start 메시지를 보낸다. 이와 같은 방법으로 NSInvocationOperation 객체나 NSBlockOperation 객체에 start 메시지를 보내 해당 작업을 실행시킬 수 있다. 하지만 메인 스레드에서 명시적으로 이런 작업을 실행하는 것은 그냥 코드 블럭을 실행하는 것과 아무런 차이가 없게된다.

Dispatch Queue 사용하기

Dispatch Queue도 큐에 코드 블럭을 밀어넣어 실행하는 것과 유사하게 디스패치 큐에 작업(코드 블럭)을 넣고 이를 동시에 실행시키는 방법이다. 동시 작업으로 진행될 작업은 메인 스레드에서 함께 돌아간다. 이것이 오퍼레이션 큐와의 가장 큰 차이점이라 하겠다. (실은 항상 메인 스레드에서 돌아가는지는 모르겠다. 동시에 처리되는 작업의 개수도 시스템이 코어의 개수나 시스템에 현재 걸려 있는 부하에 따라 자동으로 판별한다.

즉 동시 작업으로 병렬처리되는 일이 종료되었을 때 어떤 일이 이어서 일어나게 만들고자 할 때 (이때는 델리게이션이나 KVO를 써도 되지만) 이 방법을 사용하는 것도 굉장히 쉽고 간단하다. GCD를 이용해서 병렬작업을 처리하는 가장 간단한 방법은 글로벌 큐를 사용하는 것이다. 물론 글로벌 큐를 사용하지 않고 별도의 큐를 생성하여 작업을 처리할 수도 있다. 단 이렇게 생성되는 큐는 serial 큐로, 추가된 순서대로 작업이 수행된다. 대신 글로벌 큐는 들어간 순서대로 작업이 시작되나, 큐에 들어간 작업은 가능한 많은 수가 동시에 실행되므로 먼저 들어간 작업이 먼저 끝난다고는 특정할 수 없다.

큐에 작업을 추가하여 실행하기 위해서는 dispatch_async 함수를 사용한다. 이 함수에 수행할 작업을 코드 블럭으로 넘겨서 수행하도록 할 수 있다. 이 함수를 호출한 직후 프로그램의 흐름은 다음 라인으로 넘어가고, 디스패치 큐는 이와 동시에 넘겨진 작업을 즉시 처리하게 된다. 다음과 같이 글로벌 큐를 적용한 아주 간단한 코드를 사용해서 병렬 작업을 수행할 수 있다.

dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(aQueue, ^{
        // 병렬적으로 진행할 코드
        });

매우 큰 DB에서 값을 검색해 오거나, 인터넷을 통해 데이터를 다운로드 받아서 처리해야 하거나, 영구저장소에 저장된 파일을 액세스 하는 등 시간이 걸릴 수 있는 일을 처리하는 경우에는 메인스레드가 blocking 될 수 있으므로 이렇게 처리해주면 백그라운드에서 돌아가는 것처럼 처리되고 UI 반응은 멈추지 않고 계속 이루어질 수 있다.

만약 저렇게 큐에서 돌아가는 작업이 끝나거나 혹은 그 중간에 UI를 업데이트 하거나 해야 한다면, UI 갱신을 처리하는 부분은 메인 큐이므로 메인 큐에서 필요한 작업을 처리할 수 있다. 즉 메인 큐 ▶ 글로벌 큐에서 동시작업 ▶ 메인큐에서 작업 하는 식으로 중간에 메인 큐에 끼어들 수도 있다.

dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(aQueue, ^{
        // 병렬적으로 진행할 코드
        dispatch_async(dispatch_get_main_queue(), ^{
            //메인큐에서 UI 업데이트 등을 실행
            });
        });

참고자료 : Concurrency Programming Guide

[iOS] 키보드의 크기 구하기

iOS 앱을 디자인할 때, 텍스트 뷰나 텍스트 필드가 화면의 아래쪽에 위치한다면, 이를 탭했을 때 키보드가 올라오면서 정작 입력된 내용이 들어있는 부분을 가려버리게 된다. 따라서 키보드가 올라올 때 텍스트 뷰의 크기나 위치를 변경할 필요가 있다.

키보드가 올라오는 부분은 텍스트 필드/ 텍스트 뷰의 경우에는 didBeginEditing 등의 메소드를 사용할 수도 있지만, 보다 확실하게는 키보드가 나타날 때 발송되는 notification 메시지를 받는 것이 일반적이다. notification 메시지에는 userinfo 사전이 있고 여기에 여러 정보들이 있는데, 우리가 알고 싶은 키보드의 크기도 이 속에 있다.  키보드의 크기는 userInfo 딕셔너리 내의 UIKeyboardFrameBeginUserInfoKey에서 구할 수 있다.

먼저 해당 뷰 컨트롤러가 로드될 때, 키보드에 대한 알림을 받도록 한다. 키보드가 나올 때 알림을 받고 싶다면 NSNotificationCenter에 자신을 옵저버로 등록한다.

-(void)viewDidLaod
{
    ...
        [[NSNotificationCenter defaultCenter]
        addObserver:self
        selector:@selector(keyboardMoved:)
        name:UIKeyboardDidShownNotification
        object:nil];
}

UIKeyboardHadShownNotification 알림은 키보드가 나타났을 때 발송된다. 이 알림을 수신하면 selector에서 지정한 메소드(keyboardMoved:)가 호출된다. 물론 이 메서드는 여기에서 추가해 줘야 한다.

이 메소드는 알림 객체를 받게 되고, 이 알림 객체 속에는 우리가 필요로 하는 정보들이 들어있다. userInfo 프로퍼티를 사용하여 키보드의 프레임을 구한다.

-(void)keyboardMoved:(NSNotification *)notification
{
    if(notification.name = UIKeyboardDidShownNotification) {
        CGRect keyboardFrame = [[notification.userInfo valueForKey:UIKeyboardFrameBeginUserInfoKey] rectValue];
        // <# keyboardFrame 으로부터 키보드의 높이를 구해서 뷰의 크기를 조정 #>
        ....
    }
}

이 때 주의할 것은, 아이폰이 가로 모드일 때에도 키보드의 높이와 폭은 항상 “세로기준”으로 나타난다는 것이다. 즉, 가로 모드에서 키보드가 열리면 키보드 프레임의 height 값은 우리가보는 방향의 높이가 아닌 세로 기준에서의 높이, 즉 480 값을 갖는다.

따라서 텍스트뷰의 자동 조정이 가로모드에서도 정확하게 동작하려면 현재 아이폰이 세로 상태인지, 가로상태인지를 파악해야 한다. 이는 UIViewController의  userInterfaceOrientation 속성에서 구할 수 있다.

또한 NSNotificationCenter에 옵저버를 등록하였으므로, 뷰가 해제될 때, 이렇게 알림을 받도록 옵저버를 등록한 후에는 뷰를 제거할 때 반드시 해제를 해줘야 한다.

-(void)viewDidUnload
{
    ...
        [[NSNotificationCenter defaultCenter] removeObserver:self];
    ...
}

[iOS] UIScrollView 사용법

업데이트

UIScrollView를 Swift에서 사용하는 방법에 대한 (적어도 이 글 보다는 나은) 새 버전을 참고하세요.

UIScrollView는 gesture recognizer를 내장하여 실제 뷰 영역보다도 큰 영역을 스크롤하여 내용을 볼 수 있도록 해주는 클래스이다. 사진 앨범 앱의 사진 보기 화면에서 이 스크롤뷰가 사용된다. (카메라롤의 사진 목록 역시 스크롤뷰로 구현되어 있다.)

스크롤뷰는 관성 이동은 물론 내부 컨텐츠를 확대/축소하는 방법을 아주 간단히 처리할 수 있어 주로 이미지와 관련된 화면에서 상당히 유용하게 활용할 수 있다.

스크롤뷰를 사용하는 방법은 UIViewController와 거의 유사하다. 인스턴스를 생성해서 하위뷰를 추가해주면 된다. 스크롤뷰의 뷰 크기는 실제 화면에 노출되는 영역의 크기이고, 실제 전체 컨텐츠의 영역을 ContentSize로 지정해주어야 스크롤이 제대로 동작한다.

또한 줌을 위해서는 최대스케일 값과 최소 스케일 값을 지정해야 하며 (이는 IB에서도 할 수 있다.) 실제 줌 동작에 반응하기 위해서는 델리게이트가 어떤 뷰가 줌을 받게 되는지를 지정해 주어야 한다. 아주 간단한 예제를 통해 알아보도록 한다.

스크롤뷰 스터디 샘플

설명의 편의를 위해 이번에는 IB를 전혀 사용하지 않고 코드로만 작업해 본다. 우선 프로젝트를 신규로 만든다. 이 때 템플릿은 Single View Application을 사용한다. 또한 스크롤 뷰를 위해 큼지막한 이미지를 하나 더 준비한다. 프로젝트가 생성되면 Xcode 창의 왼쪽 파일 네비게이션 영역으로 파일을 끌어다 놓으면 프로젝트에 이미지 파일을 추가할 수 있다.

ViewController.m

오늘은 이 파일에서 모든 것을 처리해보자. 앱이 실행되면 루트 뷰에 스크롤뷰를 하나 추가하는데, 이 스크롤뷰에는 UIImageView가 하나 추가된다. 이 이미지뷰 안에 방금 추가한 이미지를 넣어서 스크롤이 되도록 해 볼 것이다.

먼저 파일 이름을 따로 매크로로 만들어 두고, 바로 private interface를 정의하도록 한다.

#import "ViewController.h"

#define SCV_IMAGE_FILENAME @"이미지파일.JPG"

@interface ViewController() <UIScrollViewDelegate>
@property (strong, nonatomic) UIScrollView *scrollView;
@property (strong, nonatomic) UIImageView *imageView;
@property (strong, nonatomic) UIImage *anImage;
@end

인터페이스를 정의하면서 뷰컨트롤러가 UIScrollViewDelegate 프로토콜을 따르도록 했다. 이는 줌인/줌아웃을 어떤 뷰가 받을지를 스크롤뷰에게 알려주는 역할을 하기 위해서다. (물론 imageView가 하게 된다.)

다음은 synthesize 하면서 인스턴스 변수명을 함께 정해주자. (변수명 앞에 언더스코어를 붙이는 것은 혼동을 방지하기 위해서이다. 혹은 어떤 규칙이 있는지도 모르겠다.)
@synthesize scrollView = _scrollView, imageView = _imageView, anImage = _anImage;

이제 각 프로퍼티는 처음으로 호출될 때 초기화되도록 하면 된다. 이 때 스크롤뷰의 초기화 부분을 눈여겨 보라.

-(UIImage *)anImage
{
    if(!_anImage) _anImage = [UIImage imageWithName:SCV_IMAGE_FILENAME];
    return _anImage;
}

-(UIImageView *)imageView
{
    if(!_imageView) _imageView = [[UIImageView] alloc]initWithImage:self.anImage];
    return _imageView;
}

-(UIScrollView *)scrollView
{
    if(!_scrollView) {
        CGRect viewFrame = CGRectMake(0,0,320.0f,460.0f);
        _scrollView = [[UIScrollView alloc] initWithFrame:viewFrame];
        _scrollView.contentSize = self.imageView.frame.size;
        _scrollView.minimumZoomScale = 0.1f;
        _scrollView.maximumZoomScale = 3.0f;
        _scrollView.delegate = self;
        [_scrollView addSubview:self.imageView];
    }
    return _scrollView;
}

이제 앱이 실행되고 루트뷰가 로드될 때 스크롤뷰를 화면에 추가하도록 하자.

-(void)viewDidLoad
{
    [super viewDidLoad];
    [self.view addSubview:self.scrollView];
}

스크롤뷰에서 확대 축소하기

이제 앱을 빌드하고 실행하면 이미지가 화면에 표시되고, 드래그하여 스크롤이 되는 것을 확인할 수 있다. 하지만 아직 줌이 되지 않는다. 이는 위에서 이야기한 스크롤뷰에게 어떤 뷰가 줌이 되는지를 알려주지 않아서이다. UIScrollViewDelegate 프로토콜에 정의된 메소드 중 viewForZoomingInScrollView:를 추가해준다.

-(UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
    return self.imageView;
}

이제 이미지를 두 손가락으로 확대/축소할 수 있게 됨을 볼 수 있다.

[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 속성 더보기