2014/04/18

[iOS] ZipArchive를 사용해서, Zip파일을 Unzip합니다.

iOS기기에서 zip으로 압축된 데이터를 다운 받고, 그것을 풀어야(Unzip)할 경우가 있습니다.
이 경우, ZipArchive를 이용해서 해결할 수 있습니다.

[ZipArchive는?]

오픈소스 코드인 MinZip을 기반으로 zip파일을 압축/해지를 iOS에서 사용할 수 있도록, Objective-C Class로 만들어 놓은 것입니다.

[어디서 구할 수 있나요?]

구글 코드사이트에서 나와 있습니다. (https://code.google.com/p/ziparchive/)
설명도 위 사이트에 자세히 나와 있습니다.


[사용하기 위해서]

1. 라이브러리를 등록합니다.
 프로젝트 설정에서 Framwork 등록하는 부분에서 아래와 같이 libz.dylib를 추가합니다.


2. ZipArchive 파일을 프로젝트에 추가합니다.
 위 사이트에서 download 받아서, 프로젝트에 추가합니다.


[ 압축파일을 풀려면 어떻게 해야 되나요?]

ZipArchive의 인스턴스를 사용해서, 파일을 열고, Unzip 하고, 파일을 닫으면 됩니다.
압축을 푸는 것이 리소스를 많이 잡아먹으니, 파일을 이용하는 것 같습니다.
source code
- (void)UnzipFile:(NSString*)zipFile to:(NSString *)destFile
{
    ZipArchive *zip = [[ZipArchive alloc]init];
    if ([zip unzipOpenFile:zipFile]) {
       BOOL ret = [zip unzipFileTo:destFile overwrite:YES];
       NSAssert(ret, @"unzipfile Error");
       if (![zip unzipCloseFile]) {
           NSLog(@"unzipCloseFile Failed");
       }
    }
}

감사합니다.







2014/04/15

[iOS] Segue 관련 함수

Storyboard의 Segue관련해서 몇가지 함수를 정리해 둡니다.

[Segue 선언]

- Xcode의 Storyboard에서 Ctrl+Drag를 통해서, 어디에서 어디로 이동할 것인지 Segue를 정의하고, 그 Identifier를 설정해 둡니다.

[Segue 실행]

- Storyboard에서 버튼이나 테이블뷰셀에서 호출되도록 할 수도 있고, ViewController에서 identifier로 호출할 수 있도록 설정할 수 있습니다.
- 버튼에서 Ctrl+Drag로 ViewController로 이동하면, 버튼의 Triggere Segues를 설정할 수 있습니다. 이렇게 되면, 그 버튼이 터치되면, 설정한 Segue를 실행하게 됩니다.
- ViewController에서 Ctrl+Drag를 해서 다른 ViewController로 이동해서 설정하고 id를 설정해 두면, id를 가지고 segue를 호출 할 수 있게 됩니다.
  실행함수는 performSegueWithIdentifier:sender: 함수로 id를 가지고 호출 할 수 있게 됩ㄴ디ㅏ.

[Segue 호출]

Segue가 호출되어 시작될 때, 시작됨을 알리는  -prepareForSegue:sender: 함수가 호출이되고, 이 함수에서 이동할 destinationViewController와 sourceViewController를 알 수 있고, 호출 ID를 통해서, 어떤 Segue인지를 알아서 구분할 수 있게 됩니다.
여기에서 ID에 따라서, destinationViewController에 설정을 추가해서 데이터를 전달 할 수 있게 됩니다.
- destinationViewController가 예상하는 특정 ViewController가 맞는지 확인을 위해서는 NSObject에 정의되어 있는 isKindOfClass: 함수를 이용하여 확인하는 것이 필요합니다.


[참고]
- Unwind Segue는? (블로그 : [iOS] Unwind Segue 사용하기)

2014/04/09

[iOS7] Dynamic : Attachment, Push, DynamicItem Behavior

Dynamics에 대한 기본적인 구성과 사용에 대해서는 이전 블로그에서 정리를 하였고, 이번은 Dynamics를 가지고, UI를 좀더 사용자 친화적으로 꾸밀 수 있을 지 알아보겠습니다.

모든 UIKit에 물리적 성질을 추가 할 수 있는 것이 iOS7의 Dynamics 입니다.
그것을 이용해서, 초기 화면의 UILabel 들에 Dynamics를 적용해 보도록 하겠습니다.
이전에는 UIView Animation을 이용해서 하던 것을 Dynamics를 활용해서 만드는 것입니다.

버튼 밑에 설명 Label을 붙이고, 이 Label들이 모션에 의해서 흔들리도록 해보겠습니다.
아래에 사용된 이미지는 구글 이미지에서 찾은 것들로, 저작권은 전혀 저에게 있지 않습니다.( 원본은 아래에 링크를 넣었습니다.)

위 이미지에서 Travel 라벨이 그 위의 아이콘의 center와 UIAttachmentBehavior로 연결을 하고, CoreMotion으로 X,Y 값의 변화에 따라서 좌/우로 흔들리도록 UIPushBehavior를 이용했습니다.

각 컨트롤에 대해서, Property 연결 합니다.
source code
@property (strong, nonatomic) IBOutlet UIImageView *travelImageView;
@property (strong, nonatomic) IBOutlet UIImageView *bookImageView;
@property (strong, nonatomic) IBOutlet UIImageView *workoutImageView;
@property (strong, nonatomic) IBOutlet UIImageView *foodImageView;

@property (strong, nonatomic) IBOutlet UILabel *travelLogLabel;
@property (strong, nonatomic) IBOutlet UILabel *bookLogLabel;
@property (strong, nonatomic) IBOutlet UILabel *foodLogLabel;
@property (strong, nonatomic) IBOutlet UILabel *workoutLogLabel;


@property (strong, nonatomic) UIDynamicAnimator *animator;
@property (strong, nonatomic) UIGravityBehavior *gravityBehavior;
@property (strong, nonatomic) UICollisionBehavior *collisionBehavior;
@property (strong, nonatomic) UIPushBehavior *pushBehavior;

그리고, UIDynamicsAnimator, UIGravityBehavior 로 설정합니다.
- (void) setupDynamics
{
    self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
    self.gravityBehavior = [[UIGravityBehavior alloc] initWithItems:@[self.travelLogLabel, self.bookLogLabel, self.foodLogLabel, self.workoutLogLabel]];    
    [self.animator addBehavior:self.gravityBehavior];
    ...
}

각 UILabel에 대해서 UIAttachmentBehavior를 연결하고, Damping과 Frequency를 설정합니다.

#pragma mark - Dynamics Setting
- (void) setupDynamics
{
    ...
    
    //Label들에 대해서, 마찰저항을 설정하고, 회전하지 않도록 설정
    UIDynamicItemBehavior *labelItemBehavior = [[UIDynamicItemBehavior alloc] initWithItems:@[self.travelLogLabel, self.bookLogLabel, self.foodLogLabel, self.workoutLogLabel]];
    labelItemBehavior.resistance = 1.0f;
    labelItemBehavior.allowsRotation = NO;
    [self.animator addBehavior:labelItemBehavior];

    // Travel Label에 대해서 AttchmentBehavior를 연결함.
    [self addAttachmentBehavior:self.travelLogLabel
                        atPoint:CGPointMake(self.travelImageView.center.x, self.travelImageView.center.y)
                     toAnimator:self.animator];
    
    // Book Label에 대해서 AttachmentBehavior를 연결
    [self addAttachmentBehavior:self.bookLogLabel
                        atPoint:CGPointMake(self.bookImageView.center.x, self.bookImageView.center.y )
                     toAnimator:self.animator];
    
    // Food Label에 대해서 AttachmentBehavior 연결
    [self addAttachmentBehavior:self.foodLogLabel
                        atPoint:CGPointMake(self.foodImageView.center.x, self.foodImageView.center.y )
                     toAnimator:self.animator];
    
    // Workout Label에 대해서 AttachmentBehavior 연결
    [self addAttachmentBehavior:self.workoutLogLabel
                        atPoint:CGPointMake(self.workoutImageView.center.x, self.workoutImageView.center.y )
                     toAnimator:self.animator];
    
    ....
}
- (void) addAttachmentBehavior:(UILabel *)label atPoint:(CGPoint)point toAnimator:(UIDynamicAnimator *)animator
{
    UIAttachmentBehavior *attachment = [[UIAttachmentBehavior alloc] initWithItem:label attachedToAnchor:point];
    attachment.damping = 1.0f;
    attachment.frequency = 100.0f;
    [animator addBehavior:attachment];
}

이 두 값을 설정하더라도 적절하게 멈추는 것이 아니라, 자주 흔들리게 됩니다.
아래의 왼쪽이 Attachment만 적용한 것이고, 오른쪽이 UIDynamicItemBehavior로 마찰저항을 추가한 것입니다.

 


이제, CoreMotion을 추가해서, 기기가 좌우로 흔들릴 때 마다, PushBehavior를 추가하여, Label이 흔들리도록 하겠습니다.

먼저 CoreMotion을 추가합니다.

@import CoreMotion;
...
- (void)viewDidLoad
{
    [super viewDidLoad];
	// Do any additional setup after loading the view, typically from a nib.
    
    _motionManager = [[CMMotionManager alloc] init];
    [self startMonitoringAcceleration];
}
...

#pragma mark - Dynamics Setting
- (void) setupDynamics
{
    ...
    
    self.pushBehavior = [[UIPushBehavior alloc] initWithItems:@[self.travelLogLabel, self.bookLogLabel, self.foodLogLabel, self.workoutLogLabel] mode:UIPushBehaviorModeInstantaneous]; //Instantaneous로 설정
    self.pushBehavior.active = NO;
    float angle = arc4random() % 360;
    self.pushBehavior.angle = (angle*M_PI/180.0);
    self.pushBehavior.magnitude = 0.4f;
    [self.animator addBehavior:self.pushBehavior];
}
#pragma mark - CoreMotion
- (void)startMonitoringAcceleration
{
    if (_motionManager.accelerometerAvailable) {
        //[_motionManager startAccelerometerUpdates];
        NSLog(@"accelerometer updates on...");
        [_motionManager setAccelerometerUpdateInterval:0.1f];
        [_motionManager startAccelerometerUpdatesToQueue:[NSOperationQueue mainQueue]
                                             withHandler:^(CMAccelerometerData *accelerometerData, NSError *error) {
                                                 float angle = atan2f(-accelerometerData.acceleration.y, accelerometerData.acceleration.x);
                                                 //NSLog(@"accelerometer:angle:%2.02f, x:%.02f, y:%.02f, z:%.02f", 180.0+angle*180.0/M_PI, accelerometerData.acceleration.x, accelerometerData.acceleration.y, accelerometerData.acceleration.z);
                                                 
                                                 NSLog(@"angle: %0.2f, %02.2f", angle, angle / M_PI * 180.0);
                                                 if (angle > 100*M_PI/180.0) {
                                                     angle = 100*M_PI/180.0;
                                                 }
                                                 if(angle < 80*M_PI/180.0){
                                                     angle = 80*M_PI/180.0;
                                                 }
                                                 self.gravityBehavior.angle = angle;// - 180.0 * M_PI / 180.0;
                                             }
         ];
    }else{
        NSLog(@"accelerometer Unavailable");
    }
}


- (void)stopMonitoringAcceleration
{
    if (_motionManager.accelerometerAvailable && _motionManager.accelerometerActive) {
        [_motionManager stopAccelerometerUpdates];
        NSLog(@"accelerometer updates off...");
    }
}
@end

위에서, accelerometerData의 acceleration의 x,y 좌표 값을 가지고, Angle을 구하고, 그것을 바탕으로 PushBehavior의 Active를 YES로 입력합니다.

실제 Device에서 실행을 하면, 회전을 살짝하면, Label 들이 움직이게 됩니다.


[이미지 출처]
배경화면, 비행기, , 음식, 운동

참고 :
 - WWDC 2013 : Getting started with UIKit Dynamics
      : #206 세션으로, 다이나믹스에 대한 설명이 있음.
 - UIKit Dynamics and iOS 7: Building UIKit Pong
      : 다이나믹스를 이용해서, 바운싱을 예제로 설명하고 있는 곳.
 - RayWenderlich의 UIKit Dynamics 강좌

















2014/04/06

[iOS7] UIKit Dynamics 사용법

UIKit Dynamics는 iOS7에서 추가된 것입니다.

iOS의 메시지에서 위로 올리다 보면, 마지막 부분에 튕기듯이 멈추는 부분과, Lock Screen에서 카메라 아이콘을 터치하거나, 위로 들었다가, 아래로 내리면 튕기 듯이 반응 하는 것이 UIKit의 Dynamics에서 지원하는 기능입니다.

 - UIKit Dynamics는 UIKit에 통합된 완전한 물리엔진 입니다. 즉, UIKit의 객체에 gravity, attachments, forces와 같은 동작(behavior)를 추가하여 현실처럼 느낄 수 있는 인터페이스를 만들 수 있게 지원하고 있습니다.
 - 물리특성을 정의하면, dynamics엔진이 알아서 처리를 해주고 있습니다.
 - iOS7 전에는 물리적인 시뮬레이션을 위해서, 다른 라이브러리를 사용해야 했었는데, iOS7에서는 간단한 물리적 시뮬레이션을 UIKit에서 해주는 것입니다.
 - 또한, CollectionView에서 아이콘들의 행동을 규정할 수 있습니다. 뭔가 추가되면, 다른 것들이 옆으로 흔들리게 만들 수 있습니다.

버튼들이 화면에 있다가 특정 순간이 되면, 아래로 떨어지도록 할 수도 있습니다.
이것을 가능하게 하는 것이 iOS7의 UIKit Dynamics입니다.


Dynamics는 UIKit내부에 내장되어 있으므로, 별도의 Framework를 import 할 필요가 없습니다.
위에 그림에서 처럼 Button 두개와, TextField를 움직이도록 해 보겠습니다.
먼저 Single View Application을 만들고, storyboad에서 위와 같이 객체를 등록하고, 각각 Property를 설정합니다.

UIDyanmicsAnimator를 현재 Main으로 등록된 View를 레퍼런스로 만듭니다.
Source Code

@interface DBUViewController ()

//아래로 떨어질 TextField
@property (strong, nonatomic) IBOutlet UITextField *textField; 
//아래로 떨어질 Button
@property (strong, nonatomic) IBOutlet UIButton *fallingButton; 
//스프링처럼 매달려 있을 Button
@property (strong, nonatomic) IBOutlet UIButton *danglingButton; 
//매달려 있을 버튼이 고정되어 있는 곳
@property (strong, nonatomic) IBOutlet UIView *redSquare; 
//버튼이 터치되면 결과를 알려줄 곳
@property (strong, nonatomic) IBOutlet UITextView *resultTextView; 

// 여기에 행동(Behavior)를 등록합니다.
@property (strong, nonatomic) UIDynamicAnimator *animator; 
//아래로 중력 행동을 만들것.
@property (strong, nonatomic) UIGravityBehavior* gravityBehavior; 

//원래 자리로 돌아가는 스냅 행동을 정의 돌아갈 위치가 달라지므로, 두개가 필요합니다.
@property (strong, nonatomic) UISnapBehavior * textFieldSnapBehavior; 
// 버튼 자리로 돌아갈 스냅 행동
@property (strong, nonatomic) UISnapBehavior *fallingButtonSnapBehavior;
@end

@implementation DBUViewController
- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    //
    self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
    
    UIAttachmentBehavior *springBehavior = [[UIAttachmentBehavior alloc] initWithItem:self.danglingButton offsetFromCenter:UIOffsetMake(/*20.0f*/0, /*self.danglingButton.frame.size.height/2*/0) attachedToAnchor:CGPointMake(self.redSquare.center.x, self.redSquare.center.y)];
    [springBehavior setFrequency:1.0f];
    [springBehavior setDamping:0.1f];
    
    [self.animator addBehavior:springBehavior];
}

