레이블이 Pasteboard인 게시물을 표시합니다. 모든 게시물 표시
레이블이 Pasteboard인 게시물을 표시합니다. 모든 게시물 표시

2013/12/07

[iOS] 두 App 사이에 데이터 교환하기, ( URL 및 Pasteboard 사용)

하나의 앱(SenderApp) 에서 다른 앱(ReceiverApp)으로 정보를 전달해야 될때,
방법1) URL을 이용해서, 앱을 호출하고, URL에 데이터를 base64로 인코딩해서 보낼 수 있습니다.


방법2) 다른 방법으로 Unique Pasteboard를 만들고, 거기에 데이터를 넣고, URL을 통해서 해당 앱을 호출하고, 그 앱에서 Unique Pasteboard에서 데이터를 읽어 올 수가 있습니다.


1. URL의 데이터부분으로 전달하기.

 Receiver App의 URL Type을 등록합니다.
 dbuurl의 URL Type을 등록된 Receiver App이 설치가 되면, openURL함수를 이용해서, 호출할 수 있습니다.

Sender App에서 정보를 아래와 같이 보낼 수 있습니다.
source code

- (IBAction)sendData:(id)sender
{
    NSString *encodedData = [NSString stringWithFormat:@"iamdavidbae@gmail.com:"];
    NSString *urlQuery = [NSString stringWithFormat:@"dbuurl://localhost/data?%@", encodedData];
    
    NSURL *url = [NSURL URLWithString:urlQuery];
    if( [[UIApplication sharedApplication] canOpenURL:url] == YES )
    {   
        //데이터를 보낸다.
        [[UIApplication sharedApplication] openURL:url];
    }else{
        // openURL을 할 수 없으므로, App이 설치되지 않았거나, URL이 잘못 되었다.
    }
}

ReceiverApp에서 openURL로 호출이 되면, 아래의 AppDelegate의 handleOpenURL openURL:sourceApplication:annotation함수가 호출이 됩니다.

source code

//- (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url //deprecated...
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation
    NSLog(@" openURL");
    if ([@"/data" isEqual:[url path]]) {
        NSString *urlData = [url query];
        NSLog(@" handleOpenURL: query:%@", urlData);
        _viewController.text = [NSString stringWithFormat:@" handleOpenURL: query:%@", urlData];
        //그리고 base64 스트링을 디코딩해야 한다.

        //받은 데이터를 화면에 표시합니다.
        _viewController.text = urlData;
        [_viewController.view setNeedsDisplay];
        
        return YES;
    }
    return NO;
}

주의 사항!
URL로 전달하는 것이므로, 데이터가 base64로 인코딩이 되어야 합니다. 받는 부분에서는 다시 디코딩을 해야 되고..

2. Pasteboard를 사용해서 데이터를 전달하자.

위에 방법은 URL에 데이터가 공유가 되므로, 민감한 데이터는 공유하기가 힘들어지고, 모든 것을 Base64로 만들어야 하는 번거로움이 있습니다.
그래서, Unique한 Pasteboard를 만들고 거기에 데이터를 넣어서 전달 할 수 있습니다.
일단 ReceiverApp에서 URL Type을 위와 동일하게 등록을 합니다.

SenderApp에서 URL을 호출하기 전에 특정 Pasteboard에 데이터를 저장합니다.
아래에서는 "kr.pe.davidbae.pasteboard_001"이라는 이름으로 만듭니다.
source code
- (IBAction)sendData:(id)sender
{    
    NSString *urlQuery = [NSString stringWithFormat:@"dbuurl://localhost/data?%@"];
    NSURL *url = [NSURL URLWithString:urlQuery];
    if( [[UIApplication sharedApplication] canOpenURL:url] == YES )
    {
        //받는 app이 설치되어 있다. Pasteboard를 만들고, 데이터를 넣자.
        UIPasteboard *pasteboard = [UIPasteboard pasteboardWithName:@"kr.pe.davidbae.dbuurl.pasteboard_001" create:YES];
        if (pasteboard != nil) {
            pasteboard.string = sendingData; //보내는 데이터를 넣는다.
        }else{
            NSLog(@"Can't create pasteboard");
        }
        [[UIApplication sharedApplication] openURL:url];
    }else{
        // openURL을 할 수 없으므로, App이 설치되지 않았거나, URL이 잘못 되었다.
    }
}

ReceiverApp에서 handleOpenURL openURL:sourceApplication:annotation함수에서 pasteboard를 읽어서 데이터를 읽어 옵니다.
source code
//- (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url //deprecated...
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation
{
    NSLog(@" openURL");
    if ([@"/data" isEqual:[url path]]) {
        NSLog(@" handleOpenURL: query:%@", urlData);
        //Pasteboard check
        UIPasteboard *pasteboard = [UIPasteboard pasteboardWithName:@"kr.pe.davidbae.dbuurl.pasteboard_001" create:NO];
        if (pasteboard != nil) {
            //정보를 읽어 온다. 읽어온 후에는 정보를 지운다.
            NSLog(@"pasteboard : %@", pasteboard.string);
            pasteboard.string = @"";
        }else{
            //Pasteboard가 없다!!
            NSLog(@"pasteboard is nil");
        }
        return YES;
    }

    
    return NO;
}


위 두가지 방법을 필요한 곳에 사용하면 되겠습니다.

3. 만들어진 Pasteboard는 언제 사라질 까요?

