[C/Objective-C] 블럭

Block

Block은 어디다 쓰나

블럭은 작업의 단위(= 코드 조각)을 캡슐화한 객체이고, 언제든 실행할 수 있다. 이는 포터블한 익명의 함수와 같아서 함수나 메소드의 인자로 넘겨지거나 반환될 수 있다. 또한 그 자체로 인자를 받을 수 있고 리턴 타입을 가질 수 있다. 블럭은 변수에 할당한 다음, 일반적인 함수 호출과 같은 방식으로 실행할 수 있다.

캐럿(^)기호가 블럭을 표시하는데 사용된다. 예를 들어 다음의 코드는 두 개의 정수값을 받아 정수를 리턴하는 블럭 변수를 선언한다.

int (^Multiply)(int, int) = ^(int num1, int num2) {
    return num1 * num2;
};
int result = Multiply(4, 7); // Result == 28

메소드 및 함수의 인자로서 블럭은 콜백의 타입이 될 수 있다. 블럭을 넘겨주어 호출하는 코드는 메소드나 함수의 동작을 커스터마이즈 할 수 있다. 호출 시점에 함수는 지정된 일을 하고 다시 블럭에서 지정한 일을 수행할 수 있다.

블럭을 쓰면 호출 시점에 콜백 코드를 제공할 수 있게 된다. 이 콜백 코드는 별도의 메소드나 함수로 선언할 필요가 없으므로 구현 코드가 단순해지고 읽기도 쉬워진다. NSNotification 형태의 알림을 받는 방법은 아래 코드와 같이 전개될 수 있다. 전통적인 접근방식에서는 객체는 스스로를 알림의 옵저버로 등록하고 별도의 메소드를 구현해야 할 것이다.

-(void)viewDidLoad 
{
    [super viewDidLoad];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) mane:UIKeyboardWillShowNotification object:nil];
}

-(void)keyboardWillShow:(NSNotification *) notification {
    // 알림에 대한 처리. 
}

블럭을 쓰게되면 addObserverForName:object:queue:usingBlock: 메소드를 사용해서 이를 한 번에 구현한다.

-(void)viewDidLoad
{
    [super viewDidLoad];
    [[NSNotificationCenter defaultCenter] addObserverForName:UIKeyboardWillShowNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif){
        // 알림에 대한 처리
        }];
}

블럭의 또 다른 장점은 lexical 한 범위에 있어서 블럭은 지역 함수에 액세스 할 수 있다는 점이다. 만약 어떤 메소드를 구현하면서 메소드 내에서 블럭을 정의하면, 이 블럭은 메소드의 파라미터나 지역 변수에 액세스 할 수 있게 된다. 이 접근은 기본적으로 읽기전용이지만, 만약 메소드 내의 지역 변수를 __block 키워드와 함께 선언했다면 블럭 내에서 이 변수의 값을 변경할 수도 있다. 만약 메소드나 함수가 값을 리턴하고 그 속의 지역 변수가 모두 파괴되었을 때도, 블럭의 레퍼런스가 유효하다면 블럭이 참조하고 있는 변수들은 여전히 살아남는다.

시스템 프레임워크 API에서의 블럭들

점점 더 많은 시스템 프레임워크가 파라미터로 블럭을 받고 있다. 다음과 같은 대 여섯개의 종류가 코드 블럭을 사용한다.

  • 완료 핸들러
  • 알림 핸들러
  • 에러 핸들러
  • 열거 (enumeration)
  • 뷰 애니메이션과 트랜지션
  • 정렬

아래에서 이러한 각각의 경우를 살펴볼텐데, 그 전에 프레임워크 메소드에서의 블럭 선언에 대해 잠깐 살펴보자.

-(NSSet *)objectPassingTest:(BOOL^(id obj, BOOL *stop))predicate

블럭 선언은 이 메소드가 블럭에게 동적으로 타이핑되는 객체를 전달하며, 불리언 값으 참조를 넘겨주는 것을 나타낸다. 그리고 이 블럭은 불리언 값을 반환한다. 블럭을 실제 구현할 때는 캐럿으로 시작하도록 하면 된다.

[mySet objectPassingTest:^(id obj, BOOL *stop) {
    // 객체 전달 테스트가 성공이면 YES를 아니면 NO를 반환하는 코드.
}];

완료와 에러 핸들러

완료 핸들러는 프레임워크 함수가 어떤 일을 마친 후에 클라이언트가 호출하는 콜백함수를 말한다. 흔히 클라이언트는 함수 완료 후에 자원을 정리하거나 UI를 업데이트하기 위해 콜백을 수행해야 한다. 여러 프레임워크 메소드는 이러한 핸들러를 블럭으로 구현하도록 한다. (달리 구현하자면 델리게이션이나 알림을 사용하는 방법이 있다.)

UIView 클래스는 애니메이션이나 뷰 전환과 관련되어 완료핸들러 블럭 파라미터를 받는다. 아래 코드는 animationWithDuration: animations: completion: 메소드의 예이다. 완료 핸들러는 뷰의 위치와 알파값을 원상 복귀 시킨다.

-(IBAction) animateView:(id)sender {
    CGRect cacheFrame = self.imageView.frame;
    [UIView animateWithDuration:1.5f animations:^{
        CGRect newFrame = self.imageView.frame;
        newFrame.origin.y += 150.0;
        self.imageView.frame = newFrame;
        self.imageView.alpha = 0.2;
    }
    completion: ^(BOOL finished) {
        if (finished) {
            self.imageView.frame = chaceFrame;
            self.imageView.alpha = 1.0;
        }
    }];
}

