이번에 두번째 계산기를 개발하다가 만들게된 콤마찍기 알고리즘(?)입니다. 구상하기는 생각보다 복잡했고 시간이 많이 걸렸는데 짜는데 시간은 얼마 안걸렸네요.. (허망) 
아무튼 짠다고 고생했는데 혼자 썩히기엔 아까워서 공유합니다.

사용법

  • 기본적으로 convertCommaFormula: 를 사용하면 문자열을 반환해줍니다.
  • 추가적으로 convertCommaFormula:location: 을 사용하면 현재 커서 위치를 파라미터로 넣고 현재 커서 위치까지 반환(NSDictionary)해 줍니다. 
저는 convertCommaFormula:location: 메서드를 구현해서 사용했었고 편의를 위해 convertCommaFormula: 를 뽑아보았습니다.

NSString *text = @"482193423+1284+41-(327123.4+2)";
NSString *resultText = [NSString convertCommaFormula:text];
NSLog(@"result : %@", resultText); 
// result : 482,193,423+1,284+41-(327,123.4+2)


코드

NSString+ConvertComma.h

#define COMMA_INDEX 3

@interface NSString (ConvertComma)

+ (NSString *) convertCommaFormula:(NSString *)formula ;
+ (NSDictionary *) convertCommaFormula:(NSString *)formula cursorlocation:(NSInteger)location ;

@end


NSString+ConvertComma.m

@implementation NSString (ConvertComma)


+ (NSString *) convertCommaFormula:(NSString *)formula {
    return [NSString convertCommaFormula:formula cursorlocation:0][@"text"];
}

+ (NSDictionary *) convertCommaFormula:(NSString *)formula cursorlocation:(NSInteger)location {
    
    NSString *beforeCursorText = [formula substringWithRange:NSMakeRange(0, location)];
    NSString *removedCommaBeforeCursorText = [beforeCursorText stringByReplacingOccurrencesOfString:@"," withString:@""];
    NSString *removedText = [formula stringByReplacingOccurrencesOfString:@"," withString:@""];
    
    NSMutableString *mBeforeText = [NSMutableString stringWithString:removedCommaBeforeCursorText];
    NSMutableString *mOriginText = [NSMutableString stringWithString:removedText];
    // 12|74
    
    if (![LayoutInfo shared].isNotComma) {
        NSInteger numbercount = 0;
        for (NSInteger i=mOriginText.length-1; i>=0; i--) {
            char c = [mOriginText characterAtIndex:i];
            if ('0'<=c && c<='9') {
                numbercount++;
            } else if (c=='.') {
                numbercount = 0;
                // 다시 되돌아가면서 콤마 지우기
                for (NSInteger j=i; j<mOriginText.length; j++) {
                    char cc = [mOriginText characterAtIndex:j];
                    if (cc==',') {
                        [mOriginText replaceCharactersInRange:NSMakeRange(j, 1) withString:@""];
                        if (j<mBeforeText.length) {
                            [mBeforeText replaceCharactersInRange:NSMakeRange(j, 1) withString:@""];
                        }
                        j--;
                    } else if ('0'<=cc && cc<='9') {
                        continue;
                    } else {
                        if (i==j) continue;
                        break;
                    }
                }
            } else {
                numbercount = 0;
            }
            
            
            if (numbercount==COMMA_INDEX+1) {
                numbercount-=COMMA_INDEX;
                // i+1번째에 ,추가
                
                [mOriginText replaceCharactersInRange:NSMakeRange(i+1, 0) withString:@","];
                if (i+1<mBeforeText.length) {
                    [mBeforeText replaceCharactersInRange:NSMakeRange(i+1, 0) withString:@","];
                }
            }
        }
    }
    
    return @{@"text":mOriginText, @"cursorindex":@(mBeforeText.length)};
}


@end




WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,
이번 포스팅에서는 iOS에서 Objective-C로 푸시를 구현하는 방법에 대해 설명하겠다. 그렇지만 단순히 푸시를 받는것이 다가 아닌 푸시를 받았을 때 다음 이벤트 처리도 앱에서 어떻게 처리할지에대한 고민도 함께할 것이다. 이 부분은 애플에서 제공하는 방법이 생각보다 복잡하게 되있는것 같으므로 포스팅에 남기기로 하였다.

먼저 푸시를 받는 방법에 대해 설명하..  검색해보니 바로 원하는 링크가 안나와서 푸시 받는법부터 설명해보겠다.



1. 푸시 아이디를 얻어내는법

푸시 아이디를 얻고자 하는 시점에서 다음을 호출하면 된다. 
// 현재 푸시가 On인지 Off인지 알아내는 함수
BOOL pushEnable = NO;
if ([[UIApplication sharedApplication] respondsToSelector:@selector(isRegisteredForRemoteNotifications)]) {
    pushEnable = [[UIApplication sharedApplication] isRegisteredForRemoteNotifications];
} else {
    UIRemoteNotificationType types = [[UIApplication sharedApplication] enabledRemoteNotificationTypes];
    pushEnable = types & UIRemoteNotificationTypeAlert;
}

// 푸시 아이디를 달라고 폰에다가 요청하는 함수
UIApplication *application = [UIApplication sharedApplication];
if ([application respondsToSelector:@selector(isRegisteredForRemoteNotifications)]) {
    NSLog(@"upper ios8");
    // iOS 8 Notifications
    [application registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:(UIUserNotificationTypeSound | UIUserNotificationTypeAlert | UIUserNotificationTypeBadge) categories:nil]];
    [application registerForRemoteNotifications];
} else {
    NSLog(@"down ios8");
    // iOS < 8 Notifications
    [application registerForRemoteNotificationTypes:
     (UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeAlert | UIRemoteNotificationTypeSound)];
}


// AppDelegate.m 파일에서 아래 함수를 추가한다. 아래 함수는 푸시 아이디를 받아내는 함수이고, 푸시아이디는 아래 함수를 통해서만 받을 수 있다.
// AppDelegate.m
- (void)application:(UIApplication *)app didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
     NSString* newToken = [[[NSString stringWithFormat:@"%@",deviceToken]
                           stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"<>"]] stringByReplacingOccurrencesOfString:@" " withString:@""];
     NSLog(@"DeviceToken : %@", newToken );
}


위 코드를 보면 이것저것 복잡해보인다. 여기서 중요한 라인만 설명을 하자면

UIApplication *application = [UIApplication sharedApplication];
[application registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:(UIUserNotificationTypeSound | UIUserNotificationTypeAlert | UIUserNotificationTypeBadge) categories:nil]];
[application registerForRemoteNotifications];
위 세 라인이다.
1. 앱 싱글톤 델리게이트를 받아와서 2. 푸시받을 타입을 정한다. 그리고 3. 실제로 푸시 아이디를 달라고 요청한다.
registerForRemoteNotifications가 호출되는 순간 appdelegate.m에 
첫째, 앱을 설치하고 처음으로 registerForRemoteNotifications를 호출하는 상황이면 사용자에게 푸시를 허용할지 말지에 대한 경고창이 뜬다. 여기서 허용을 하면 AppDelegate.m에 구현된application:didRegisterForRemoteNotificationsWithDeviceToken: 함수가 호출되면서 deviceToken파라미터에 푸시아이디가 담기게 된다. 반대로 사용자가 푸시 허용을 하지 않는다면 application:didRegisterForRemoteNotificationsWithDeviceToken: 함수는 호출되지 않는다. 그게 다다. 사용자가 푸시알림 허용 안했다고해서 뭐 어떻게 더 바로 할 수 있는게 없다. 푸시알림 무한 유도를 막기위함의 애플의 생각이 아닐까 싶은데, 이 부분때문에 고민을 좀 많이했다. 그래서 준비한게 BOOL pushEnable;이고 푸시가 현재 허용되있는지 아닌지 대강 판단해준다. 푸시가 꼭 필요한 어플일 경우는 pushEnable를 받아내서 확인할 수 있다. (하지만 이것도 사용자가 한번도 푸시 설정을 안했는지, 실제 푸시 거부를 한건지 알 방법은 없다. 단지 현재 푸시 설정 On/Off 여부만 알 수 있다.)
둘째, 앱을 설치하고 푸시 허용을 해놓은 상태이며(최초 registerForRemoteNotifications를 호출 해본 상태), registerForRemoteNotifications를 호출했다면 AppDelegate.m에 application:didRegisterForRemoteNotificationsWithDeviceToken:함수가 실행되면서 푸시아이디를 받아올 수 있다. 푸시 아이디를 서버에 등록할 때 사실상 푸시 아이디는 바뀔 수가 있다. 그렇기에 나같은 경우는 푸시아이디와 디바이스아이디를 함께 서버에 보내서 디바이스 아이디 기준으로 푸시 아이디를 저장한다. 아무튼 두번째의 경우는 무조건 푸시아이디를 받아올 수 있는 상황이다.