시작버튼을 누르면, Gravity Behavior를 만들고, 등록을 시킨다.
source code
- (IBAction)startButton:(UIButton *)sender
{
    //gravity behavior가 없는 경우 새로 만들고 등록한다.
    if (!self.gravityBeahvior) {
        [sender setTitle:@"Stop" forState:UIControlStateNormal];
        [self.animator removeBehavior:self.textFieldSnapBehavior];
        [self.animator removeBehavior:self.fallingButtonSnapBehavior];
        
        //중력 행동을 만들때, 어느 Item을 행동에 넣을지 넣습니다.
        self.gravityBeahvior = [[UIGravityBehavior alloc] initWithItems:@[self.fallingButton, self.textField]];
        //만들고 난 다음에 추가할 수도 있습니다.
        [self.gravityBeahvior addItem:self.danglingButton];
        //Dynamic Animator에 행동을 추가하면, 실제 동작하게 됩니다.
        [self.animator addBehavior:self.gravityBeahvior];
    }else{
        //원래 위치로 돌아가기 위해서, 중력 행동을 빼고, 스냅 행동을 넣습니다.
        [sender setTitle:@"Start" forState:UIControlStateNormal];
        [self.animator removeBehavior:self.gravityBeahvior];
        self.gravityBeahvior = nil;
        [self.animator addBehavior:self.textFieldSnapBehavior];
        [self.animator addBehavior:self.fallingButtonSnapBehavior];
    }
}