UIPasteboard의 설명에는, 페이스트보드를 만든 앱이 quit를 할 경우 사라진다고 되어 있는데, 직접 테스트를 해보니, 리부팅할 때까지 그대로 남아 있습니다.
Persistant속성을 넣으면, 기부팅할 때까지 계속 남아 있고, App을 uninstall하였을 경우에만 사라진다고 되어 있습니다.

[iOS] Pasteboard에 Copy한 데이터를 Background에서 읽어서 표시하자.

앱스토어에 등록된 Biscuit이라는 앱이, Background에 있으면서, 사용자가 Copy한 영어 단어를 받아서, 단어의 뜻을 Notification으로 알려주는 기능이 있습니다.
단어를 따로 copy해서 찾을 필요 없고, 바로 알림(Notification)으로 알려주니 정말 잘 만든 것 같습니다.
 그럼, App이 Backgound에서 어떻게 사용자가 Copy를 하였는지 알 수 있을까요?

1. 어디에 copy를 하느냐.

 사용자가 특정 String을 선택해서 copy를 하면, 시스템의 generalPasteboard에 추가가 됩니다. 이것은  UIPasteboard 를 참고하시면 됩니다.
 [UIPasteboard generalPasteboard]로 리턴되는 pasteboard는 시스템에서 공통적으로 사용하는 것으로, 리부팅해도 그대로 남아 있습니다. persist의 속성을 가지고 있습니다.
 하지만 공용으로 사용하는 것이므로, 다른 앱에서 모두다 copy를 하면 여기에 써지게 됩니다.
 그래서, 웹페이지에서 특정 단어를 Copy를 하면, 여기에 저장이 됩니다. 그래서 이 저장이 언제 변경이 되었는지 알아낼 수 있다면, Biscuit 앱처럼 기능을 구현할 수 있습니다.

2. 변경사항 알아내기 ( UIPasteboardChangedNotification )

앱이 Active되어 있는 상태에서는 UIPasteboardChangedNotification 이벤트를 NotiCenter에 등록하면, pasteboard가 변경되었을 때마다 해당 Event를 받을 수 있습니다.

source code
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(changeNotification) name:UIPasteboardChangedNotification object:nil];


하지만, 앱이 백그라운드로 들어간 상태에서는 이 이벤트를 받을 수 없게 됩니다.

3. 백그라운드에서 Pasteboard의 변경사항 알아내기.

(이 방식은 Stackoverflow의 Grabbing the UIPasteboard like Pastebot while running in the background 의 답변으로 되어 있는 부분을 참고하여 만들었습니다.)
앱이 백그라운드로 들어가는 함수에서, task를 만들어서 일정시간 동안 동작하게 하고, 1초 단위로 Pasteboard에 변경이 있는지 Polling방식으로 체크를 합니다.

source code
- (void)applicationDidEnterBackground:(UIApplication *)application
{
    // Create a background task identifier
    __block UIBackgroundTaskIdentifier task; 
    task = [application beginBackgroundTaskWithExpirationHandler:^{
        NSLog(@"System terminated background task early"); 
        [application endBackgroundTask:task];
    }];

    // If the system refuses to allow the task return
    if (task == UIBackgroundTaskInvalid)
    {
        NSLog(@"System refuses to allow background task");
        return;
    }

    // Do the task
    dispatch_async(dispatch_get_global_queue(0, 0), ^{

        NSString *pastboardContents = nil;

        for (int i = 0; i < 1000; i++) 
        {
            if (![pastboardContents isEqualToString:[UIPasteboard generalPasteboard].string]) 
            {
                //사용자에게 notification을 보냄.
                UILocalNotification *noti = [[UILocalNotification alloc] init];
                if(noti)
                {
                    noti.repeatInterval = 0.0f;
                    noti.alertBody = [NSString stringWithFormat:@"Pasteboard:%@ -> %@", 
                                                               pastboardContents, 
                                                               [UIPasteboard generalPasteboard].string];
                    [[UIApplication sharedApplication] presentLocalNotificationNow:noti];
                }
                //중복체크를 위해서, 데이터를 저장함.
                pastboardContents = [UIPasteboard generalPasteboard].string;
                NSLog(@"Pasteboard Contents: %@", pastboardContents);
            }

            // Wait some time before going to the beginning of the loop
            [NSThread sleepForTimeInterval:1];

            //앱이 다시 Active되면 task를 멈춰야 한다.
            if (_stopFlag) {
                break;
            }
        }

        // End the task
        [application endBackgroundTask:task];


    });


}
(원문: http://stackoverflow.com/a/10268252)

4. 동작 방식 보기




왼쪽이 Biscuit 앱을 실행하고, 'Retina'를 Copy한 상태이고, 오른쪽은 '디스플레이'를 copy해서, Pasteboard의 String이 'Retina'에서 '디스플레이'로 변경되었음을 알 수 있습니다.

5. 응용방법

가계부 등의 앱에서 문자로 들어온 카드 사용 내용을 사용자가 copy해서 붙여넣는 것이아니라, SMS에서 copy만 하면, 바로 바로 알아 내게 됩니다.


6. 그러나.. 문제점

  1) 백그라운드에서 무한정 1초마다 체크하는 것은 비효율적입니다.
  2) 애플에서 이런 방식의 Background 동작을 승인할 것인가?
  3) 10분이 지나면, 자동으로 Expired되어서 Task가 종료 됩니다.

10분이 지나면, 아래와 같이 Background에 있던 타스크가 종료됩니다. 거의 정확하게 10분이 지나면 종료가 되는 군요.


Biscuit에서는 어떻게 해결을 했는지 궁금합니다.