세번째, 앱을 설치하고 푸시 허용을 해제해놓은 상태이며(마찬가지로 최초 registerForRemoteNotifications를 호출 해본 상태), registerForRemoteNotifications를 호출했다면 아무일도 일어나지 않는다. 그렇기때문에 이 경우는 미리 pushEnable를 받아내서 푸시가 가능한지 아닌지를 판단해놓을 필요가 있다. pushEnableNO이면 푸시 허용 Off인 상태인거다. 그러면 푸시 설정을 해라고 경고를 띄우면 된다. 


2. 서버에서 푸시를 쏘았을때 폰에서 받은 위 호출되는 함수
서버에서 푸시를 쏘자마자 알아내는 방법은 하나밖에 없다. -> 앱이 실행되는 중일때이다.
앱이 만약 꺼저있거나, 백그라운드에서 돌고있으면 푸시가 왔는지 안왔는지 앱에서는 모른다. 앱이 다시 포그라운드로 왔을때 푸시가 왔으면 ‘푸시받는함수'가 호출된다. 하지만 웃긴것이, 앱이 백그라운드에도 없고 메모리에 올라가있지 않을때 푸시가 왔을때, 바탕화면에서 푸시를 받아서 푸시를 눌러 앱에 들어가면 ‘푸시받는함수’가 호출되지 않는다. 이 예외적인 부분을 처리하는 방법에 대해 설명하겠다.
 
// AppDelegate.m
// 앱이 런칭되서 메모리에 올라갈때 실행되는 함수이다.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    UILocalNotification *notificationUserInfo = [launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey];
    if (notificationUserInfo) {
//        NSLog(@"app recieved notification from remote%@",notification);
        NSMutableDictionary *mNotificationUserInfo = [NSMutableDictionary dictionaryWithDictionary:(id)notificationUserInfo];
        mNotificationUserInfo[@"appLaunch"] = @"yes";
        [self application:application didReceiveRemoteNotification:mNotificationUserInfo];
    }else{
//        NSLog(@"app did not recieve notification");
    }
    return YES;
}