스냅은 특정 Item이 어디로 이동할 지 넣는 것입니다.
이 스냅이 호출 될 때는 이미 아이템 2개가 아래로 떨어지고 난 다음입니다. 그래도, 원래의 위치는 그대로 저장되어 있는 상태로, Dynamics 엔진 내부에 위치만 달라진 것이므로, 자시느이 위치로 돌아오도록 center 값을 읽을 수 있습니다.
source code
- (UISnapBehavior *)textFieldSnapBehavior
{
    if (_textFieldSnapBehavior == nil) {
        _textFieldSnapBehavior = [[UISnapBehavior alloc] initWithItem:self.textField 
                                                          snapToPoint:self.textField.center];
    }
    return _textFieldSnapBehavior;
}
- (UISnapBehavior *)fallingButtonSnapBehavior
{
    if (_fallingButtonSnapBehavior == nil) {
        _fallingButtonSnapBehavior = [[UISnapBehavior alloc] initWithItem:self.fallingButton 
                                                   snapToPoint:self.fallingButton.center];
    }
    return _fallingButtonSnapBehavior;
}

감사합니다.

[소스코드: https://github.com/davidbae/iOS7-Dynamics-Test.git ]


참고 :
 - WWDC 2013 : Getting started with UIKit Dynamics
      : #206 세션으로, 다이나믹스에 대한 설명이 있음.
 - UIKit Dynamics and iOS 7: Building UIKit Pong
      : 다이나믹스를 이용해서, 바운싱을 예제로 설명하고 있는 곳.
 - Apple의 Dynamics 예제
 - RayWenderlich의 UIKit Dynamics 강좌
 -

2014/03/07

[iOS7] Background Fetch in iOS7

오늘 정리할 내용은, Background Fetch에 대한 것입니다.
이 기능은 iOS7에서 추가 되었고, Background Fetch 는 앱이 백그라운드에 있을 때, 간헐 적으로 iOS에서 호출해서, 앱이 실행 될 수 있도록 해주는 것입니다.

그럼 얼마나 자주 호출이 될까요?

- iOS의 문서에는 "the system uses available information to determine the best time to launch or wake apps. For example, it does so when networking conditions are good or when the device is already awake. "라고 되어 있습니다.
- iPhone 5s에서 테스트를 해보니, 대략 8~10분 정도에서 호출이 되기도 하고,
- 8~10분 정도 지난 후에 파워 키로 화면을 켜고, 패스워드를 입력해서, Main화면이 표시되면, Background Fetch가 호출 되기도 합니다.
- 일정한 시간으로 주기적으로 호출되지 않습니다. 간헐적으로 시스템에서 지금이 최적이다라고 생각하는 시간, 즉 배터리를 사용하기 좋은 시간이라고 생각할 때 호출이 됩니다.

호출 된 후에 얼마나 계속 실행이 될까요?

실행 될 때,  completionHandler를 파라미터로 받습니다.
모든 패치를 끝내면, 이 함수를 호출 해서, Fetch가 끝났음을 시스템에 알려줘야 합니다.
약 30초 안에, 모든 패치를 끝내고, handler를 호출해줘야 합니다. 만약 호출하지 않거나, 시간이 많이 걸리다던지 하는 것에 의해서, 다음 Fetch가 언제 호출 될지 영향을 받는 것 같습니다.
 간단하게 끝날 것 같으면, NSURLRequest를 사용하고, 시간이 걸리면, NSURLSession을 사용하라고 권고하고 있습니다.

Background Fetch를 사용하려면?


1. App에 Background Mode에 설정을 추가합니다.
- Helloworld-Info.plist 에 "Required background modes"항목을 추가하고, Item으로 "App downloads content from the network"를 추가하면 된다.

- 다른 방법으로 Project의 Capability에서 Background Mode를 On하고 background fetch를 체크합니다.

2. 앱이 시작될 때, 시스템에 background fetch시간을 설정합니다.
- 일정 간격을 설정할 수는 없고, Minimum과 Never를 선택할 수 있습니다.
source code
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // Override point for customization after application launch.
    
    //Background Fetch 초기화 함. 가장 짧은 주기로 설정함.
    [[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];

    return YES;
}

3. Fetch가 실행되는 시간을 알아보기 위해서, Background로 들어갈 때와, Fetch가 실행될 때, Local Notification을 보내도록 합니다.
source code
- (void)applicationDidEnterBackground:(UIApplication *)application
{
    // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 
    // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
    UILocalNotification *noti = [[UILocalNotification alloc] init];
    if(noti)
    {
        noti.repeatInterval = 0.0f;
        noti.alertBody = [NSString stringWithFormat:@"Backgournd:%@", [NSDate date]];
        NSLog(@"%@", noti.alertBody);
        [[UIApplication sharedApplication] presentLocalNotificationNow:noti];
    }
    [[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];
}

4. Fetch가 호출 되면, 웹사이트에 접속합니다.
source code
#pragma mark - Background Fetch
- (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
    NSLog(@"Received Background Fetch");
    //데이터를 받아오거나 호출할 URL
    NSURL *url = [NSURL URLWithString:@"http://hidavidbae.blogspot.kr"];
    
    //URL객체가 잘 만들어졌을 경우
    if (url != nil) {
        // URL로 호출할 때 사용할 request객체임.
        NSURLRequest *request = [NSURLRequest requestWithURL:url];
        //데이터 로드 실패 시 주소를 받아올 에러 포인터
        NSError *error = nil;
        //해당 URL의 데이터를 가져옴
        NSData *data = [NSURLConnection sendSynchronousRequest:request
                                             returningResponse:nil error:&error];
        //데이터가 로딩이 되었을 경우
        if (data != nil) {
            //사용자에게 notification을 보냄.
            UILocalNotification *noti = [[UILocalNotification alloc] init];
            if(noti)
            {
                noti.repeatInterval = 0.0f;
                noti.alertBody = [NSString stringWithFormat:@"Fetch Success:(%d):%@",
                                  (int)data.length, [NSDate date]];
                [[UIApplication sharedApplication] presentLocalNotificationNow:noti];
            }            
            //성공했음을 알림
            completionHandler(UIBackgroundFetchResultNewData);
        }else{
            //에러를 로그로 찍음.
            NSLog(@"%@", error);
            //실패했음을 알림.
            completionHandler(UIBackgroundFetchResultFailed);
        }
    }else{
        //실패했음을 알림.
        completionHandler(UIBackgroundFetchResultFailed);
    }
}


실제 동작은 어떻게 될까요?


- Simulator에서 동작하는지 먼저 테스트를 할 수 있습니다.
 시뮬레이터에 앱을 설치해서 동작시키고, Home키로 앱을 Background로 보냅니다.
 이후, Xcode의 debug > Simulate background fetch 를 선택해서 동작을 바로바로 테스트해 볼 수 있습니다.

- 실제 iPhone 5s 디바이스에서는 아래와 같이, 8~10분 쯤에, 아이폰을 켜고, 비밀번호 력후 Main화면에 들어가면, Fetch가 실행이 되었습니다.
 이렇게 몇번 화면이 동작하고 Fetch된 이후 약 8분 정도에 자동으로 Fetch도 된 경우가 있었습니다.
 그리고는 2~3시간 후, 한번, 그리고 1시간 후 한번 정도 실행이 되었습니다.
 특별하게 화면이 꺼져 있는데, 동작한 적은 없는것 같고, 내가 화면을 켜고, Home화면에 들어가면, 위치 정보를 읽고 있음이 화면에 표시되고, Fetch가 실행되는 것으로 보입니다.


마치며..


- Fetch는 언제 일어날까요? 
  - 사용자가 iPhone을 어떤 형태로 사용하는 가에 따라서 달라지는 것 같습니다.
  - 밤에는 Fetch를 하지 않고, 낮에 사용자가 자주 iPhone을 켜면, Fetch도 일어나는 것 같습니다.

- Fetch는 사용자의 편의를 위해서 만들어졌습니다.
   패치가 필요한 이유는 사용자가 iPhone을 켜고, 앱을 실행하기 전에, 데이터가 딱!!! 들어와 있기를 위해서 만들어진 것입니다.
  즉, 필요 없는 시간, 혹은 사용자가 많이 안쓰는 앱에 대해서 불 필요하게 자주 Fetch를 하지 않습니다.
 그리고, 패치하는데 시간이 오래 걸리거나, 응답이 잘 없는 앱에 대해서는 Fetch 스케쥴을 뒤로 미뤄버리는 것 같습니다.

- 어떻게 사용하면 좋을까요?
  실시간은 아니지만, 사용자가 앱을 실행하기 전에, 데이터가 미리 다운로드 받으면, 사용자가 편리하게 이용할 수 있도록 장치를 마련해 놓는 용도로 사용하면 되겠습니다.

[소스코드]

https://github.com/davidbae/iOS7-background-fetch

[추가 분석 자료]

3월 9일 새벽부터, 오후 9시쯤 까지 Fetch가 실행 된 시간을 분석해 보았습니다.
제 iPhone 5s에서는 오전 3시~ 오후1시까지 9시간 동안은 Fetch가 진행이 되지 않았고, 그외의 시간에는 약 8~20분 사이를 오락가락 하며 Background Fetch가 실행이 된 것을 볼 수 있습니다.
 아마도 제가 오후에 iPhone을 많이 사용해서, 점심먹고부터 Fetch가 주기적으로 실행이 되는 것 같습니다.

 2014-03-09 01:42:47 (Background Fetch Start)
 2014-03-09 01:43:12 (00:00:25)
 2014-03-09 01:43:42 (00:00:29)
 2014-03-09 01:44:11 (00:00:29)
 2014-03-09 01:54:26 (00:10:14)
 2014-03-09 02:16:57 (00:22:31)
 2014-03-09 02:39:17 (00:22:20)
 2014-03-09 02:49:21 (00:10:04)
 2014-03-09 03:01:36 (00:12:14)
 2014-03-09 03:31:21 (00:29:44)
 2014-03-09 13:09:56 (09:38:34
      ==>오전에는 한번도 실행이 안되었습니다.
      ==>제가 오전에 사용을 잘 안해서 그런지, 아니면, 중간에 내가 앱을 종료했는지 잘 모르겠네요.
      ==> 며칠 더 자료를 모아보면 알 수 있을 것 같습니다.
 2014-03-09 13:19:40 (00:09:44)
 2014-03-09 13:30:16 (00:10:35)
 2014-03-09 13:40:01 (00:09:45)
 2014-03-09 13:49:46 (00:09:45)
 2014-03-09 13:58:52 (00:09:05)
 2014-03-09 14:07:52 (00:09:00)
 2014-03-09 14:21:36 (00:13:43)
 2014-03-09 14:29:17 (00:07:41)
 2014-03-09 14:30:02 (Background Fetch Start)
 2014-03-09 14:30:22 (Background Fetch Start)
 2014-03-09 14:54:38 (00:24:16)
 2014-03-09 15:02:17 (00:07:38)
 2014-03-09 15:12:01 (00:09:44)
 2014-03-09 15:26:43 (00:14:42)
 2014-03-09 15:41:27 (00:14:43)
 2014-03-09 15:50:51 (00:09:24)
 2014-03-09 16:02:04 (00:11:12)
 2014-03-09 16:15:32 (00:13:28)
 2014-03-09 16:23:53 (00:08:21)
 2014-03-09 16:36:04 (00:12:11)
 2014-03-09 16:45:46 (00:09:41)
 2014-03-09 16:57:40 (00:11:54)
 2014-03-09 17:05:34 (00:07:54)
 2014-03-09 17:18:53 (00:13:19)
 2014-03-09 17:31:16 (00:12:22)
 2014-03-09 17:41:01 (00:09:44)
 2014-03-09 17:50:46 (00:09:45)
 2014-03-09 18:04:46 (00:13:59)
 2014-03-09 18:15:11 (00:10:24)
 2014-03-09 18:23:30 (00:08:19)
 2014-03-09 18:34:16 (00:10:45)
 2014-03-09 18:45:21 (00:11:04)
 2014-03-09 18:53:31 (00:08:10)
 2014-03-09 19:03:46 (00:10:15)
 2014-03-09 19:13:31 (00:09:44)
 2014-03-09 19:21:03 (00:07:32)
 2014-03-09 19:30:26 (00:09:22)
 2014-03-09 19:45:31 (00:15:05)
 2014-03-09 19:55:39 (00:10:07)
 2014-03-09 20:03:54 (00:08:14)
 2014-03-09 20:13:23 (00:09:29)
 2014-03-09 20:23:06 (00:09:42)
 2014-03-09 20:30:56 (00:07:49)
 2014-03-09 20:50:39 (00:19:42)
 2014-03-09 21:08:18 (00:17:38)
 2014-03-09 21:28:46 (00:13:37)
 2014-03-09 21:38:31 (00:09:45)
 2014-03-09 21:48:16 (00:09:44)
 2014-03-09 22:15:36 (00:27:20)

 2014-03-09 23:10:19 (00:54:43)

아래는 3월 10일 자료 입니다.
 2014-03-10 00:30:58 (01:20:38)
 2014-03-10 04:45:58 (04:15:00
 2014-03-10 12:33:41 (07:47:43) //오후 1시이후, 약10분단위로 실행
  ==> 이것으로 미뤄봐서, iOS7에서는 사용자의 사용 형태에 따라서, Fetch를 적당한 시간에 실행하는 것으로 보입니다.

 2014-03-10 12:45:00 (00:11:18)
 2014-03-10 12:53:26 (00:08:26)
 2014-03-10 13:05:01 (00:11:35)
 2014-03-10 13:14:45 (00:09:44)
 2014-03-10 13:31:21 (00:16:35)
 2014-03-10 13:40:11 (00:08:50)
 2014-03-10 13:55:26 (00:15:14)
 2014-03-10 14:05:11 (00:09:45)
 2014-03-10 14:14:56 (00:09:44)
 2014-03-10 14:27:24 (00:12:28)
 2014-03-10 14:35:57 (00:08:32)
 2014-03-10 14:46:11 (00:10:14)
 2014-03-10 14:55:56 (00:09:45)
 2014-03-10 15:05:27 (00:09:30)
 2014-03-10 15:14:24 (00:08:57)
 2014-03-10 15:22:26 (00:08:01)
 2014-03-10 15:32:12 (00:09:46)
 2014-03-10 15:41:46 (00:09:34)
 2014-03-10 15:51:31 (00:09:44)
 2014-03-10 16:01:16 (00:09:45)
 2014-03-10 16:11:01 (00:09:44)
 2014-03-10 16:22:00 (00:10:59)
 2014-03-10 16:33:16 (00:11:15)
 2014-03-10 16:48:01 (00:14:45)
 2014-03-10 16:57:46 (00:09:44)
 2014-03-10 17:07:31 (00:09:45)
 2014-03-10 17:16:40 (00:09:09)
 2014-03-10 17:26:26 (00:09:45)
 2014-03-10 17:36:11 (00:09:44)
 2014-03-10 17:44:43 (00:08:31)
 2014-03-10 17:54:56 (00:10:13)
 2014-03-10 18:02:50 (00:07:53)
 2014-03-10 18:12:31 (00:09:41)
 2014-03-10 18:22:16 (00:09:44)
 2014-03-10 18:31:23 (00:09:07)
 2014-03-10 18:40:01 (00:08:38)
 2014-03-10 18:51:46 (00:11:44)
 2014-03-10 19:01:30 (00:09:44)
 2014-03-10 19:12:47 (00:11:17)
 2014-03-10 19:20:18 (00:07:30)
 2014-03-10 19:30:20 (00:10:01)
 2014-03-10 19:39:49 (00:09:28)
 2014-03-10 19:49:01 (00:09:12)
 2014-03-10 19:56:45 (00:07:44)
 2014-03-10 20:13:16 (00:16:30)
 2014-03-10 20:24:36 (00:11:20)
 2014-03-10 20:33:41 (00:09:05)
 2014-03-10 20:41:34 (00:07:53)
 2014-03-10 20:49:46 (00:08:12)
 2014-03-10 21:02:32 (00:12:46)
 2014-03-10 21:12:22 (00:09:49)
 2014-03-10 21:20:39 (00:08:17)
 2014-03-10 21:30:46 (00:10:06)
 2014-03-10 21:38:47 (00:08:00)
 2014-03-10 21:48:56 (00:10:08)
 2014-03-10 22:00:01 (00:11:05)
 2014-03-10 22:08:20 (00:08:19)
 2014-03-10 22:15:57 (00:07:36)
 2014-03-10 22:28:51 (00:12:54)

 2014-03-10 22:43:56 (00:15:05)

[참고자료]

- iOS7 : What's new in iOS7
- [iOS] :iOS Programming Guide - App State and Multitasking
- [Double Encore]: http://www.doubleencore.com/2013/09/ios-7-background-fetch/
- [Hayageek] : http://hayageek.com/ios-background-fetch/
- [Tuts+] : http://code.tutsplus.com/tutorials/ios-7-sdk-working-with-background-fetch--mobile-20520

2014/01/17

[iOS] Keyboard가 표시될 때, Toolbar의 위치 이동하기.

 키보드가 표시될 때, 발생하는 이벤트를 받아오는 방법은 블로그 ([iOS] Keyboard가 표시될 때, 사라질 때 이벤트와 그 키보드의 위치는?)를 참고하시면 됩니다.
 이번에는 아래에 표시되는 toolbar를 키보드가 표시되는 Animation에 맞춰서 위로 이동하고, 아래로 이동하도록 정리합니다.

1. 관련한 정보는 어디에서 가져오나요?

keyboard가 표시될 경우, NSNotification 객체에 정보를 담아서 전달이 됩니다. 이 notification에서 해당 정보를 찾아서 가져오면 됩니다.

2. 어떤 정보가 필요할 까요?

Toolbar에 위치를 이동시키는데, 이동 시간과, 애니메이션 방법, 위치가 필요합니다.
키보드가 나타나는 시간동안 어떤 형태로 애니메이션으로 어디에 표시되는지를 알면, 그와 같은 방식으로 이동하면 같이 붙어서 이동하는 것처럼 표시가 됩니다.
source code
    NSTimeInterval animationDuration;
    UIViewAnimationCurve animationCurve;
    CGRect keyboardFrame;
    [[userInfo objectForKey:UIKeyboardAnimationCurveUserInfoKey] getValue:&animationCurve];
    [[userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] getValue:&animationDuration];
    [[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] getValue:&keyboardFrame];

키보드가 표시될 때, 사라질 때, Toolbar에 대한 Animation을 아래 소스와 같이 설정해 두면 됩니다.

3. 소스

source code
#pragma mark - Toolbar animation helpers

// Helper method for moving the toolbar frame based on user action
- (void)moveToolBarUp:(BOOL)up 
forKeyboardNotification:(NSNotification *)notification
{
    NSDictionary *userInfo = [notification userInfo];
    
    // Get animation info from userInfo
    NSTimeInterval animationDuration;
    UIViewAnimationCurve animationCurve;
    CGRect keyboardFrame;
    [[userInfo objectForKey:UIKeyboardAnimationCurveUserInfoKey] 
                                      getValue:&animationCurve];
    [[userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] 
                                      getValue:&animationDuration];
    [[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] 
                                      getValue:&keyboardFrame];
    
    // Animate up or down
    [UIView beginAnimations:nil context:nil];
    [UIView setAnimationDuration:animationDuration];
    [UIView setAnimationCurve:animationCurve];
    
    UIToolbar *toolbar = self.navigationController.toolbar;
    [toolbar setFrame:CGRectMake(toolbar.frame.origin.x,
                                 toolbar.frame.origin.y + 
        (keyboardFrame.size.height * (up ? -1 : 1)), toolbar.frame.size.width, toolbar.frame.size.height)];
    [UIView commitAnimations];
}

- (void)keyboardWillShow:(NSNotification *)notification {
    // move the toolbar frame up as keyboard animates into view
    [self moveToolBarUp:YES 
forKeyboardNotification:notification];
}

- (void)keyboardWillHide:(NSNotification *)notification {
    // move the toolbar frame down as keyboard animates into view
    [self moveToolBarUp:NO 
forKeyboardNotification:notification];
}


4. 결과 화면,

 


2014/01/14

[iOS7] Multipeer Connectivity Framework를 이용하여 iOS 기기를 연결하여 봅시다.

(소스 추가: 2014.03.08)

iOS7에서 처음 소개되는 MultipeerConnectivity Framework에 대해서 테스트해 보도록 합니다.
 관련한 자료로는 Apple Developer Site에 여러 기기에서 각각 같은 Room Name으로 접속을 하면, 서로 연결이되어서, 메시지와 이미지를 보낼 수 있도록 하는 예제가 있고, WWDC 2013에 관련 설명이 있습니다. (링크는 아래 참조)
위 예제에서는 MCBrowserViewController와 MCAdvertiserAssistant를 사용하고 있어서, iOS에서 제공하는 기본 UI를 사용해서 만들어진 것입니다.
이 블로그에서는 기본 제공 UI 없이 자체 UI를 가지고 연결하는 방법을 예제로 만들어보겠습니다.

About Multipeer Connectivity Framework

멀티피어 연결 프레임워크는 iOS 3.0 에서 지원되어 오던 GKSession이 Deprecated되면서, 별도의 프레임워크로 등장한 것입니다. 대부분 비슷하지만, 기능이 많이 축소되었고, 서로간에 연결이 가능하면서, Wi-Fi, BT등의 여러 경로에 있는 Peer to Peer를 연결할 수 있도록 지원하고 있습니다.
 즉, A-B가 연결하고, B-C가 연결을 하면, A-C가 자동으로 같은 세션으로 연동이 되게 됩니다.
그렇다는 말은, A-B는 Wi-Fi로 연결되어 있고, B-C가 BT로 연결이 되면, A-C는 B를 중간 연결로 서로 연결이 되는 것입니다.
 iOS6가지 GKSession으로 개발해오던 저는 이 기능으로 소스를 많이 고쳐야 되기에, 고치기 전에 어디까지 지원하는 지 알아보고자 합니다.

1. iOS7 기기를 서로 어떻게 찾을까요?
- 이전에는 GKSession을 통해서, Available한 Peer를 찾고, 거기에 연결을 요구해서, 연결을 했었는데, iOS7에서는 이것을 다른 클래스에서 별도로 제공해 줍니다.

- "나 연결이 가능해요" 알리기
  : MCNearbyServiceAdvertiser 클래스를 통해서, 특정 서비스 타입을 광고하게 할 수 있습니다.
  : MCAdvertiserAssistant 표준 UI를 이용해서 연결할 수도 있습니다.

- "누구 연결 할 수 있는 사람 있나요?" 찾기
  : MCNearbyServiceBrowser 클래스를 통해서, 특정 서비스 타입을 지원하는 기기를 찾을 수 있습니다.
  : MCBrowserViewController 표준 UI를 이용해서, 찾고, 선택해서 연결할 수 있습니다.

위에서 Browser부분은 능동적으로 연결을 요청하는 쪽이 되고, Advertiser를 수동적으로 연결을 기다리게 됩니다.
 Peer-to-Peer로 연결이 가능하기 때문에, Advertiser를 실행해서 광고를 하고, 필요하면, 내가 Browser를 이용해서 지원하는 디바이스를 찾아서 연결할 수도 있습니다.

2. 특정 서비스를 지원하는지 어떻게 알고 연결을 하나요?
- Advertiser와 Browser를 생성하고, 시작할 때, 아래와 같이 특정 서비스 이름을 넣게 됩니다.

MCAdvertiserAssistant의 시작 함수
- (instancetype)initWithServiceType:(NSString *)serviceType discoveryInfo:(NSDictionary *)info session:(MCSession *)session
MCNearbyServiceAdvertiser 의 시작 함수
- (instancetype)initWithPeer:(MCPeerID *)myPeerID discoveryInfo:(NSDictionary *)info serviceType:(NSString *)serviceType
MCNearbyServiceBrowser 의 시작 함수
- (instancetype)initWithPeer:(MCPeerID *)myPeerID serviceType:(NSString*)serviceType
MCBrowserViewController 의 시작 함수
- (instancetype)initWithServiceType:(NSString *)serviceType session:(MCSession*)session

이 serviceType이 동일한 것을 Wi-Fi, BT를 통해서 찾아주게 됩니다. 이 이름이 다르면 전혀 다른 서비스로 인식을 해서, 연결해 주지 않습니다.

3. 연결 이후에 데이터는 어떻게 주고 받나요?
- Browser에서 찾으면, Invitation을 보내는데, 거기에 생성한 MCSession객체를 넘겨주어서, 해당 PeerID와 Session이 연결되도록 하고,
- Advertiser에서는 invitation을 받고, accept하는 handler를 호출할 때, MCSession객체는 넘겨서, 해당 PeerID와 연결하게 됩니다.
- 연결이 완료가 되면, MCSessionDelegate의 didChangeState함수가 호출이되고, 거기에서 Connected로 상태가 넘어오게 됩니다. 그러면 연결이 완료가 된 것입니다.
- 데이터는 MCSession의 sendData 함수를 통해서 NSData 타입의 데이터를 전송할 수 있게 됩니다.
- NSData를 보낼 수 있으므로, Binary데이터도 Serialize해서 전송할 수 있겠습니다.

4. 연결하고 데이터를 송/수신하기까지 전체 과정은 어떻게 되나요?

- Peer를 찾는 경우..
 위 그림의 Peer1의 경우로, MCNearbyServiceBrowser 클래스를 이용해서 특정 서비스 타입/이름으로 등록한 Peer를 찾게 됩니다.
 browser가 찾게 되면, foundPeer:withDiscoveryInfo 함수를 통해서 찾은 피어를 알 수 있게 됩니다. 이 때 같이 오는 info를 통해서, 어떤 Peer인지 구분할 수 있습니다.
 한번 찾은 Peer는 계속 유지를 해야 하고, lostPeer를 통해서, 망에서 피어가 나갔는지 인식할 수 있게 됩니다. lostPeer를 받으면 찾은 피어리스트에서 제거해 주면 됩니다.
 찾은 피어에 대해서 초대를 하기 위해서는 invitePeer:forSession:withContext:timeout 함수를 통해서 초대를 합니다. 파라미터로 넘기는 MCSession객체를 통해서 연결되었는지 알 수 있고, 연결된 후 데이터를 주고 받고, 연결을 끊을 수도 있습니다.
 위 그림은 연결될 때까지만 표현하고 있습니다.

- 연결 받을 경우...
 MCNearbyServiceAdvertiser를 생성하고, 내가 지원하는 서비스 타입(이름)으로 advertising을 시작합니다. 그러면, Wi-Fi, BT에서 해당 서비스를 지원하는 Peer로 등록이 됩니다.
 서비스브라우저가 찾고 난 후, 연결을 위해서, invite를 하면, advertiser에서는 didReceiveInvitationFromPeer:withContext:inviteationHandler이 호출되어서 초대를 수락할 것인지 아닌지를 결정하게 된다.
 invitationHandler함수를 실행하면서, MCSession을 전달하게되고, 그 세션을 통해서, 연결이 되었는지 알 수 있고, 연결된 후 데이터를 주고 받고, 연결을 끊을 수도 있게 됩니다.


연결하는 테스트 Single Application을 만들어 봅니다.

1. XCode를 이용해서 예제를 만듭니다.
 - Single Application
 - Storyboard에
    - Create/Destory/Start/Stop Advertiser
    - Create/Destory/Start/Stop Browser, Invite Peers
    - Session Disconnect/SendData, Create/Destroy Session
    - Info
    버튼과 UITextView를 추가합니다.
 - 각 버튼들에 대한 관련 함수를 만듭니다.
 - Session Container 클래스를 추가합니다. (NSObject 상속)

2. DBUSessionContainer클래스의 함수 추가합니다.

1) 해당 클래스에 필요한 변수와 함수를 정의 합니다.
.h file source code
@protocol DBUSessionContainerDelegate;

@interface DBUSessionContainer : NSObject 

@property (readonly, nonatomic) MCSession *session;
@property (assign, nonatomic) id delegate;

// Designated initializer
- (id)initWithDisplayName:(NSString *)displayName serviceType:(NSString *)serviceType;
// Method for sending text messages to all connected remote peers.  Returna a message type transcript

- (MCPeerID *)peerID;

- (NSData *)sendMessage:(NSString *)message;
// Method for sending image resources to all connected remote peers.  Returns an progress type transcript for monitoring tranfer
- (NSData *)sendImage:(NSURL *)imageUrl;

- (void) startBrowser;
- (void) stopBrowser;
- (void) startBrowsingForPeers;
- (void) stopBrowsingForPeers;
- (void) inviteFoundPeers;

- (void) startAdvertiser;
- (void) stopAdvertiser;
- (void) startAdvertisingPeer;
- (void) stopAdvertisingPeer;

- (void) startSession;
- (void) stopSession;
- (void) disconnect;

- (void) info;
@end

 내부 함수에는 ServiceBrowser, ServiceAdvertiser, Session을 생성/종료, 시작/중지 등의 함수를 호출 할 수 있도록 추가하고, Delegate도 추가합니다.

.m file source code
@interface DBUSessionContainer()
{
    MCPeerID *_myPeerID;
    NSString *_serviceType;
    
    NSMutableDictionary *_foundPeersDictionary;
    NSArray *_invitationArray;
    
    void (^_invitationHandler)(BOOL, MCSession *);
}
//
@property (retain, nonatomic) MCNearbyServiceBrowser *browser;

// Framework UI class for handling incoming invitations
@property (retain, nonatomic) MCAdvertiserAssistant *advertiserAssistant;
@property (retain, nonatomic) MCNearbyServiceAdvertiser *advertiser;
@end


@implementation DBUSessionContainer


- (id)initWithDisplayName:(NSString *)displayName serviceType:(NSString *)serviceType
{
    if (self = [super init]) {
        // Create the peer ID with user input display name.  This display name will be seen by other browsing peers
        _myPeerID = [[MCPeerID alloc] initWithDisplayName:displayName];
        // Create the session that peers will be invited/join into.  You can provide an optinal security identity for custom authentication.  Also you can set the encryption preference for the session.
        _serviceType = serviceType;
        
        [self startSession];
        
        _foundPeersDictionary = [[NSMutableDictionary alloc] init];
    }
    return self;
}
// On dealloc we should clean up the session by disconnecting from it.
- (void)dealloc
{
    [self stopAdvertisingPeer];
    [self stopSession];
}
 생성자에서 MCPeerID를 생성하고 MCSession을 Create합니다.

2) MCNearbyServiceBrowser와 Delegate를 추가합니다.

.m file source code
#pragma mark - MCNearbyServiceBrowserDelegate
- (void)browser:(MCNearbyServiceBrowser *)browser
      foundPeer:(MCPeerID *)peerID withDiscoveryInfo:(NSDictionary *)info
{
    NSString *log;
    if ([[_foundPeersDictionary allKeys] containsObject:peerID])
    {
        log = [NSString stringWithFormat:@"found PeerID(%@) but already found", peerID.displayName];
    }else{
        [_foundPeersDictionary setObject:info forKey:peerID];
        log = [NSString stringWithFormat:@"found PeerID(%@)", peerID.displayName];
    }
    LOGMESSAGE(log);
    
    [self.delegate updateFoundPeers:[_foundPeersDictionary allKeys]];
}

- (void)browser:(MCNearbyServiceBrowser *)browser lostPeer:(MCPeerID *)peerID
{   //Advertiser가 stopAdvertisingForPeer를 호출하여 광고를 중단했을 경우 호출되며,
    //연결하려는 client가 더 연결을 하기 어려울 때 호출됨. 추가 적으로 연결하면 안됨.
    NSString *log;
    if([[_foundPeersDictionary allKeys] containsObject:peerID] ){
        log = [NSString stringWithFormat:@"browser lostPeer: %@", peerID.displayName];
        [_foundPeersDictionary removeObjectForKey:peerID];
    }else{
        log = [NSString stringWithFormat:@"browser lostPeer: %@ but not in list", peerID.displayName];
    }
    LOGMESSAGE(log);
    [self.delegate updateFoundPeers:[_foundPeersDictionary allKeys]];
}

- (void)browser:(MCNearbyServiceBrowser *)browser didNotStartBrowsingForPeers:(NSError *)error
{
    NSLog(@"didNotStartBrowsingForPeers:%@", error.localizedDescription);
}


3) MCNearbyServiceAdvertiser와 Delegate를 추가합니다.

.m file source code
#pragma mark - MCNearbyServiceAdvertiserDelegate
- (void)advertiser:(MCNearbyServiceAdvertiser *)advertiser didNotStartAdvertisingPeer:(NSError *)error
{
    NSLog(@"didNotStartAdvertisingPeer: %@", [error description]);
}
- (void)advertiser:(MCNearbyServiceAdvertiser *)advertiser didReceiveInvitationFromPeer:(MCPeerID *)peerID withContext:(NSData *)context invitationHandler:(void (^)(BOOL, MCSession *))invitationHandler
{
    NSLog(@"didReceiveInvitationFromPeer:%@", peerID.displayName);
    if( context )
    {
        NSLog(@"                 context:%@", [NSString stringWithCString:[context bytes] encoding:NSUTF8StringEncoding]);
    }
    if(_session){
        [self.delegate logMessage:[NSString stringWithFormat:@"Invitation received from %@", peerID.displayName]];
        
        if (!_invitationArray) {
            _invitationArray = [NSArray arrayWithObject:[invitationHandler copy]];
        }
        _invitationHandler = invitationHandler;
        UIAlertView *alertView = [[UIAlertView alloc]
                                  initWithTitle:@"Invitation"
                                  message:[NSString stringWithFormat:@"from %@", peerID.displayName]
                                  delegate:self
                                  cancelButtonTitle:@"NO"
                                  otherButtonTitles:@"YES", nil];
        [alertView show];
        alertView.tag = 2;
        
        NSLog(@"                     accepts: YES");
    }else{
        invitationHandler(NO, _session);
        NSLog(@"                     accepts: NO");
    }
    NSLog(@"                     session:%@", _session);
    
    //[self stopAdvertisingPeer]; //연결 된 후에 중단한다.
}

4) MCSession과 delegate를 추가합니다.