에러 핸들러도 이런 식으로 정의되는데, 흔히 에러 상황을 사용자에게 알려주는데 사용한다.

알림핸들러

NSNotificationCenteraddObserverForName:object:queue:usingBlock: 메소드는 프로그래머가 알림에 대한 핸들러를 옵저버를 등록하는 시점에 작성하도록 한다. 아래는 그 예로, 메소드는 NSNotification 객체를 파라미터로 전달한다.

-(void)applicationDidFinishedLaunching:(NSNotification *)aNotification {
    opQ = [[NSOperationQueue alloc] init];
    [[NSNotificationCenter defatultCenter] addObserverForName:@"CustomOpertaionCompleted"
        object:nil
        queue:opQ
        usingBlock: ^(NSNotification *notif) {
            NSNumber *theNum = [notif.userInfo objectForKey:@"NumberOfItemsProcessed"];
            NSLog(@"Number of Items Processed : %i", [theNum intValue]);
        }];
}

열거

파운데이션 프레임워크의 집합 클래스 (NSArray와 같은…)등른 각각의 원소에 대해 어떤 작업을 수행하도록 하는 메소드를 가지고 있다. 쉽게 말해 메소드는 그 자신에 대해 다음과 같은 식의 처리를 한다.

for (id item in colletion ) {
    // 개별 아이템에 대한 처리
}

이런 메소드에는 두 종류가 있는데, 하나는 enumerate로 시작하는 이름을 가지고 리턴값이 없는 메소드이다. 이 메소드는 블럭으로 받은 코드를 개별 원소에 대해 각각 수행하게 된다. 다른 하나는 passingTest: 인데 이 종류의 메소드는 정수나 NSIndexSet를 반환한다. 이는 각각의 원소에 대해 테스트를 수행하여 YES가 리턴되는 객체들의 인덱스나 인덱스 집합을 리턴해준다.

아래 코드는 이런 메소드들을 사용하는 예이다.

    NSString *area = @"Europe";
    NSArray *timeZoneNames = [NSTimeZone knownTimeZoneNames];
    NSMutableArray *areaArray = [NSMutableArray arrayWithCapacity:1];
    NSIndexSet *areaIndexes = [timeZoneNames indexesOfObjectsWithOptions:NSEnuertaionConcurrent passingTest:^(id obj, NSUInteger idx, BOOL *stop) {
        NSString *tmpStr = (NSString*)obj;
        return [tmpStr hasPrefix:area];
    }];

    NSArray *tmpArray = [timeZoneNames objectsAtIndexes:areaIndexes];
    [tmpArray enumerateObjectsWithOptions:NSEnumerationConcurrent|NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        [areaArray addObject:[obj substringFromIndex:[area length]+1]];
    }];
    NSLog(@"Cities in %@ time zone:%@", area, areaArray);

stop 파라미터는 참조로 넘겨지는데, 여기에 NO를 부여하면 메소드는 루프를 그만두게 된다.

NSString은 비록 집합은 아니지만 유사한 방식의 메소드들을 가지고 있다. enumerateSubstringsInRange:options:usingBlock:enumerateLinesUsingBlock:이 그것이다. 아래 코드는 파일을 읽어 각 줄에서 “Beatles”라는 단어가 있는지를 검사하는 코드이다.

NSString *musician = @"Beatles";
NSString *musicDate = [NSString stringWithContentsOfFile:@"/usr/share/calendar/calendar.music" encoding:NSASCIIStringEncoding error:NULL];
[musicDates enumerateSubStringsInRange:NSMakeRange(0, [musicDates length]-1) 
    options:NSStringEnumerationByLines
    usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) {
        NSRange found = [substring rangeOfString:musician];
        if (found.location != NSNotFound) {
            NSLog(@"%@", substring);
    }
}];

뷰 애니메이션

간단히 코드로만.

[UIView animateWithDuration:0.2 animations:^{
    view.alpha = 0.0;
    } 
    completion:^(BOOL finished) {
        [view removeFromSuperView];
    }];

다음은 뷰 전환

[UIView TransitionWithView:containerView duration:0.2 options:UIViewAnimationOptionTransitionFlipFromLeft
    animations:^{
        [fromView removeFromSuperView];
        [containerView addSubView:toView];
    }
    completion:NULL];

정렬

파운데이션에는 NSComparator가 정의되어 있는데, 이는 두 객체를 비교한다.

typedef NSComparison Result (^NSComparator)(id obj1, id obj2);

이 블럭은 다음과 같이 사용한다. @_@ 아 복잡해.

NSArray *stringsArray  = @{ @"string 1", @"String 21", @"string 12", @"String 11", @"String 02"};
static NSStringCompareOptions comparisonOptions  = NSCaseInsensitiveSearch | NSNumericSearch | NSWidthInsensitiveSearch | NSForcedOrderingSearch;
NSLocale *currentLocale = [NSLocale currentLocale];
NSComparator findSort = ^(id string1, id string2) {
    NSRange string1Range = NSMakeRagne(0, [string length]);
    return [string1 compare:string2 optiions:comparisonOptions range:string1Range locale:currentLocale];
};
NSLog(@"finderSort: %@", [stringsArray sortedArrayUsingComparator:findSort]);

블럭과 병렬작업

블럭은 코드를 포터블하게 쓸 수 있도록 캡슐화한 것이고, 비동기적으로 나중에 수해될 수 있으므로 GCD와 NSOperationQueue에서 쓰기에 알맞다.