여기서
// AppDelegate.m
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {

    [PushController shared].userInfo = userInfo;

    if (userInfo[@"appLaunch"]) {
        [PushController shared].waitingViewDidLoad = YES;
    } else {
        if (application.applicationState==UIApplicationStateActive) {
            [[PushController shared] pushInActiveStatus];
        } else {
            // if (application.applicationState==UIApplicationStateInactive) {
            [[PushController shared] presentPushPostTableViewController];
        }
    }
}
위의 PushController는 일단 신경쓰지 말고 푸시를 받았을 때 라이프 사이클만 신경쓰자. 
저기서 특이한 점은 
if (userInfo[@"appLaunch”])
를 이용하여 첫째 상황인지 먼저 구분하고
if (application.applicationState==UIApplicationStateActive) ;
를 이용하여 둘째상황과 셋째상황을 구분했다는 점이다.

첫째 상황에서는 아직 ViewController가 생성이 되기 전의 라이프사이클이다. 그렇기때문에 ViewControllerviewDidLoad를 호출하기 전까지 푸시정보를 잠시 가지고 있어야한다. 
둘째 상황에서는 application.applicationState==UIApplicationStateInactive에 해당하는 상태인데, 현재 앱이 백그라운드에서 포그라운드로 가고 있다는 뜻으로 사용자가 앱을 켜놓고 바탕화면에서 푸시를 눌렀을 때를 말한다. 이때는 바로 푸시 행위를 진행하면 된다.
셋째상황은 application.applicationState==UIApplicationStateActive의 상황으로써 앱이 이미 포그라운드에서 돌고 있었다는 뜻이고 이때는 자동으로 푸시 기능을 실행시켜버리는게 아니라 상단에 팝업창이 떠서 푸시가 온것처럼 만들어주면 사용자가 인지하기 쉬울 것이다. (안드로이드는 기본으로 제공되고, 아이폰은 카톡에서 볼 수 있는 기능이다) 상단에서 팝업이 잠시 내려왔을때 팝업을 누르면 푸시 기능을 수행하게 만들었는데, 이 기능은 PushController에 있다. PushController는 UI작업을 좀 했기 때문에 라인이 좀 되서 첨부해두겠다.

아무튼 위와같이 처리하면 푸시를 받는 일을 놓히지 않고 처리할 수 있다. 
 



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

원본 : http://www.raywenderlich.com/60749/grand-central-dispatch-in-depth-part-1

위 링크의 강좌를 2포스트에 걸쳐서 번역을 진행할 것이다. 본 포스트는 위 원문 포스팅의 절반을 번역한 것이다.


Update note : Swift를 사용한 iOS8 기반의 Grand Central Dispatch tutorial 최신 튜토리얼이 있다!

비록 Grand Central Dispatch(혹은 줄여서 GCD)가 얼마동안 주변에 있어 왔다고 해서, 모두가 그것을 잘 아는건 아니다. 이것은 이렇게 이해될 수 있다; 동시에 하는것은 편법이고, C기반 API의 GCD는 Objective-C의 세계로 자연스럽게 뽀죡한 끝으로 푹 찌르는 것처럼 보인다. 깊이있게 두 파트의 시리즈를 가진 Grand Central Dispatch 강좌를 배워보자.

이 두 파트에서, 첫번째 강좌에서는 GCD가 뭘하는지와 GCD 기능 기반의 몇몇 예시를 보여줄것이다. 두번째 파트는 GCD가 가진 더 많은 기능적인 이점을 배울 것이다. 



GCD란?
GCD는 libdispatch를 위한 마케팅적인 이름이다, 애플 라이브러리는 iOS와 OSX의 멀티코어 하드웨어에서 코드 동시 실행을 위한 기능을 제공한다. 이것은 아래 이점들을 따른다:
  • GCD는 무거운 처리를 살짝 뒤로 미루고 백그라운드에서 처리할 수 있게 도와주므로써  당신 앱의 대응력을 높혀준다.
  • GCD는 쓰레드를 락 거는것 보다 더 쉬운 동시 실행 모델을 제공하고 동시 실행시 생기는 버그들을 피할 수 있게 도와준다.
  • GCD는 싱글톤으로써 일반적인 패턴의 높은 퍼포먼스와 함께 당신의 코드를 아름답게 만들어 줄 것이다.
이 강좌는 블록코딩과 GCD가 어떻게 동작하는지 잘 알고있다는 가정하에 진행한다. 만약 GCD를 처음 접한다면 Multithreading and Grand Central Dispatch on iOS for Beginners를 먼저 필수로 보고 오면 좋겠다.


GCD 용어
GCD를 이해하기위해, 여러분은 쓰레딩과 동시실행어대한 몇몇 개념을 명확하게 할 필요가 있다. 이것들은 둘다 모호하고 헷갈리는 것이지만 잠시 다시 GCD의 관점에서 그것들을 가볍게 생기시켜 봐야한다.
(역자 주 : 원문에서는 GCD 용어라고 해놓았지만, 구글에 "쓰레드 용어"로 검색하면 더 많은 정보를 얻을 수 있다)

Serial vs. Concurrent
이 용어는 작업들이 서로관에 연관이되어 실행되는 경우를 설명하는 용어이다. 연속적으로 실행되는 작업들은 항상 한번에 실행된다. 작업들은 한번에 동시에 실행되어야한다.

비록 이 용어가 넓은 응용프로그램이지만, 이 강의의 목적상 당신은 Objective-C block 작업 수행에만 초점을 맞출 수 있다. block이 뭔지 모른다고? 여기 이 강좌(How to Use Blocks in iOS 5 Tutorial)를 참고해라. 사실 당신은 또한 함수포인터와 함께 GCD를 사용할 수 있지만, 대부분의 케이스에서는 이것이 사용에 실질적이고 편법적이다. Block은 매우 편하다!



Synchronous vs. Asynchronous
GCD에서, 이 용어는 함수가 연관된 다른 작업을 끝났을때 함수는 실행하기위해 GCD에게 물어보는 것을 뜻한다. 동기화(synchronous) 함수는 명령된 일이 모두 끝난 후에 리턴한다.

반면 비동기화(asynchronous) 함수는 즉시 리턴하고, 일이 다 끝났다고 알려주지만 실제 일이 끝날때까지 기다리지는 않는다. 그러므로 비동기화 함수는 다음 함수에서 처리되는 실행의 현재 쓰레드 block이 아니다.

조심하자 - 현재 쓰레드의 동기화 함수 “blocks”을 읽을때나 함수가 “blocking”함수 이거나 blocking operation인경우 혼동하지 말자! blocks라는 동사는 어떻게 한 함수가 현재 쓰레드에 영향을 미치는지이고 명사 block과는 아무 연관이 없다. 명사 block는 Objective-C에서 이름없는 함수의 용어이고 GCD에 보내는 일들을 정의한다.

Critical Section
이것은 한번에 두 쓰레드가 도는 상황에서 반드시 동시에 실행되지 않는 코드 조각이다. 동시 프로세스에 의해 접근된다면, 이것은 코드가 (변수와 같은) 공유된 자원을 동시에 건드릴 수 있기 때문에 잘못된 값이 들어가거나 할 수 있다.

Race Condition
---

Deadlock
만약 그 두가지(혹은 그 이상)의 일이 서로의 처리가 끝나기를 기다리고 있을때 이것을 deadlock 되었다고 부른다(대부분의 쓰레드에서 나타날 수 있다). 첫번째 처리는 두번째 처리가 끝나기를 기다리기 때문에 끝날 수 없다. 그러나 두번째 처리 또한 첫번째 처리가 끝나기를 기다리기 때문에 두번째 처리도 끝날 수가 없을 것이다.

Thread Safe
쓰레드 세이프 코드는 여러 문제(데이터 오염, 크래쉬 등등)를 피하면서 멀티쓰레딩이나 동시처리로부터 안전하게 콜이 가능하다. 쓰레드 세이프가 되지 못한 코드는 한번에 한 콘텍스 안에서 동작한다. 쓰레드 세이프 코드 중 하나는 NSDictionary이다. 당신은 멀티 쓰레드 이슈 없이 저것을 이용할 수 있다. 반면 NSMutableDictionary는 쓰레드 세이프가 아니다. 이것은 오직 한번에 한 쓰레드안에서 접근할 수 있다.

Context Switch
context switch는 당신이 한 싱글 프로세스에서 다른 쓰레드를 실행하여 전환할 때의 저장 및 복구 실행 상태의 프로세스이다. 이 프로세스는 멀티쓰레딩하는 앱을 만들때 아주 일반적인 방법이지만, 이것은 추가적인 비용이 따른다.

Concurrency vs Parallelism

참고 한글 자료 : http://skyul.tistory.com/263
Concurrency와 parallelism은 종종 함께 거론된다. 그래서 짧게 그 두개를 비교하여 설명할 것이다.

concurrent 코드의 나눠진 부분은 “동시에” 실행될 수 있다. 그러나 그것이 어떻게 처리되는지는 시스템이 정하기 나름이다.

멀티코어 디바이스는 병렬적으로 같은 시간에 멀티 쓰레딩을 실행시키지만, 싱글코어 디바이스는 이것을 수행하기 위해 쓰레드를 실행시키고, 컨텍스 스위칭을 동작하며, 다른 쓰레드나 프로세스를 작동시킨다. 이것은 아래 그림처럼 마치 병렬적으로 수행되게 보이는데 충분히 빨리 수행된다.

비록 당신이 GCD에서 동시수행을 코드에 사용했었어도, 얼마나 병렬수행이 필요한지 GCD가 정하기 나름이다. 병력수행은 동시에 일어나는 것을 요구한다. 그러나 ‘동시’는 보장된 병렬을 제공하지 않는다.

중요한 포인트는 ‘동시’는 사실 구조에 관한 것이다. 당신이 GCD를 코드에 사용할 생각이 있을 때, 당신은 동시에 일어날 수 있는 일의 부분을 당신 코드 구조체에 노출한다. 게다가 한개는 반드시 동시에 일어나지 않는다. 만약 당신이 이 주제에대해 더 깊게 알고싶으면 this excellent talk by Rob Pike를 확인해보아라.

Queues

GCD는 코드의 블럭들을 다룰 수 있게
dispatch queues를 제공한다; 큐(queue)들은 당신이 GCD에 제공한 테스크를 관리하거나 FIFO 명령에 의한 테스크를 실행시킨다. 첫 테스크를 큐에 추가하는데 이점은 첫번째 테스크가 큐에서 시작되고 두번째로 추가된 테스크가 두번째에서 시작할것이며, 차례로 될 것이다. 

모든 dispatch queues는 당신이 멀티쓰레드에서 동시에 접근하려해도 스스로 쓰레드 세이프하다. 이러한 GCD의 이점은 어떻게해서 dispatch queues가 당신의 코드의 부분에서 쓰레드-세이프한 것을 제공하는지 이해할때 나타날 것이다. 이것의 핵심은 옳은 dispatch queue 종류와 옳은 dispatching function을 골라서 당신의 일의 queue에 보내기 위함이다.

이 섹션에서 특별한 GCD queue가 제공하는 두가지 종류의 dispatch queue를 보게 될 것이다, 그리고 GCD dispatching function과 함께
큐에 어떻게 추가하는지 알려주는 형상화한 예제를 돌려볼 것이다.

Serial Queues

serial queues에서 데스크는 한번에 한회만 실행된다, 각 테스크는 이전 실행되는 테스크가 끝나고나야지만이 시작된다. 뿐만아니라, 아래 그림과같이 우리는 한 블럭의 끝나는 점과 다음 블럭의 시작하는 점의 시간차이를 알지도 못한다.

이 테스크의 동작 타이밍은 GCD의 컨트롤 아래에 이루어진다; 당신이 알고 있는 GCD의 이점은 한번에 한번만 수행한다는것과 queue에 추가되어서 명령을 받으면 테스크를 실행한다는 것이다.

Seiral queue에서는 절때 동시에 두 테스크가 실행될 수 없으나, 동시실행에서 같은 섹션을 접근할 위험은 없다; 이 테스크를 오직 수행한다는 점에서 race condition으로부터 섹션이 위험해지는것을 막는다. 그러므로 위험한 섹션에 접근 할 수 있는 유일한 방법은 dispatch queue에 테스크를 담아서 보내는 방법이다, 그리고 위험한 섹션이 안전하다는 것을 검증받을 수 있다.



Concurrent Queues

concurrent queues에 테스크들은 추가하라는 명령에서 시작하는 보장을 가지고... 그것은 당신이 완전히 보장됬다는 것을 의미한다! 요소들은 어떤 명령들도 끝낼 수 있고 당신이 다음 블럭이 언제 시작될건지 모르거나 많은 블럭이 얼마동안 시간을 잡아먹으면서 동작하는지 몰라도된다. 이것이 GCD의 모든것이다.

아래 그래프는 GCD에서 4개의 동시수행을 동작하는 테스트 샘플이다.


Block 1, 2, 3 모두가 어떻게하면 빨리 연이어서 실행될지 주목하라, Block0이 시작된 후 블럭1이 시작되는 동안 일어날 것이다. 또한 Block3은 Block2 다음에 시작되지만 Block2보다 빨리 끝난다.

block이 언제 시작되는지의 결정은 완전히 GCD에서 한다. 만약 block의 실행 시간이 다른것과 겹칠때, 다른 코어에서 실행시킬지, 한개만 사용할것인지 혹은 코드를 다른 block에 콘텍스 스위칭을 할지 정하는건 GCD가 하기 나름이다.

단지 흥미로운 점은 GCD가 각자 다른 queue 타입으로부터 정해서 적어도 5가지의 각 queue들을 제공한다는 점이다.


Queue Types

처음으로, 이 시스템은 당신이 main queue라고 알고있는 특별한 종류의 큐를 제공한다. 다른 queue들과 같이, 이 큐에서도 한번에 테스크를 수행한다. 그러나 이것은 모든 테스크를 메인 스레드에서 수행할 것이라는 뜻이며, 당신의 UI를 유일하게 업데이트 할 수 있는 스레드이기도하다. 이 queue는 UIView에 메세지를 보내거나 노티피케이션을 보내는 것에 사용되어야하는 유일한 queue이다.

시스템은 또한 여러 동시 queues를 제공한다. 이것은 우리가 알고있는 Global Dispatch Queues라고 불리는 놈이다. 이것은 다른 우선순위를 가진 4개의 global queue가 있다: backgroundlowdefault, and high

마지막으로, 당신은 커스텀화된 종류나 동시 queues 또한 생성할 수 있다. 이 말은 당신이 적어도 5개의 queue들을 마음대로 생성 소멸시킬 수 있다는 뜻이다: main queue, 4개의 global dispatch queues, 추가로 당신이 커스텀화시켜 만든 queues 까지!

그리고 dispatch queue들의 큰 그림이 있다!

GCD의 예술적인 면은 queue에 당신의 일을 적당한 queue dispatching function을 골라 보내는 역할을 한다는 것이다. 이것을 경험하기 가장 좋은 방법은 권장해놓은 길을 따라서 예제를 실행해보는 것이다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,


-(void) setMaskTo:(UIView*)view byRoundingCorners:(UIRectCorner)corners cornerRadius:(CGFloat)cornerRadius{
    UIBezierPath* rounded = [UIBezierPath bezierPathWithRoundedRect:view.bounds
                                                  byRoundingCorners:corners
                                                        cornerRadii:CGSizeMake(cornerRadius, cornerRadius)];
    
    CAShapeLayer* shape = [[CAShapeLayer alloc] init];
    [shape setPath:rounded.CGPath];
    view.layer.mask = shape;
}


view.frame = CGRectMake...하고나서
[self setMaskTo:view byRoundingCorners:...];
을 호출해주어야지 정상적으로 라운딩된 뷰를 볼 수 있다. 


WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

1. 일단 뷰를 생성해 둔다.

// view 생성
UIView *view = [[UIView alloc] init];
view.backgroundColor = [UIColor colorWithWhite:0.f alpha:1.f];
view.frame = CGRectMake(100, 100, 100, 100);
[self.view addSubview:view];

x, y : 100, 100
w, h : 100, 100
배경색 : 검정 


2. 테두리

// 테두리
view.layer.borderColor = [UIColor colorWithRed:1.f green:0.f blue:0.f alpha:1.f].CGColor;
view.layer.borderWidth = 1.f;

빨간색의 너비가 1인 테두리를 만든다


3. 둥근 모서리

// 둥근 모서리
view.layer.cornerRadius = 10.f;

 

아래와 같이 둥근 모서리를 이용한 원 형태의 뷰도 만들 수 있다.

UIImageView *iv = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"profile.jpg"]];
iv.frame = CGRectMake(100, 100, 100, 100);
[self.view addSubview:iv];