.m file source code
#pragma mark - MCSessionDelegate

- (NSString *)stringForPeerConnectionState:(MCSessionState)state
{
    switch (state) {
        case MCSessionStateConnected:
            return @"MCSessionStateConnected";
        case MCSessionStateConnecting:
            return @"MCSessionStateConnecting";
        case MCSessionStateNotConnected:
            return @"MCSessionStateNotConnected";
        default:
            break;
    }
    return @"NoneState";
}
- (void)session:(MCSession *)session
           peer:(MCPeerID *)peerID
 didChangeState:(MCSessionState)state
{
    NSLog(@"Peer [%@] changed state to %@", peerID.displayName, [self stringForPeerConnectionState:state]);
    NSLog(@"       Session: %@", session);
    
    switch (state) {
        case MCSessionStateConnected:{
            
            NSArray *peers = _session.connectedPeers;
            if(![peers containsObject:peerID]){
                [_session connectPeer:peerID withNearbyConnectionData:nil];
                [self.delegate logMessage:[NSString stringWithFormat:@"Peer(%@) is connected but not in connectedPeers(%@)\n connectPeer", peerID.displayName, peers]];
            }else{
                //[self stopAdvertisingPeer];
                [self.delegate logMessage:[NSString stringWithFormat:@"Peer(%@) is connected", peerID.displayName]];
            }
            break;
        }
        case MCSessionStateConnecting:
            break;
        case MCSessionStateNotConnected:
            //self.sendDataButton.enabled = NO;
            [self.delegate logMessage:[NSString stringWithFormat:@"Peer(%@) is Not connected", peerID.displayName]];
            break;
        default:
            break;
    }
}