iv.clipsToBounds = YES;
iv.layer.cornerRadius = iv.frame.size.width/2.f;



4. 스케일 변경 및 애니메이션

// 2배로 크게
view.transform = CGAffineTransformMakeScale(2.0, 2.0);

각 너비와 높이의 스케일 값을 넣는다
애니메이션 효과를 원한다면 아래와 같이 하면 된다.

// 애니메이션
[UIView animateWithDuration:3.f animations:^{
    view.transform = CGAffineTransformMakeScale(2.0, 2.0);
}];



5. 각도 변경 및 애니메이션

// 180도 회전
view.transform = CGAffineTransformMakeRotation(M_PI);

M_PI는 미리 정의된 3.141592... 파이값이다. 저 값만 바꿔주면 되며 
애니메이션 효과를 원한다면 아래와 같이 하면 된다.

// 애니메이션
[UIView animateWithDuration:3.f animations:^{
    view.transform = CGAffineTransformMakeRotation(M_PI);
}];





CALayer

The CALayer class manages image-based content and allows you to perform animations on that content. Layers are often used to provide the backing store for views but can also be used without a view to display content. A layer’s main job is to manage the visual content that you provide but the layer itself has visual attributes that can be set, such as a background color, border, and shadow. In addition to managing visual content, the layer also maintains information about the geometry of its content (such as its position, size, and transform) that is used to present that content onscreen. Modifying the properties of the layer is how you initiate animations on the layer’s content or geometry. A layer object encapsulates the duration and pacing of a layer and its animations by adopting the CAMediaTiming protocol, which defines the layer’s timing information.

CALayer 클래스는 컨텐츠를 기반으로한 이미지를 관리하거나 컨텐츠의 애니메이션 기능을 수행하는 역활을 한다. 레이어는 보통 뷰들의 역할을 보완하는데 사용되기도 하지만 뷰 없이도 콘텐츠를 나타내는데 사용될 수 있다. 레이어의 주된 기능은 당신이 제공한 콘텐츠를 보여주는 역할을 관리하는 것이다. 그러나 레이어 그 자신이 배경색이나 테두리, 그림자와 같은 시각적인 속성을 가지고 있다. 추가적으로 시각적으로 콘텐츠를 관리하기 위해, 레이어는 화면에 보여지고 있는 기하학적인 정보들(좌표, 크기, transform등등)을 가지고있다. 레이어의 속성을 변화시키는 것은 콘텐츠의 레이어나 기하학적으로 애니메이션을 시작할 수 잇는 방법이다. 레이어 오브젝트는 레이어의 지속시간이나 페이스를 담고있고 이것은 CAMediaTiming를 적용시킴으로써 레이어에 정의된 시간적인 정보에 따라 애니메이션 시킬 수 있다.




If the layer object was created by a view, the view typically assigns itself as the layer’s delegate automatically, and you should not change that relationship. For layers you create yourself, you can assign a delegate object and use that object to provide the contents of the layer dynamically and perform other tasks. A layer may also have a layout manager object (assigned to the layoutManager property) to manage the layout of subviews separately. 

만약 뷰에 의해 레이어 오브젝트가 생성되었다면, 뷰는 일반적으로 레이어 델리게이트에 자동으로 지정되있을 것이고 당신은 이 지정을 바꿀 수 없다. 당신이 직접 만든 레이어의 한해서, delegate 오브젝트에 어싸인 할 수 있고 그 델리게이트 오브젝트는 콘텐츠 레이어의 동적인 행동이나 다른 일들을 제공하는 것들을 사용할 수 있다. 레이어는 또한 차별적으로 subview들의 레이아웃을 관리하기 위한 (
layoutManager 속성에 어싸인된) 레이아웃 메니저 오브젝을 가진다.







WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

1. 블럭 코딩
나는 애니메이션 효과를 쓸 때는 블럭코딩을 주로 사용하는 편인데, 보기도 편하고 사용하기 좋다.
사용법은 아래와 같이 사용하면 된다.

[UIView animateWithDuration:3.f animations:^{
    // 애니메이션 ..
}];


2. CGRect 변경 (좌표 및 사이즈) 애니메이션

view.frame = CGRectMake(0, 0, 100, 100);

[UIView animateWithDuration:3.f animations:^{
    view.frame = CGRectMake(100, 100, 50, 50);
}];



3. 투명도 변경 애니메이션

view.frame = CGRectMake(100, 100, 100, 100);

[UIView animateWithDuration:3.f animations:^{
    view.alpha = 0;
}];







WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,



구글 애널리틱스가 뭔지부터 설명하겠다. 이놈은 구글에서 만든 "서비스 분석기”로 웹이나 앱에서 사용자가 서비스(웹이나 앱등을)를 어떻게 실행했는지 얼마나 실행했는지를 분석해서 통계를 내주고 도식화해주는 그런 놈이다. 그러니까 쉽게 예를 들어보면 '하루에 몇명이 접속했는지’, ‘실시간으로 몇명이 접속해있는지’ 이런 정보들을 제공한다. 또한 이런 방대한 기능에 비해서 앱에 적용해 사용하기가 쉽다! (아이고 감사합니다) 한번 구글 애널리틱스를 사용해본 개발자라면 다음부턴 항상 사용하게 될 것을 장담한다.

좀 더 자세한 설명을 보고싶다면 구글애널리틱스 자세히 알아보기 여기 들어가면 구글에서 자세히 설명해놓았다.

이번 포스팅에서는 iOS앱에서 간단히 적용시키는 것에 초점을 맞춰서 글을 써내려갈 생각이다.



구글 애널리틱스 사이트에 내 앱 등록하기

1. 구글 아이디 만든다 (웬만하면 있지요들? 근데 개발자용 하나 따로 만들어 놓으면 편합니다)

2. 구글 애널리틱스 홈페이지가서 가서 구글 애널리틱스 가입을 합니다. (구글 계정이 필요) 오른쪽 상단에 계정만들기 눌러서 진행하면 됨

3. 서비스 등록
- 처음 가입했다면 바로 아래와 같은 창이 뜰것이고

- 이미 가입한 상태라면 

상단에 “관리”라는 탭이 있다. 그걸 눌러서 

“새 속성 만들기”를 누르면 새로운 서비스를 등록할 수 있다.

4. 이것저것 작성하고나서 "추적 ID가져오기” 버튼을 누르면 추적 아이디가 생긴다.
예) DB-271282139-1 이런 모양으로 생겼다. 기억해두고 나중에 코드에 삽입해야한다.


구글 애널리틱스 SDK를 앱에 심기
1. 구글 애널리틱스 SDK를 다운받는다. 다운로드 링크
zip파일로 되있고 3.10버전(14년11월15일 기준)이라고 되있다.


2. 프로젝트에 추가해야할 .h파일들과 .a파일이다.
libGoogleAnalyticsServices.a
그 외 .h파일들을 집어 넣는다.


이렇게 GoogleAnalytics폴더 하나 만들어서 필요한 파일들 넣어주면 된다.

3. 이제 필요한 프레임 워크를 넣을거다

아이고 친절하다

  • CoreData.framework
  • libAdIdAccess.a
  • AdSupport.framework
  • SystemConfiguration.framework
  • libz.dylib
  • libsqlite3.dylib


환경 세팅은 끝났다.
코드 작성하러 가자

4. 코드 작성

AppDelegate.m에 임포트 시킨다.

#import "GAI.h"
#import "GAIDictionaryBuilder.h"
#import "GAIFields.h"

AppDelegate의 application:didFinishLaunchWithOptions:메서드에 다음 코드를 추가한다.
(그냥 초기화, 초기세팅 정도의 기능을 하지만 사실 없어도 동작하더라..)

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Optional: automatically send uncaught exceptions to Google Analytics.
    [GAI sharedInstance].trackUncaughtExceptions = YES;
    // Optional: set Google Analytics dispatch interval to e.g. 20 seconds.
    [GAI sharedInstance].dispatchInterval = 5;
    // Optional: set Logger to VERBOSE for debug information.
    [[[GAI sharedInstance] logger] setLogLevel:kGAILogLevelVerbose];
    // Initialize tracker.

    return YES;
}


그리고 이제 실제로 서버에 Hit을 날려줄 코드를 작성한다.
나같은 경우 ViewController.m에 viewDidLoad에다가 넣었다.

#define GOOGLE_ANALYTICS_KEY @"DB-271282139-1"

아까 내가 기억해놓으라고 했던 추적ID를 이제 쓸 차례가 왔다.

- (void) viewDidLoad {
    [super viewDidLoad];

    id<GAITracker> tracker = [[GAI sharedInstance] trackerWithTrackingId:GOOGLE_ANALYTICS_KEY];
    [tracker set:kGAIScreenName value:@"테스트화면1"];
    [tracker send:[[GAIDictionaryBuilder createAppView] build]];
}


마무리
이렇게 하면 이제 앱이 켜지고 저 화면이 켜질때마다 알아서 구글 애널리틱스에 hit을 시킨다. 위치정보나 시간정도 이런거 주서다가 보내주는 모양이다. 나중에 구글 애널리틱스 홈페이지 들어가서 통계자료를 보면 되고,
실제 앱이 잘 연동되었는지 확인을 하려면 앱을 실행시켜놓고 구글애널리틱스 홈페이지 들어가서 전체 모바일 데이터 (All Mobile Data)를 눌러서 실시간 왼쪽탭의 메뉴를 눌러보면 1명이 활성화 된것을 확인 할 수있다. 오오 신기하다.

왜 구글 애널리틱스인가?
일단 수많은 이유가 있겠지만
첫째 공짜이고 
둘째 공짜이고 
셋째 공짜이고 
넷째 연동하기 너무 쉽고 
다섯째 통계를 다양한 관점에서 잘 분석해 놓았다.(앱이 강제 종료 되는것도 알아서 통계가 나옴)

그리고 최종적인 목적은 내 서비스를 다음에 어떤식으로 개발해야할지 업데이트 해야할지 방향이 서기 때문이다.




아무튼 글을 마치겠습니다. 오류 지적이나 여러가지 의견은 댓글로 달아주시면 감사하겠습니다.


WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

준비할것

카카오톡 API 홈페이지 들어가서 준비를 한다.
- 나머지 프레임 워크도 추가한다.

#import <MessageUI/MessageUI.h>
#import <KakaoOpenSDK/KakaoOpenSDK.h>
#import <Social/Social.h>
#import <Accounts/Accounts.h>


MFMessageComposeViewControllerDelegate 델리게이트를 등록한다. 메시지 전송 팝업을 컨트롤 할때 사용한다.

@interface ViewController () <MFMessageComposeViewControllerDelegate> {

}
@end


아래 코드는 실제 동작하는 코드