- (void)session:(MCSession *)session
 didReceiveData:(NSData *)data
       fromPeer:(MCPeerID *)peerID
{
    NSLog(@"didReceiveData: %@ from peerID:%@", [data description], peerID.displayName);
    
    // Decode the incoming data to a UTF8 encoded string
    NSString *receivedMessage = [[NSString alloc] initWithData:data encoding: NSUTF8StringEncoding];
    [self.delegate receivedMessage:[NSString stringWithFormat:@">>>[%@]%@",peerID.displayName,receivedMessage]];
    
    NSArray *peers = _session.connectedPeers;
    if(![peers containsObject:peerID])
    {
        [_session connectPeer:peerID withNearbyConnectionData:nil];
    }
}

- (void)session:(MCSession *)session didStartReceivingResourceWithName:(NSString *)resourceName
       fromPeer:(MCPeerID *)peerID
   withProgress:(NSProgress *)progress
{
    NSLog(@"didStartReceivingResourceWithName: %@ fromPeer:%@", resourceName, peerID.displayName);
}
- (void)session:(MCSession *)session didReceiveStream:(NSInputStream *)stream
       withName:(NSString *)streamName
       fromPeer:(MCPeerID *)peerID
{
    NSLog(@"didREceiveStream: %@, fromPeer:%@", streamName, peerID.displayName);
}
- (void)session:(MCSession *)session didFinishReceivingResourceWithName:(NSString *)resourceName fromPeer:(MCPeerID *)peerID atURL:(NSURL *)localURL withError:(NSError *)error
{
    NSLog(@"didFinishReceivingResourceWithName: %@, %@", resourceName, peerID.displayName);
}