- (void) shareWithIndex:(NSInteger)buttonIndex text:(NSString *)text image:(UIImage *)image imageURLString:(NSString *)imageurl/*카톡 이미지 공유에서 쓰임*/ url:(NSURL *)url { if (buttonIndex==0) { // 문자 메세지 [self shareMessageWithText:text image:image url:url]; } else if (buttonIndex==1) { // 카카오톡 if ([KOAppCall canOpenKakaoTalkAppLink]) { // 카카오톡 공유 [self kakaoWithText:text image:image imageURLString:imageurl url:url]; } else { // 카카오톡 설치 [self openInstallKakaoAlert]; } } else if (buttonIndex==2) { // 페이스북 [self shareWithServiceType:SLServiceTypeFacebook Text:text image:image url:url]; } else if (buttonIndex==3) { // 트위터 [self shareWithServiceType:SLServiceTypeTwitter Text:text image:image url:url]; } } #pragma mark - 메시지 - (void) shareMessageWithText:(NSString *)text image:(UIImage *)image url:(NSURL *)url { if(![MFMessageComposeViewController canSendText]) { UIAlertView *warningAlert = [[UIAlertView alloc] initWithTitle:@"메시지 보내기 기능을 지원하지 않습니다." message:@" " delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil]; [warningAlert show]; return; } else { MFMessageComposeViewController *controller = [[MFMessageComposeViewController alloc] init]; if([MFMessageComposeViewController canSendText]) { controller.body = [NSString stringWithFormat:@"%@\n\n%@", text, url]; // controller.recipients = recipients; controller.messageComposeDelegate = self; NSData *data = UIImageJPEGRepresentation(image, 0); [controller addAttachmentData:data typeIdentifier:@"image/jpg" filename:@"thumbnail"]; [self presentViewController:controller animated:YES completion:nil]; } } } #pragma mark - 메시지 전송 delegate - (void)messageComposeViewController:(MFMessageComposeViewController *)controller didFinishWithResult:(MessageComposeResult)result { [controller dismissViewControllerAnimated:YES completion:nil]; } #pragma mark - 카카오톡 - (void) kakaoWithText:(NSString *)text image:(UIImage *)image imageURLString:(NSString *)imageurl url:(NSURL *)url { // 카카오톡 KakaoTalkLinkAction *androidAppAction = [KakaoTalkLinkAction createAppAction:KakaoTalkLinkActionOSPlatformAndroid devicetype:KakaoTalkLinkActionDeviceTypePhone marketparam:nil execparam:@{@"kakaoFromData":[NSString stringWithFormat:@"{seq:\"%@\", type:\"%@\"}", self.dataInfo[@"contentsSeq"], self.dataInfo[@"contentsType"]]}]; KakaoTalkLinkAction *iphoneAppAction = [KakaoTalkLinkAction createAppAction:KakaoTalkLinkActionOSPlatformIOS devicetype:KakaoTalkLinkActionDeviceTypePhone marketparam:nil execparam:@{@"kakaoFromData":[NSString stringWithFormat:@"{seq:\"%@\", type:\"%@\"}", self.dataInfo[@"contentsSeq"], self.dataInfo[@"contentsType"]]}]; NSString *buttonTitle = @"앱으로 이동"; NSMutableArray *linkArray = [NSMutableArray array]; KakaoTalkLinkObject *button = [KakaoTalkLinkObject createAppButton:buttonTitle actions:@[androidAppAction, iphoneAppAction]]; [linkArray addObject:button]; /*[NSString stringWithFormat:@"%@ (%@)",[[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleDisplayName"], LOC(@"msg_invite_kakao", @"경영전문대학원 MBA 모바일 주소록 앱")]*/ if (text) { KakaoTalkLinkObject *label; label = [KakaoTalkLinkObject createLabel:text]; [linkArray addObject:text]; } if (imageurl && image) { KakaoTalkLinkObject *kimage = [KakaoTalkLinkObject createImage:imageurl/*self.dataInfo[@"thumbnail1"]*/ width:image.size.width height:image.size.height]; [linkArray addObject:kimage]; } [KOAppCall openKakaoTalkAppLink:linkArray]; } - (void) openInstallKakaoAlert { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"카카오톡이 설치되어 있지 않습니다." message:@"카카오톡을 설치하겠습니까?"// @"Do you want to install the KakaoTalk?" delegate:self cancelButtonTitle:@"취소" otherButtonTitles:@"확인", nil]; alert.tag = 141; [alert show]; } #pragma mark - Alert View Delegate - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex { if (alertView.tag==141) { if (buttonIndex==[alertView cancelButtonIndex]) { // cancel } else { // 카카오톡 링크로 이동 [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"https://itunes.apple.com/kr/app/id362057947"]]; } } } #pragma mark - 페이스북 트위터 - (void) shareWithServiceType:(NSString *)serviceType Text:(NSString *)text image:(UIImage *)image url:(NSURL *)url { if ([SLComposeViewController isAvailableForServiceType:serviceType]) { SLComposeViewController *mySLComposerSheet = [SLComposeViewController composeViewControllerForServiceType:serviceType]; if (text) [mySLComposerSheet setInitialText:text]; if (image) [mySLComposerSheet addImage:image]; if (url) [mySLComposerSheet addURL:url]; [mySLComposerSheet setCompletionHandler:^(SLComposeViewControllerResult result) { switch (result) { case SLComposeViewControllerResultCancelled: NSLog(@"Post Canceled"); break; case SLComposeViewControllerResultDone: NSLog(@"Post Sucessful"); break; default: break; } }]; [self presentViewController:mySLComposerSheet animated:YES completion:nil]; } else { [[[UIAlertView alloc] initWithTitle:@"실패" message:@" " delegate:nil cancelButtonTitle:@"확인" otherButtonTitles:nil] show]; } }


페이스북, 트윗 등등 다른 기본 소셜 공유기능을 사용하려면

SOCIAL_EXTERN NSString *const SLServiceTypeTwitter NS_AVAILABLE(10_8, 6_0);
SOCIAL_EXTERN NSString *const SLServiceTypeFacebook NS_AVAILABLE(10_8, 6_0);
SOCIAL_EXTERN NSString *const SLServiceTypeSinaWeibo NS_AVAILABLE(10_8, 6_0);
SOCIAL_EXTERN NSString *const SLServiceTypeTencentWeibo NS_AVAILABLE(10_9, 7_0);
SOCIAL_EXTERN NSString *const SLServiceTypeLinkedIn NS_AVAILABLE(10_9, NA);

상수 스트링을 사용하면 된다. 이 포스팅에서는 SLServiceTypeFacebook, SLServiceTypeTwitter 만 사용했다.


WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

키보드 올라오는 Delegate받기

- (void)viewDidAppear:(BOOL)animated {
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    [center addObserver:self selector:@selector(keyboardOnScreen:) name:UIKeyboardWillShowNotification object:nil];
    [center addObserver:self selector:@selector(keyboardHideScreen:) name:UIKeyboardWillHideNotification object:nil];
}
- (void)viewDidDisappear:(BOOL)animated {
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    [center removeObserver:self name:UIKeyboardWillShowNotification object:nil];
    [center removeObserver:self name:UIKeyboardWillHideNotification object:nil];
}

#pragma mark - 키보드
-(void)keyboardOnScreen:(NSNotification *)notification {
    //...
}
-(void)keyboardHideScreen:(NSNotification *)notification {
    //...
}



- (void) viewDidAppear:(BOOL)animated ; 는 화면이 나타나기 바로직전에 호출되는 함수이고

- (void) viewDidDisappear:(BOOL)animated ; 는 화면이 없어지기 바로 직전에 호출되는 함수이다.

viewDidAppear에 "키보드 올라오면 말해주세요" 용도의 노티피케이션을 설정하고

viewDidDisappear에 "아까 등록한 노티피케이션을 지워주세요"라고 설정한다.




#pragma mark - 키보드
-(void)keyboardOnScreen:(NSNotification *)notification {
    NSDictionary *info  = notification.userInfo;
    NSValue      *value = info[UIKeyboardFrameEndUserInfoKey];
    
    CGRect rawFrame      = [value CGRectValue];
    CGRect keyboardFrame = [self.view convertRect:rawFrame fromView:nil]; /*키보드 프레임*/

    NSDictionary *userInfo = notification.userInfo;
    NSNumber *durationValue = userInfo[UIKeyboardAnimationDurationUserInfoKey];  /*키보드가 올라오는 동안의 시간*/
    NSTimeInterval animationDuration = durationValue.doubleValue;
    NSNumber *curveValue = userInfo[UIKeyboardAnimationCurveUserInfoKey];       /*키보드 애니메이션 옵션 효과 (ease)*/
    UIViewAnimationCurve animationCurve = curveValue.intValue;
    
    [UIView animateWithDuration:animationDuration delay:0.f options:animationCurve<<16 animations:^{
        // ...
    } completion:nil];
}

-(void)keyboardHideScreen:(NSNotification *)notification {
    NSDictionary *userInfo = notification.userInfo;
    NSNumber *durationValue = userInfo[UIKeyboardAnimationDurationUserInfoKey];
    NSTimeInterval animationDuration = durationValue.doubleValue;
    NSNumber *curveValue = userInfo[UIKeyboardAnimationCurveUserInfoKey];
    UIViewAnimationCurve animationCurve = curveValue.intValue;


    
    [UIView animateWithDuration:animationDuration delay:0.f options:animationCurve<<16 animations:^{
        // ...
    } completion:nil];
}


// ... 부분에 애니메이션 코드를 넣어주면 키보드에서와 동일한 애니메이션 ease가 연출된다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

이미지를 보여주는 앱을 만들다가 UIImageView는 GIF를 지원하지 않는걸 깨달았다. 여러가지 오픈소스를 사용해봤지만 FLAnimatedImage 이게 제일 맘에 든다. 빠르고 빠르고 빠르고 빠르다. 메모리도 적게 먹는다. 심지어 가장 최근에 만들어진듯하다. 만세다.

Github : FLAnimatedImage


사용법

FLAnimatedImage, FLAnimatedImageView파일 두개 프로젝트에 추가하고나서 아래 코드처럼 사용하면 된다.

#import "FLAnimatedImage.h"
#import "FLAnimatedImageView.h"


    FLAnimatedImage * /*__block*/ animatedImage2 = nil;
    NSURL *url2 = [NSURL URLWithString:@"http://raphaelschaad.com/static/nyan.gif"];
    NSData *data2 = [NSData dataWithContentsOfURL:url2];
    animatedImage2 = [[FLAnimatedImage alloc] initWithAnimatedGIFData:data2];
    
    FLAnimatedImageView *gifImageView = [[FLAnimatedImageView alloc] init];
    [gifImageView performSelectorOnMainThread:@selector(setAnimatedImage:) withObject:animatedImage2 waitUntilDone:NO];






WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,
// NSDate -> NSStirng
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];

NSDate *now = [[NSDate alloc] init];
NSString *nowText = [dateFormatter stringFromDate:now];          // 2014-08-28 04:12:21



// NSStirng -> NSDate
NSString *dateString = @"2014-08-28";
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"yyyy-MM-dd"]; 