- (void) session:(MCSession*)session didReceiveCertificate:(NSArray*)certificate fromPeer:(MCPeerID*)peerID certificateHandler:(void (^)(BOOL accept))certificateHandler
{
    if (certificateHandler != nil) {
        certificateHandler(YES);
        NSLog(@"certificateHandler called: %@", certificateHandler);
        [self.delegate logMessage:@"CertificateHandler called"];
    }
}



3. 화면 구성을 아래와 같이 구성합니다.

iPad Test UI
iPhone Test UI 

위 화면에서처럼 각 ServiceBrowser, ServiceAdvertiser, Session에 대한 기능들을 내가 컨트롤 할 수 있게 만들면 여러 가지 내용을 이해하는데 도움이 됩니다.

테스트 결과

1. Server/Client의 전형적인 구조를 만든다면...
  - 서버와 클라이언트를 Host Game, Join Game의 버튼으로 한번에 하나의 모드로 동작할 수 있도록 만들고, 각 기능이 종료하거나, 끝나면, Browser, Advertiser, Session을 종료하고 다시 시작하도록 하는 것이 좋습니다.
 - 서버에 클라이언트가 찾아서 접속하는 방식이라면, 서버는 Advertiser가 되고, 클라이언트는 Browser로 동작을 하도록 해서, 클라이언트에서 서버로 접속을 요청하도록 해야 합니다. (물론 반대로도 구현이 가능합니다.)

2. 클라이언트를 실행도중에 home 키로 종료를 하게 되면...
 - Background로 들어가기 전에, Browser와 Session을 종료하여야, 다시 접속할 경우, 다른 이름으로 접속이 용이 합니다.
 - 세션을 그대로 남겨두어로 무관한데, 브라우저가 보낸 초대를 수락하지 못하는 경우가 종종 발생하고 있습니다.

3. Browser에서 찾은 advertiser를 기록하고 있는 것이 중요함..
 - foundPeer로 찾은 Peer에 대해서 따로 저장하고 있어야, advertiser가 종료하여도 다시 invitation을 보낼 수 있습니다.
 - 그렇지 않으면, Advertiser를 stop, destroy하고 다시 생성하여야, 신규 Peer로 browser에서 foundPeer가 호출이 됩니다.


몇가지 더 있는 것은 실험하면서, 정리하도록 하겠습니다.


[소스 코드]

- 기존 소스에서 메시지를 입력할 수 있는 기능을 추가하였습니다.
- 소스: https://github.com/davidbae/MultipeerConnectivityTest