NSDate *dateFromString = [dateFormatter stringFromDate:dateString];     // NSDate로 바뀜..

WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

계산기 만들때 썼던거

int num = 300000;
NSString *numberString = [NSNumberFormatter localizedStringFromNumber:@(num) numberStyle:NSNumberFormatterDecimalStyle];
// numberString : 300,000

.. 작년에 알았더라면 귀찮게 쉼표 달아주는 함수를 따로 만들 필요도 없었을텐데.....ㅜㅜ 


WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

※ 번역 작업 처음 해봅니다.

※ 발번역 죄송합니다.. 틀린점 댓글로 달아주시면 수정하겠습니다.

※ 블로그에 글 올리는 것도 처음 해봅니다. 눈에 거슬리는 점이 있어도 너그럽게 이해해주시길 바라겠습니다.




원문 : Streaming Audio to Multiple Listeners via iOS' Multipeer Connectivity

 Tony DiPasquale  November 20, 2013 

Translate by canapio




음악은 아이폰 혹은 모든 애플 제품에서 굉장히 중요한 부분이다. iOS7이 출현하면서, 애플은 NSOutputStreamNSInputStream를 이용하여 데이터 스트림에 접근이 가능한 "Multipeer Connectivity"이라는 새로운 기술을 선보였다. 그러나, NSOutputStream를 이용해 재생하는 것은 쉬운일이 아니었고, 나는 CoreAudio를 이용해 사용할 수 있게 만드는 모험을 시작했다.

개요
Multipeer Connectivity는 NSOutputStream를 이용하여 연결된 요소(connected peer)에 데이터를 스트림한다. 이것은 오디오 데이터를 전송하는데 사용할 것이다. 전송이 끝나고, Multipeer Connectivity는 우리가 incoming data를 얻는데 사용할 NSInputStream 를 쓴다. 애플에서 제공한 Audio Queue Services을 사용하여, 이 데이터를 디바이스 시스템에 보낼 것이다. Audio Queue Services는 버퍼를 치우고 재생까지 할 수 있게 해준다. 이것은 오디오 데이터 열(audio data raw)을 재생시킬 수 있지만, MP3나 AAC와 같은 대부분의 오디오 파일은 크기를 줄이기 위해 인코딩작업이 되어 있다. 애플은 인코딩된 오디오 포맷을 처리하고 오디오 데이터 열을 반환해주는 Audio File Stream Services를 제공했다. 아래 그림은 데이터의 플로우이면서 계획된 솔루션이 실행되기 직전의 모습이다.



첫째로, 오디오 스트림이 시작되고 데이터를 받으면, 디코딩을 해주는 스트림 파서에 넣는다. 이 파서는 우리가 필요로하는 오디오 데이터 열(audio data raw)를 보내준다. 파서로부터 하나씩 받은 세개의 오디오 버퍼 데이터가 오디오 큐(audio queue)안에 있다. 버퍼가 다 차면 시스템으로 보내진다. 그리고 시스템에서 소리를 다 냈으면 다시 돌아와 다시 채워지고, 소리내고 비우고 채우는 과정을 더이상 플레이할 것이 없을 때까지 반복한다. 아래 GIF는 시스템 하드웨어에서 오디오 데이터가 코드에서 어떻게 흘러가는지 보여주는 애니메이션이다. 빨간 박스는 빈 버퍼, 초록 박스는 가득찬 버퍼를 의미한다.




오디오 데이터 보내기
이제 우리는 스티리밍이 백그라운드에서 어떻게 동작하는지 좀 안다. 아이튠즈 라이브러리에서 노래 한곡을 재생해보자. MPMediaPickerController을 사용하여 유저는 노래를 고를 수 있다. 우리는 피커컨트롤러의 델리게이트 메소드(mediaPicker:didPickMediaItems:)를 이용하여 MPMediaItem들을 담은 배열을 얻을 것이다. 
MPMediaItem는 곡의 타이틀, 작사, 작곡 등.. 수많은 프로퍼티를 가지고 있지만, MPMediaItemPropertyAssetURL 프로퍼티에 초점을 둘것이다. AVAssetReader 와 AVAssetReaderTrackOutput 을 사용해서 데이터 파일의 위치를 알아낸 다음 저것(MPMediaItemPropertyAssetURL)을 사용하여 AVURLAsset 를 만들어낸다. 


NSURL *url = [myMediaItem valueForProperty:MPMediaItemPropertyAssetURL];
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:nil];
AVAssetReader *assetReader = [AVAssetReader assetReaderWithAsset:asset error:nil];
AVAssetReaderTrackOutput *assetOutput = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:asset.tracks[0] outputSettings:nil];
[self.assetReader addOutput:self.assetOutput];
[self.assetReader startReading];
이제, 미디어 아이템으로부터 AVURLAsset를 뽑아냈다. 우리는 이걸 사용해서 AVAssetReader 와 AVAssetReaderTrackOutput를 만들것이다. 마지막으로 우리는 읽'어주는 놈(reader)'한테 output을 던저주고 읽게 할 것이다. startReading 이라는 메소드는 읽어주는 놈(reader)을 열고 데이터를 요청했을때를 위해 준비하는 일밖에 하지 않는다.

다음으로 NSOutputStream 을 열고 해당 델리게이트 메서드는 NSStreamEventHasSpaceAvailable 이벤트가 호출 될때까지 읽어주는 놈(reader)의 데이터를 보냅니다.


CMSampleBufferRef sampleBuffer = [assetOutput copyNextSampleBuffer];

CMBlockBufferRef blockBuffer;
AudioBufferList audioBufferList;

CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(sampleBuffer, NULL, &audioBufferList, sizeof(AudioBufferList), NULL, NULL, kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment, &blockBuffer);

for (NSUInteger i = 0; i < audioBufferList.mNumberBuffers; i++) {
    AudioBuffer audioBuffer = audioBufferList.mBuffers[i];  
    [audioStream writeData:audioBuffer.mData maxLength:audioBuffer.mDataByteSize];
}

CFRelease(blockBuffer);
CFRelease(sampleBuffer);
먼저, 리더 아웃풋에서 나온 셈플 버퍼를 가져온다. 그리고나서 오디오 버퍼의 리스트를 얻기 위해 CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer 함수를 호출한다. 마지막으로 아웃풋 스트림의 각 오디오 버퍼를 쓴다(write).

이것이 우리는 처음에 하고자 했던 아이튠즈 라이브러리에 있는 음악을 스트리밍 한 것이다.(재생한건 아님) 이제 이 스트림 데이터를 어떻게 받아내는지 그리고 오디오를 어떻게 재생하는지 보자.


데이터 스트림
Multipeer Connectivity를 사용할 때 부터 NSInputStream 는 이미 만들어져 있었다. 먼저 우리는 데이터를 받기 위해 스트림을 해야한다.
// Start receiving data
// Start receiving data
inputStream.delegate = self;
[inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[inputStream open]; 
이 클래스는  NSStreamDelegate 를 씌울 것이고, 우리는 이제 NSInputStream 으로부터 이벤트를 받을 수 있다.
@interface MyCustomClass () <NSStreamDelegate[CDATA[]]>
//...
@end

@implementation MyCustomClass
//...

- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode
{
    if (eventCode == NSStreamEventHasBytesAvailable) {
        // handle incoming data
    } elseif (eventCode == NSStreamEventEndEncountered) {
        // notify application that stream has ended
    } elseif (eventCode == NSStreamEventErrorOccurred) {
        // notify application that stream has encountered and error
    }
}

//...
@end

위의 코드를 보면 델리게이트 메소드를 이용해서 스트림으로부터 이벤트를 받는다. 스트림이 끝나거나 에러가 나왔을 때, 앱에 알려야한다. 그래서 다음에 어떤 행동을 취할건지 정해야한다. 이제 우리는 이 이벤트로부터 얻은 스트림이 처리하여 가지고 있는 데이터에만 초점을 두면 된다. 우리는 이 데이터를 가져다가 사용하고, 그다음 Audio File Stream Services에 보내주는 작업이 필요하다.


스트림 파서
스트림 파서는 인코딩된 오디오 데이터를 넣고 디코딩된 오디오 데이터를 얻어오는 AudioFileStream 클래스이다. 먼저 AudioFileStream을 만들어보자.
AudioFileStreamID audioFileStreamID;
AudioFileStreamOpen((__bridge void *)self, AudioFileStreamPropertyListener, AudioFileStreamPacketsListener, 0, &audioFileStreamID);
우리는 클래스에 참조된 파서를 보내고, 프로퍼티는 콜백함수에 의해 바뀌고, 콜백함수에 의해 패킷들을 받는다. 우리는 이러한 기능을 하고 그 클래스에 참조되어 사용될 수 있는 콜백함수가 이 클래스 안에 필요하다.
void AudioFileStreamPropertyListener(void *inClientData, AudioFileStreamID inAudioFileStreamID, AudioFileStreamPropertyID inPropertyID, UInt32 *ioFlags)
{
    MyCustomClass *myClass = (__bridge MyCustomClass *)inClientData;
    [myClass didChangeProperty:inPropertyID flags:ioFlags];
}

void AudioFileStreamPacketsListener(void *inClientData, UInt32 inNumberBytes, UInt32 inNumberPackets, constvoid *inInputData, AudioStreamPacketDescription *inPacketDescriptions)
{
    MyCustomClass *myClass = (__bridge MyCustomClass *)inClientData;
    [myClass didReceivePackets:inInputData packetDescriptions:inPacketDescriptions numberOfPackets:inNumberPackets numberOfBytes:inNumberBytes];
}

didChangeProperty:flags: 메소드 안에서 다른 모든 프로퍼티가 준비됫다고 말해주는 kAudioFileStreamProperty_ReadyToProducePackets 프로퍼티를 찾고 있다. 이제 파서로부터 AudioStreamBasicDescription 를 가져올 수 있다. AudioStreamBasicDescription 는 오디오의 샘플비율(sample rate), 채널, 패킷당 바이트수 등등의 정보를 담겨있고 이것은 오디오 큐(audio queue)를 만드는데 꼭 필요한 요소이다.
AudioStreamBasicDescription basicDescription;
UInt32 basicDescriptionSize = sizeof(basicDescription);
AudioFileStreamGetProperty(audioFileStreamID, kAudioFileStreamProperty_DataFormat, &basicDescriptionSize, &basicDescription);
콜백으로 부터 받은 패킷에 사용될 다른 함수들은 나중에 오디오 큐 버퍼에 쌓일 디코딩된 오디오 데이터를 반환해줄것이다.

이제 스트림의 NSStreamEventHasBytesAvailable 이벤트로부터 인코딩된 데이터를 파서에 넣을 차례이다.
uint8_t bytes[512];
UInt32 length = [audioStream readData:bytes maxLength:512];
AudioFileStreamParseBytes(audioFileStreamID, length, data, 0);
파일 스트림은 파일의 타입을 알기게 충분한 바이트를 가질 때 까지 파싱을 할 것이다. At this point, it invokes its property changed callback with the property kAudioFileStreamProperty_ReadyToProducePackets. 그다음 이것은 우리가 사용하기 좋게 잘 포장된 디코딩된 데이터와 함께 해당 패킷이 받을 콜백을 호출한다.


오디오 큐
오디오 큐는 우리에게 오디오 버퍼를 생성하고, 채우고, 큐에 더할 수 있는것을 허락해주는 AudioQueue 클래스이다. 이것은 또한 재생, 일시정지, 멈춤등과 같은 오디오 컨트롤을 제공한다. 이제 큐와 버퍼를 생성해보자.
AudioQueueRef audioQueue;
AudioQueueNewOutput(&basicDescription, AudioQueueOutputCallback, (__bridge void *)self, NULL, NULL, 0, &audioQueue);

AudioQueueBufferRef audioQueueBuffer;
AudioQueueAllocateBuffer(audioQueue, 2048, &audioQueueBuffer);


오디오 큐를 만들기 위해서는 파서로부터 받은 theAudioStreamBasicDescription를 AudioQueueNewOutput 함수에 전달해 줘야하고, 시스템으로부터 호출된 콜백함수가 버퍼와 클래스 참조를 끝낸다. 다음으로 AudioQueueAllocateBuffer 함수를 호출하여 오디오 큐에 넘겨줄 수 있는 오디오 버퍼 한개를 만들고 잠시 멈추기 위한 버퍼의 사이즈도 함께 넘겨준다.


이제 파서가 패킷을 담은 콜백함수를 호출할 때까지 기다린다. 그리고 빈 버퍼를 패킷으로 채운다. 파서로부터 받을 수 있는 포멧은 VBR과 CBR 두가지 종류가 있는데, Variable Bitrate (VBR)는 비트율이 패킷이 어디있는지 따라 변할 수 있다는 것이고 Constant Bitrate (CBR)은 변하지 않는다(constant)는 것이다.

VBR의 경우,  많은 바이트를 가진 전체 패킷만을 버퍼에 채울 스 있다. 이것은 시스템에서 패킷을 보내주기 전까지는 버퍼가 차지 않는다는걸 의미한다. CBR의 경우, 패킷이 전송되는 도중에 버퍼를 가득 채울 수 있다.


CBR
AudioQueueBufferRef audioQueueBuffer = [self aFreeBuffer];
memcpy((char *)audioQueueBuffer->mAudioData, (constchar *)data,
또한 우리는 버퍼가 오버플로우가 되지 못하게 하거나 이것이 가득 차지 않았을 경우 기다리는 로직이 필요하다.

VBR
AudioQueueBufferRef audioQueueBuffer = [self aFreeBuffer];
memcpy((char *)audioQueueBuffer->mAudioData, (constchar *)(data + packetDescription.mStartOffset), packetDescription.mDataByteSize);

패킷이 버퍼에 넘처 남게되는 것을 체크하는 코드가 있다. 만약 다른 패킷의 mDataByteSize에 맞지 않는다면 우리는 다른 버퍼를 가져와야한다. 또한 패킷 디스크립션(packet descriptions)이 큐 되는동안 기다려야 한다.
바퍼가 차면, AudioQueueEnqueueBuffer 와 함께 시스템에 큐를 날린다.

AudioQueueEnqueueBuffer(audioQueue, audioQueueBuffer, numberOfPacketDescriptions, packetDescriptions);

이제 오디오를 재생할 준비가 끝났다. 모든 버퍼가 채워지고 큐 되면 AudioQueuePrime과 AudioQueueStart
를 사용하여 소리를 재생할 수 있다.
AudioQueueBufferRef audioQueueBuffer = [self aFreeBuffer];
memcpy((char *)audioQueueBuffer->mAudioData, (constchar *)(data + packetDescription.mStartOffset), packetDescription.mDataByteSize);

AudioQueueStart는 두번째 파라메터에 언제 재생될지에대한 시간을 나타내는 값을 NULL대신에 넣을 수 있다. 지금은 별로 중요하지 않으니 넘어가지만, 나중에 오디오 동기화(audio synchronization)을 하는데 꼭 필요한 것이니 기억해두면 좋다.

끝으로
이 글은 Multipeer Connectivity를 이용한 오디오 스트리밍에 대한 기초적인 글이다. 글을 마치면서 나는 조금더 복잡하고 잘 정리된 오픈소스 라이브러리를 민들었다. 좀더 자세한 내용을 알고싶으면, Github에 올라가있는 tonyd256/TDAudioStreamer 다듬어진 코드를 볼 수 있다.



 Tony DiPasquale  Developer

translate by canapio



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,