iOS2012. 8. 20. 13:21

1. 밖에서 안으로
당연한 이야기지만
[webView stringByEvaluatingJavaScriptFromString:@"alert('aa')"];

이런게 가능하다.
외부(ex:push notification)로부터 해당 웹뷰 안에다가 Javascript를 실행하게 할 수 있다.
2. 안에서 밖으로
document.location = "iOS:checkParams:" + param1;
javascript 에서 custom protocol (여기선 iOS라는 이름)
마찬가지로

-(BOOL) webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {

NSString *URL = [[request URLabsoluteString];

NSLog(@"requested URL :%@", URL);

// URL 특정 문자열과 매치하면

if ([URL isEqualToString:@"iOS:checkParams:"]) {

// 처리

return NO;

}

return YES;

}

커스텀 프로토콜을 지정할 수 있다. 말이 거창해서 커스텀 프로토콜이지 그냥 문자열 지지는 것으로 보면 쉽다.

Posted by 다오나무
iOS2012. 8. 20. 13:10

모달 뷰 (Modal View) 란?

모달 뷰(Modal View)란 iOS(iPhoneOS) 에서 제공하는 여러 개의 뷰를 전환하기 위한 아주 기본적인 뷰 관리 기법 중 하나입니다. 특히 모달 뷰 컨트롤러(Modal View Controller)는 현재 어플리케이션 흐름을 잠시 중단하고 사용자에게 무언가를 입력을 받게 하거나 표시한 후에 다시 원래의 흐름으로 되돌아가도록 흐름을 변경할 때 사용됩니다. 그림을 보며 설명하겠습니다. 아래 표시된 All Calendar / Add Event 간의 관계가 전형적인 모달 뷰 관계로 볼 수 있습니다.



 

일정을 하나 추가하기 위해서는 다음과 같은 절차가 필요합니다.

  • Calender 화면에서 추가하기 버튼을 터치하여 Add Event 화면을 띄웁니다.
  • Add Event 화면에서 일정을 입력합니다.
  • Done 버튼을 터치하여 Add Event 를 종료시킵니다.
  • Calender 화면으로 되돌아 옵니다.


위와 같이 사용자가 일정을 관리하다가  현재의 흐름을 잠시 중단하고 원래의 흐름으로 다시 되돌아 오도록 애플리케이션의 흐름을 구현하고자 할때 모달 뷰를 사용하는 것입니다. 참고로 모달 뷰 모델에서는 호출자(Caller)와 피호출자(Callee)간의 부모(Parent)와 자식(Child) 관계가 생깁니다.

모달 뷰 컨트롤러 띄우기
현재 애플리케이션 흐름에서 모달 뷰 컨트롤러를 띄우기 위해서는 부모 뷰의 인터랙션 내 아래와 같이 구현합니다. 

- (IBAction) buttonPressed:(id)sender
  {
      
SecondViewController *viewController = [[SecondViewController alloc] init];    // 새롭게 띄울 뷰 컨트롤러 생성
      [self presentModalViewController:viewController animated:YES];  // 새로운 뷰 컨트롤러를 모달로 표시
  }
 


모달 뷰 컨트롤러 닫기
모달 뷰를 닫고 원래의 흐름으로 되돌아 가기 위해서는 모달 뷰의 인터랙션 내 아래와 같이 구현합니다.

- (IBAction) buttonPressed:(id)sender
  {
      
[self dismissModalViewControllerAnimated:YES];  // 현재 모달로 띄워진 뷰 컨트롤러를 닫음
  
}



모달 뷰 컨트롤러 전환시 효과주기
iOS에서는 기본적으로 모달 뷰를 전환하기 위한 기본적인 전환 애니메이션을 제공합니다.
 모달 뷰를 전환하기 위한 애니메이션 효과는 setModalTransitionStyle을 이용하여 아래와 같이 값을 설정하면 전환시 효과가 적용됩니다.

  -(IBAction)buttonPressed:(id)sender
   {
      SecondViewController *viewController = [[SecondViewController alloc] init];
      [viewController setModalTransitionStyle:UIModalTransitionStylePartialCurl];
      [self presentModalViewController:viewController animated:YES];
   }

  // UIModalTransitionStyleCoverVertical : 모달 뷰가 아래서 위로 덮으며 전환됩니다. (기본값)

  // 
UIModalTransitionStyleFlipHorizontal : 앞면의 부모 뷰가 회전되어 뒷면의 모달 뷰로 전환됩니다
  // UIModalTransitionStyleCrossDissolve : 부모 뷰가 서서히 사라짐과 동시에 모달 뷰로 전환됩니다.

  // UIModalTransitionStylePartialCurl : 부모 뷰가 종이처럼 휘어지며 모달 뷰로 전환됩니다. 


실행화면 - UIModalTransitionPartialCurl

















Posted by 다오나무
iOS2012. 8. 1. 08:21

UDID 대체 방법 -> MAC 어드레스 얻기

올해 3월이었죠. 애플이 개인정보 유출을 막고자 UDID를 쓰는 앱에 대해 단계적으로 심사하겠다고 했습니다.

지금은 IOS 5.0 이상에서는 UDID를 추출하는

NSString *udid = [[UIDevice currentDeviceuniqueIdentifier];


메소드를 쓰면 안된다고 하네요. 


그래서 나온 대안 중, MAC 어드레스 값을 추출하는 방법입니다.


일종의 PC의 랜카드 고유번호(?)라고 하는데 아이폰에서는 네트워크 접속 이더넷카드 시리얼 넘버라고 보시면 되요.


구글에서 떠도는 소스는 3G와 WI-FI 환경에서 값이 다르게 추출된다고 하네요.


형식은 FF:FF:FF:FF:FF:FF 이런식으로 나와요

그 문제를 해결한 소스입니다. 저도 어디서 구한 소스구요 ㅋ

개발자님들 대단하다.


구현 방법입니다.



Macaddress.h


char*  getMacAddress(char* macAddress, char* ifName);


Macaddress.c


#include <sys/types.h>

#include <stdio.h>

#include <string.h>

#include <sys/socket.h>

#include <net/if_dl.h>

#include <ifaddrs.h>


char*  getMacAddress(char* macAddress, char* ifName) {

    

    int  success;

    struct ifaddrs * addrs;

    struct ifaddrs * cursor;

    const struct sockaddr_dl * dlAddr;

    const unsigned char* base;

    int i;

    

    success = getifaddrs(&addrs) == 0;

    if (success) {

        cursor = addrs;

        while (cursor != 0) {

            if ( (cursor->ifa_addr->sa_family == AF_LINK)

                && (((const struct sockaddr_dl *) cursor->ifa_addr)->sdl_type == 0x06) && strcmp(ifName,  cursor->ifa_name)==0 ) {

                dlAddr = (const struct sockaddr_dl *) cursor->ifa_addr;

                base = (const unsigned char*) &dlAddr->sdl_data[dlAddr->sdl_nlen];

                strcpy(macAddress, ""); 

                for (i = 0; i < dlAddr->sdl_alen; i++) {

                    if (i != 0) {

                        strcat(macAddress, ":");

                    }

                    char partialAddr[3];

                    sprintf(partialAddr, "%02X", base[i]);

                    strcat(macAddress, partialAddr);

                    

                }

            }

            cursor = cursor->ifa_next;

        }

        

        freeifaddrs(addrs);

    }    

    return macAddress;

}



갖다 쓰실 때는 이런식으로 


#import "MacAddress.h"


char* macAddressString= (char*)malloc(18);

    NSString *macAddress= [[NSString allocinitWithCString:getMacAddress(macAddressString,"en0")

                                                   encoding:NSMacOSRomanStringEncoding];

    NSLog(@" %@ ", macAddress);


 

Posted by 다오나무
iOS2012. 7. 10. 18:42

어제 클리앙 모두의 공원 게시판은 NSString 관련 메소드만 가지고 파싱을 해 보았는데요. 코드도 꽤 길어지고 굉장히 무식한 방법이었습니다. 어제 포스팅 마지막에 언급했던대로 좀 더 편한 방법들을 살펴보려고 하는데요. hpple라는 API가 있습니다. 나온지도 오래되었고(업데이트 된지도...), 제가 원래 외부API를 잘 사용안하는데요. 뭐 대체제가 없다보니 써 보겠습니다.


일단 아래 주소에서 받으실 수 있구요.

https://github.com/topfunky/hpple


프로젝트에 6개의 파일을 포함시켜 주시면 됩니다.

TFHpple.h

TFHpple.m

TFHppleElement.h

TFHppleElement.m

XPathQuery.h

XPathQuery.m


그리고 환경설정을 두개 해 주셔야 하는데 PROJECT환경설정에서(Build Settings) All을 누르시면 여러가지 설정들을 찾아보실 수 있는데요. 설정해야될 것은 아래와 같습니다.

Header Search Paths  : ${SDKROOT}/usr/include/libxml2

Other Linker Flags : -lxml2


그리고 마지막으로 html파싱이 이루어지는 프로젝트에 TFHpple.h를 import 시켜주시면 설정은 다 끝났습니다.


프로젝트에 포함시킨 소스파일들을 한번 살펴보면 실제 파싱은 XPathQuery에서 이루어 지고 있음을 확인할 수 있습니다. 다른 소스들도 복잡하지 않으니 한번 살펴보시면 좋을것 같고요.


어제 클리앙 모공을 파싱했던 부분 기억나시나요. 가져온 데이터가 게시물제목, 링크, 아이디, 조회수, 글쓴시간이었는데요. 실제 데이터 파싱하는 부분만 구현 해 보겠습니다.


// 클리앙 모공

NSString * path = @"";

NSError * error;

NSString * stringFromURL = [[NSString allocinitWithContentsOfURL:[NSURLURLWithString:@"http://clien.career.co.kr/cs2/bbs/board.php?bo_table=park"encoding:NSUTF8StringEncodingerror:&error];

    

if(stringFromURL == nil)

{

    NSLog(@"Error reading URL at %@\n%@", path, [error localizedFailureReason]);

}

윗 부분은 어제 했던 부분과 동일하고요.


NSData * data = [stringFromURL dataUsingEncoding:NSUnicodeStringEncoding];

        

// Create parser

TFHpple * xpathParser = [[TFHpple allocinitWithHTMLData:data];

        

// 타이틀 & URL

NSArray * elements = [xpathParser searchWithXPathQuery:@"//tr//td[2]//a"];

for(int i = 0 ; i < [elements count] ; i++)

{

    NSLog(@"%@", [[elements objectAtIndex:i] content]);                             // 타이틀

    NSLog(@"%@", [[[elements objectAtIndex:i] attributes] valueForKey:@"href"]);    // URL

}

타이틀과 URL을 파싱해서 가져오는 방법입니다. 오렌지색으로 하이라이트 된 부분과 일치하는 형태를 찾아서 elements에 배열형태로 넣는 방법인데요. tr태그 내부에 td태그가 2번 나온 뒤 a태그가 나오는 형태를 찾으라는 말인데요.


<tr class="mytr">

<td>10816621</td>

<td class="post_subject">&nbsp;&nbsp;<a href='../bbs/board.php?bo_table=park&wr_id=10816621' >[차단공시]삭제요청에 의한 차단공시</a></td>

<td class="post_name"><img src='../data/member/ci/cipher.gif' width='54' height='16' align='absmiddle' border='0'></td>

<td><span title="2012-02-15 17:25:24">17:25</a></td>

<td>1</td>

</tr>

<tr class="mytr">

<td>10816618</td>

<td class="post_subject">&nbsp;&nbsp;<a href='../bbs/board.php?bo_table=park&wr_id=10816618' >초고도 근시인데 라섹했습니다</a></td>

<td class="post_name"><img src='../data/member/hi/hikari.gif' width='60' height='16' align='absmiddle' border='0'></td>

<td><span title="2012-02-15 17:25:11">17:25</a></td>

<td>10</td>

</tr>

<tr class="mytr">

<td>10816613</td>

<td class="post_subject">&nbsp;&nbsp;<a href='../bbs/board.php?bo_table=park&wr_id=10816613' >암스텔담에선 자전거 뒤에 타려면  정도 운동 신경은 가져야...</a></td>

<td class="post_name"><span class='member'>Guylian</span></td>

<td><span title="2012-02-15 17:25:01">17:25</a></td>

<td>36</td>

</tr>

위의 html 중 색칠해진 부분들이 배열에 들어가게 됩니다. 여기서 한가지 보셔야 할 것은..


content의 경우 해당 태그가 열리고 닫힌 사이의 값, 즉 아래에서 색칠된 부분입니다.

<tr class="mytr">

<td>10816618</td>

<td class="post_subject">&nbsp;&nbsp;<a href='../bbs/board.php?bo_table=park&wr_id=10816618' >초고도 근시인데 라섹했습니다</a>

<a 태그가 열리고, </a>로 닫힌 사이의 값입니다.


attribute의 경우 해당 태그 내부에서 key=value 형태로 저장된 값입니다. NSDictionary형의 값이기 때문에 태그의 key가 딕셔너리의 키가 되고 태그의 value가 딕셔너리의 value가 됩니다.

<tr class="mytr">

<td>10816618</td>

<td class="post_subject">&nbsp;&nbsp;<a href='../bbs/board.php?bo_table=park&wr_id=10816618' >초고도 근시인데 라섹했습니다</a>

여기에서는 오렌지색이 key가 되고  청록색이 value가 되기 때문에 

[[[elements objectAtIndex:i] attributes] valueForKey:@"href"]


이런식으로 value를 가져올 수 있었습니다.


그럼 아이디를 가져오는 방법을 살펴볼텐데요. 아이디의 경우 두가지 형태가 있기 때문에 조금 복잡합니다. 일단 html 로 아이디가 스트링인 경우와 아이디가 이미지인 경우를 살펴보면요.

<tr class="mytr">

<td>10816618</td>

<td class="post_subject">&nbsp;&nbsp;<a href='../bbs/board.php?bo_table=park&wr_id=10816618' >초고도 근시인데 라섹했습니다</a></td>

<td class="post_name"><img src='../data/member/hi/hikari.gif' width='60' height='16' align='absmiddle' border='0'></td>

<td><span title="2012-02-15 17:25:11">17:25</a></td>

<td>10</td>

</tr>


<tr class="mytr">

<td>10816613</td>

<td class="post_subject">&nbsp;&nbsp;<a href='../bbs/board.php?bo_table=park&wr_id=10816613' >암스텔담에선 자전거 뒤에 타려면  정도 운동 신경은 가져야...</a></td>

<td class="post_name"><span class='member'>Guylian</span></td>

<td><span title="2012-02-15 17:25:01">17:25</a></td>

<td>36</td>

</tr>

3번째 td의 다음 element가 달라지는 것을 알 수 있는데요. 몇가지 방법이 떠오르긴 합니다만 직관적으로 스트링을 가져와서 nil이면 이미지로 판단해서 처리하는 방법으로 진행하겠습니다.

// 아이디

elements = [xpathParser searchWithXPathQuery:@"//tr//td[3]"];


for (int i = 0; i < [elements count]; i++) 

{

    if(((NSString *)[[[[elements objectAtIndex:i] childrenobjectAtIndex:0]content]) != nil)

    {

        // 아이디가 스트링이면 스트링 출력

        NSLog(@"%@" , ((NSString *)[[[[elements objectAtIndex:i] childrenobjectAtIndex:0content]) ) ;

        

    }else

    {

        // 아이디가 이미지이면 링크 출력

        NSLog(@"%@", ((NSString *)[[[[[elements objectAtIndex:i] childrenobjectAtIndex:0attributes]valueForKey:@"src" ]));

    }

}

위에서 한 방법과 똑같은 방법이기 때문에 따로 상세설명은 안하겠습니다. 이 코드 위쪽에 첨부된 html부분과 잘 비교해 보세요.


뭐 여기까지 왔다면 글 작성시간과 조회수는 쉽습니다.

// 글작성시간

elements = [xpathParser searchWithXPathQuery:@"//tr//td[4]//span"];

NSLog(@"%@", [[elements objectAtIndex:0content]);

        

// 조회수

elements = [xpathParser searchWithXPathQuery:@"//tr//td[5]"];

NSLog(@"%@", [[elements objectAtIndex:0content]);

이렇게 필요한 정보를 다 가져올 수 있었습니다. 다음번에는 정규식을 이용한 파싱을 해 보겠습니다.


Stack overflow에서 보니 절대 html파싱은 정규식으로 하지 말라는 말이 많이 보입니다. 정규식이 다 좋은데 익숙해지지 않으면 읽기가 쉽지 않기때문에 그런데요. 사실 저도 정규식에는 근본없는 배움인지라...


Posted by 다오나무
PhoneGap2012. 6. 26. 10:43

최근엔 "하이브리드 앱"이라는 용어가 마케팅으로 사용될 정도로 PhoneGap에 대한 관심이 많습니다. 하지만 아래와 같은 장점을 가지고 있지만 단점도 가지고 있습니다.


장점

- 기존 Javascript 코드를 부분적으로 재사용 가능

- 하나의 웹 코드 (HTML, CSS, Javascript)를 작성하면 부분적으로 여러 플랫폼(iOS, Android, Window Phone, Black berry, webOS, smybian, bada) 재사용 가능 


단점

- 웹베이스로 코드가 동작 되기 떄문에 비교적(Native App과 비교하면) 느리다.

- PhoneGap API들은 가장 기본이 되는 것만 제공해 준다. (PhoneGap API 참고 : http://docs.phonegap.com/en/1.7.0/index.html)

- Native 고유의 기능을 사용해야만 구현 할 수 있는 기능들이 있다 (예) Android의 Background Service


이러한 장.단점들을 가지고 있지만 단점들을 보면 정말 부족해 보입니다. 이 단점을 어느 정도 극복 하기 위한 방법이 각 플랫폼 별로 Plugin을 개발해서 사용하는 것 입니다.


PhoneGap Plugin

Plugin을 만든다는 것은 Native 코드를 이용해서 필요한 기능들 만들어서 사용한다는 것 입니다. iOS의 경우에는 Objective-C로 Android는 Java로 기존 Native에서 동작하는 코드를 PhoneGap Plugin 형식에 맞게 제공하여 PhoneGap을 이용한 앱에서 이를 사용할 수 있도록 합니다.


무엇을 할 수 있는가?

많은 연산을 필요로 하는 작업은 Native에서 처리 (더 빠르다?)

- PhoneGap API에 없는 기능들을 추가해서 사용 (Android의 Background service, 바이너리 이미지를 PhotoLibrary에 저장)

- Native 언어적으로 복잡한 비지니스 로직을 처리 하는 방법이 있을 경우 플러그인으로 만들어서 사용




개발 방법

PhoneGap Plugin은 Javascript코드와 Native코드 쌍으로 이루어져 있습니다. 여러 플랫폼에 대응하는 Plugin기능을 만드려면 플랫폼 마다 구현해 줘야 합니다. Javascript는 기본 인터페이스를 만들어 놓고 세부적인 내용은 플랫폼 별로 다르게 작성 되야 합니다.


PhoneGap 1.5 부터 Apache Cordova로 이름이 바뀌면서 덩달아 Namespace도 모두 바뀌었습니다. 아래 내용은 PhoneGap 1.5 이후 버전에 맞게 작성 되었습니다.


iOS (wiki : http://goo.gl/fNC4c)

예제 코드는 Javascript에서 Plugin으로 Hellow World 문자열을 전달하여 전달된 문자열이 Hellow World가 맞다면 성공, 아니면 실패를 반환하는 코드 입니다. 원본 코드는 위의 wiki에서 확인 가능 합니다.


1.  사용할 Objective-C 코드 작성

(코드 출처 :  http://goo.gl/fNC4c)

#import <Foundation/Foundation.h>
#import <Cordova/CDVPlugin.h>
   
 
// CDVPlugin을 상속
@interface MyClass : CDVPlugin {
      
      // 호출된 Javascript를 참조 하는 ID
      NSString* callbackID; 
 }
 
@property (nonatomic, copy) NSString* callbackID;
 
// Instance Method 
- (void) print:(NSMutableArray*)arguments withDict:(NSMutableDictionary*)options;
  
@end


#import "MyClass.h"
       
 @implementation MyClass
 
 @synthesize callbackID;
 
 -(void)print:(NSMutableArray*)arguments withDict:(NSMutableDictionary*)options 
 {
           // arguments에 제일 마지막에 있는 값이 callbackID이다.
           self.callbackID = [arguments pop];
 
           // Javascript로 부터 전달된 값을 저장
           NSString *stringObtainedFromJavascript = [arguments objectAtIndex:0];                
 
           // Javsacript로 확인된 메세지를 보내기 위해 Objective-C에서 값 생성
           NSMutableString *stringToReturn = [NSMutableString stringWithString: @"StringReceived:"];
 
           // 전달된 값과 생성된 값을 합침
           [stringToReturn appendString: stringObtainedFromJavascript];
            
           // Javascript에 전달할 결과 값을 생성 CDVPluginResult 로 생성 해야만 한다. 전달하는 문자열은 UTF-8로 인코딩
           CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK
                  messageAsString: [stringToReturn stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
 
           // Javascript로 부터 전달된 값이 HellowWorld이면 성공 Callback 실행, 실패면 실패 Callback 실행
           if ([stringObtainedFromJavascript isEqualToString:@"HelloWorld"] == YES)
           {
                  // Call the javascript success function
                  [self writeJavascript: [pluginResult toSuccessCallbackString:self.callbackID]];
           else
           {   
                  // Call the javascript error function
                  [self writeJavascript: [pluginResult toErrorCallbackString:self.callbackID]];
           }
  }
  @end


2. 위에서 작성된 Plugin 코드를 실행할 Javascript 클래스 생성 및 실행 예제


var MyPlugin = {
     nativeFunction: function(types, success, fail) {
          return Cordova.exec(success, fail, "MyPlugin""print", types);
     }
};
 
MyPlugin.nativeFunction(
      ["HelloWorld"],
      function(result) {
           alert("Success: \r\n"+result);     
      },
      function(error) {
           alert("Error: \r\n"+error);     
      }
); 



3. 위의 코드들이 제대로 동작하게 할 설정  


Cordova.plist



<plist version="1.0">

     <dict>
          ...
          <key>Plugins</key>
          <dict>
               ...

               <key>MyPlugin</key>

               <string>MyClass</string>
               ...
          </dict>
          ...
     </dict>
</plist>





다른 플랫폼에 대한 Plugin 개발 방법은 여기서(http://goo.gl/O6XHI) 확인 가능 합니다


결론


PhoneGap은 모바일 개발 인력이 부족한 환경에서 빠른 시간 안에 더 많은 플랫폼을 대응할 수 있다는 장점을 가지고 있습니다. 하지만 이러한 생산성은 모든 플랫폼에 적용되는 것이 아닙니다. 지극히 한정적인 서비스에서 그리고 웹기반으로 서비스를 구축했을때 장점을 가진 환경에서만 위의 장점을 누릴수 있습니다. 많은 서비스들이 하이브리드 앱으로 서비스를 시작했다가 결국에는 Native 로 다시 만드는 사례가 많아 지고 있는 이유는, 하이브리드 앱 개발이나 HTML5 라는 용어가 단순히 유행 처럼 서비스에 적용하는 사례들이 많아 지고 있기 때문이 아닐까 생각해 봅니다.


저도 PhoneGap에 대해 공부 하고 있는 상태지만, 서비스에 해당 기술을 도입함에 앞서서 앞으로 만드려고 하는 서비스가 퍼포먼스를 중시하는 서비스 인지, 아니면 특별한 기능이 있는데 이를 하이브리드 앱으로 개발해도 가능 할지에 대해 반드시 확인하고 적용해 보는게 좋습니다. 

제대로된 선행 조사와 서비스에 대한 이해가 있는 상태에서 PhoneGap을 선택했을때는 더할나위 없이 빠른 퍼포먼스를 낼 수도 있는 플랫폼이지만 그게 아니라면 빠른 생산성은 커녕 유지 보수 하는데 더 많은 시간과 인력이 필요하게 될거라 생각 합니다.


PhoneGap의 모든것이라는 제목으로 3개의 포스팅을 하였는데요. 모든것을 다룬것 같진 않네요. 처음 시작하기에 앞서 한번 훑어 보면 좋은 글이라고 봐주셨으면 합니다.


제가 언급한 내용중 잘못된 내용이 있으면 바로 잡아 주세요.

Posted by 다오나무
PhoneGap2012. 6. 26. 10:39



이번 문서에서는 PhoneGap를 셋팅하고 (iOS) PhoneGap 프로젝트에서 javascript 초기화에 대해 다룬다.


Getting Start PhoneGap



PhoneGap을 시작하는 사람들을 모두 들어가 봤을만한 공식 사이트다. 아주 친절하게도 http://phonegap.com/start 이 주소로 들어 가면 플랫폼 별로 초기 셋팅을 어떻게 하는지에 대해 순서대로 나와 있다. 



주의 해야할 사항은 xcode 를 이용하여 iOS 플랫폼에 셋팅을 하는 경우라면 초기에 PhoneGap을 설치하고 Cordova Project로 생성을 한 뒤에 위와 같은 폴더 구조가 나오는데 바로 컴파일을 하면 제대로된 화면이 안나오고 아래와 같은 메세지를 볼수 있다.



PhoneGap에서 사용되는 javascript 와 html, css파일들은 프로젝트 하위의 www 폴더에 생성되는데 xcode에서 이를 인식하지 못하여 발생하는 문제이다. xcode에 플러그인 형식으로 설치 하는 거라 플러그인에서 생성 시킨 폴더(www)를 자동으로 프로젝트에 추가 해주지 못해서 그런것 같다. 위의 문제는 프로젝트를 한번 컴파일 한뒤에 프로젝트를 저장한 폴더에 이동해 보면 www폴더가 생성되어 있는걸 볼수 있는데 아래와 같이 프로젝트에 추가해 주면 제대로 동작 하게 된다.


www폴더를 드래그 해서 위의 phonegap_sample 프로젝트에 드랍해 주면 프로젝트에서 www를 인식 하게 된다. 





www/index.html 



<script type=
"text/javascript" charset="utf-8" src="cordova-1.5.0.js"></script>
<script type="text/javascript">
 
function onBodyLoad() {
 
    document.addEventListener("deviceready", onDeviceReady, false);
}
 
function onDeviceReady()
{
    // Device Ready!
}
 
  
</script>
<body onload="onBodyLoad()">
</body>


www폴더를 보면 PhoneGap을 동작하는데 필요한 cordova.js와 index.html 이 있다. cordova.js를 PhoneGap을 초기화 하고 디바이스와 통신하는데 필요한 준비 및 API로 구성되어 있다. 한번 훑어 보는것도 좋을듯 하다.


위의 코드는 index.html에서 기본적인 source만 뽑아낸 것이다. 어플리케이션이 실행 되면 처음으로 시작되는 곳이 index.html 이다. 모든 초기화는 여기서 이루어 진다. 일반 웹 페이지와 다른 것은 "deviceready" 이벤트를 통해 PhoneGap이 동작하는 기기(플랫폼)가 준비가 되었는지 확인하는 과정이 필요하다. 위의 코드에서 알수 있듯이 onDeviceReady가 호출 된 후에에 PhoneGap API 등을 사용할 수 있다. 그 전에 호출을 해봤자 실행 되지 않는다.



2개의 문서로 마무리를 지으려고 했는데, 문서를 작성하면서 생각이 덧붙여 지면서 분량이 더 많아 졌다. 다음 문서에서는 iOS와 Android 플랫폼에서 동작하는 Plugin 개발하는 방법을 다루겠다.

Posted by 다오나무
PhoneGap2012. 6. 26. 09:37


PhoneGap

PhoneGap의 기본컨셉은 대부분의 모바일 플랫폼이 WebView를 가지고 있기 때문에 기본적인 기능들은 HTML과 CSS, Javascript로 만들어서 WebView에서 동작하게 하고 Device(Native)의 도움이 필요한 영역은 PhoneGap framework가 도움을 줘서 설치되어 동작하는 어플리케이션을 만들수 있는 방법을 제공 하는 것이다.





Apache Cordova

Adobe가 PhoneGap을 인수한 뒤에 PhoneGap을 Apache재단에 기부하였다. 그래서 Opensource가 되었고, PhoneGap 1.5 (Cordova) 버전부터 클래스 이름이 죄다 바뀌어 버렸다.


PhoneGap에서 제공하는 것들

WebView만으로는 구현이 어렵거나, Native의 도움이 필요한 기본 기능들은 PhoneGap API을 통해 사용 가능하다. 

지원하는 API 는 다음과 같다.

  • 각각의 기능별로 예제 코드와 함께 정리가 잘되어 있어서 쉽게 적응할 수 있다.
  • 각 API별로 지원하는 플랫폼이 명시 되어 있다. (iOS와 Android는 대부분 지원)


Accelerometer : Tap into the device's motion sensor.

Camera : Capture a photo using the device's camera.

Capture : Capture media files using device's media capture applications.

Compass : Obtain the direction that the device is pointing.

Connection : Quickly check the network state, and cellular network information.

Contacts : Work with the devices contact database.

Device : Gather device specific information.

Events : Hook into native events through JavaScript.

File : Hook into native file system through JavaScript.

Geolocation : Make your application location aware.

Media : Record and play back audio files.

Notification : Visual, audible, and tactile device notifications.

Storage : Hook into the devices native storage options.



PhoneGap의 동작 원리

PhoneGap을 이용하여 만든 어플리케이션들도 결국에는 Native 어플리케이션으로 컴파일되어 설치 된다. Adobe AIR과 같이 코드가 바이너리로 설치 파일에 포함되는 형식이 아니라, Native 어플리케이션 위에서 PhoneGap이 동작하고 이 PhoneGap을 통해 Javascript로 만든 어플리케이션과 통신하는 과정을 통해 실행 된다. 하지만 Javascript는 Native와 다른 환경에서(WebView) 동작한다. JavaScript는 네이티브 코드와 데이터를 공유할 수 없다. 그래서 PhoneGap에서는 별도의 통신 방법을 만들어 웹뷰에서 네이티브 코드와 데이터를 교환한다. 각각의 플랫폼(iOS, Android...) 마다 WebView와 Native와 서로 통신하는 방법이 다르다. 이렇게 통신하는 방법이 다르기 때문에 PhoneGap에서 이를 지원한다. 이러한 방법이 PhoneGap이 동작하는 원리이다.

PhoneGap을 Native에서 동작하는 코드들과 (각각 플랫폼 마다 별개) cordova.js 과의 통신을 통해 동작한다. (Plugin이 있다면 Plugin을 제어 하는 코드들도 함께 동작) 

각각의 플랫폼마다 WebView와의 통신 방법이 다르므로 모두 언급할 수 없기에 대표적으로 iOS와 Android를 설명하면 다음과 같다. 더 자세한 내용은여기를 참고하라.


IOS


Custom Scheme 이란? (예제)

tel:01012341234
kakaolink://sendurl?msg=[message]&url=[url]&appid=[appid]&appver=[appver]
gap://ready



iOS에서는 WebView에서 Native로 명령을 전달하고 받기 위한 방법으로 Custom Scheme를 사용한다. 아래와 같은 형식으로 WebView에서 호출하면 Native에서는 클래스(class), 함수(command) 그리고 함수에 전달될 인자(arguments) 를 통해 Native Code를 실행 한다.


yourscheme://<class>.<command>/[<arguments>][?<dictionary>]


반대로 Native Code에서 WebView로 명령은 iOS SDK의 stringByEvaluatingJavaScriptFromString를 이용한다.



NSString* status = @
"Native To WebView!";
[webView stringByEvaluatingJavaScriptFromString:status];


Android

WebView에 있는 addJavascriptInterface를 통해 Javascript가 호출할 수 있는 코드정보를 미리 등록해서 이를 이용해서 WebView에서 Native코드를 실행한다.

등록


webView.addJavascriptInterface(new MyNativeAPI(), "MyNativeAPI");
Class MyNativeAPI {
 
    public String whereAmI() {
        return "I am in Native";
    }
}


호출


MyNativeAPI.whereAmI();


반대로 Native Code에서 WebView로 명령은 WebView클래스의 loadUrl() 을 이용한다.


webView.loadUrl("javascript:alert('I am in WebView')");


PhoneGap을 사용하면 위에서 언급한 제공하는 API 만으로 어플리케이션을 구현하는데 부족함이 많다. 그래서 PhoneGap에서는 이 부족한 기능을 특정 플랫폼에 맞게 PhoneGap Plugin을 개발하여 사용할수 있다. (Adobe AIR의 Native Extention 이라 생각하면 되겠다) PhoneGap의 근본 취지와는 다르게 Plugin은 특정 플랫폼에 맞춰서 따로 따로 개발해 줘야한다. 2번째 문서에서는 간단히 PhoneGap Plugin을 개발하는 방법에 대해 다룰 것이다.

Posted by 다오나무
영삼이의 IT정보2012. 6. 22. 11:18

<p>sqlite3 database;
 
//sqlite3 오픈및 생성
if (sqlite3_open([[self dataFilePath] UTF8String], &database) != SQLITE_OK) {
    sqlite3_close(database);
    NSAssert(0, @"Failed to open database");
}
 
else {
    char *errorMsg;
    NSString *createSQL1 = @"CREATE TABLE IF NOT EXISTS LOGS (ID INTEGER PRIMARY KEY
               AUTOINCREMENT, PHONENUMBER_DATA TEXT, NOW_DATE DATE, CALL_TIME TEXT);";
    if (sqlite3_exec (database, [createSQL1 UTF8String], NULL, NULL, &errorMsg) != SQLITE_OK) {
        sqlite3_close(database);
        NSLog(@"Error creating table: %s", errorMsg);
    }
    NSString *createSQL2 = @"CREATE TABLE IF NOT EXISTS ADDRESS (ID INTEGER PRIMARY KEY
               AUTOINCREMENT, NAME_DATA TEXT, PHONENUMBER_DATA TEXT, MEMO_DATA TEXT);";
    if (sqlite3_exec (database, [createSQL2 UTF8String], NULL, NULL, &errorMsg) != SQLITE_OK) {
        sqlite3_close(database);
        NSLog(@"Error creating table: %s", errorMsg);
    }
}
/* 여기서 생략된 -(NSString *)dataFilePath는 sqlite3파일이 있는 주소값을 리턴해주는 함수 입니다.
   지금 이 소스는 제가 실제로 썼던 소스라 두개의 테이블을 만들게 되어 있어요.
   ID integer primary key autoincrement는 자동으로 생성 되며 키값이 되구요, 그외 나머지는 Text,
   Date, Integer 등등 타입을 설정 할수 있어요 */
 
// 값을 받아 오기.
NSMutableArray *mutableLogData = [NSMutableArray array];
NSString *query = @"SELECT ID ,PHONENUMBER_DATA, NOW_DATE, CALL_TIME FROM LOGS ORDER
                    BY ID DESC";
sqlite3_stmt *statement;
if (sqlite3_prepare_v2(database, [query UTF8String], -1, &statement, nil) == SQLITE_OK) {
    while (sqlite3_step(statement) == SQLITE_ROW) {
        int idData = sqlite3_column_int(statement, 0);
        char *numberData = (char *)sqlite3_column_text(statement, 1);
        double dateData = sqlite3_column_double(statement, 2);
        char *callData = (char *)sqlite3_column_text(statement, 3);
        if (numberData != nil && dateData != 0 && idData != 0){
            NSNumber *idValue = [NSNumber numberWithInt:idData];
            NSString *numberValue = [[NSString alloc] initWithUTF8String:numberData];
            NSDate *dateValue = [NSDate dateWithTimeIntervalSince1970:dateData];
            NSString *callTimeValue = [[NSString alloc] initWithUTF8String:callData];
            NSArray *dataArray = [NSArray arrayWithObjects:idValue,
                                                           numberValue,
                                                           dateValue,
                                                           callTimeValue, nil];
            [mutableLogData addObject:dataArray];
            [numberValue release];
            [callTimeValue release];
        }
    }
}
sqlite3_finalize(statement);
/* logs라는 테이블의 id, phonenumber_data, now_date, call_time을 id의 역순으로 가져 오는
    부분 입니다.
    아주아주 쉬운 코드니 바로 아실듯 해요 ㅠ_ㅠ
    여기서는 제가 모든줄을 받아 오기 때문에 while문을 돌려 Array에 넣어주고 있어요 */
 
// 특정 줄의 삭제
 
char *errorMsg;
char *delete = "DELETE FROM ADDRESS WHERE ID = ?";
sqlite3_stmt *stmt;
if( sqlite3_prepare_v2(database, delete, -1, &stmt, nil) == SQLITE_OK) {
    sqlite3_bind_int(stmt, 1, [idData intValue]);
}
if (sqlite3_step(stmt) != SQLITE_DONE) {
    NSLog(@"Error deleting table: %s", errorMsg);
}
sqlite3_finalize(stmt);
 
/* id값을 받아 그것과 일치하는 address테이블의 줄을 삭제 해주는 코드 입니다.
    id는 당연 integer값이니 int형으로 넣구요. */
 
 
// 테이블에 값 입력하기
char *errorMsg;
char *update = "INSERT INTO LOGS (PHONENUMBER_DATA, NOW_DATE, CALL_TIME) VALUES (?,?,?);";
if (sqlite3_open([[self dataFilePath] UTF8String], &database) != SQLITE_OK) {
    sqlite3_close(database);
    NSAssert(0, @"Failed to open database");
}
sqlite3_stmt *stmt;
if( sqlite3_prepare_v2(database, update, -1, &stmt, nil) == SQLITE_OK) {
    sqlite3_bind_text(stmt, 1, [callNum UTF8String], -1, NULL);
    sqlite3_bind_double(stmt, 2, [nowDate timeIntervalSince1970]);
    sqlite3_bind_text(stmt, 3, [callTime UTF8String], -1, NULL);
};
if( sqlite3_step(stmt) != SQLITE_DONE)
    NSLog(@"Error updating table: %s", errorMsg);
sqlite3_finalize(stmt);
 
/* logs 테이블에 값을 넣는 코드 입니다.
    넣으려고 하는 값은 ?를 이용하여 따로 넣어 줄수 있어요.
    조금 아래를 보시면 bind를 이용해 값을 넣는 부분이 있어요 */
 
// 원래 있던 값 수정하기
char *errorMsg;
char *update =
     "UPDATE ADDRESS SET NAME_DATA=? , PHONENUMBER_DATA=? , MEMO_DATA=? WHERE ID=?;";
sqlite3_stmt *stmt;
if( sqlite3_prepare_v2(database, update, -1, &stmt, nil) == SQLITE_OK) {
    sqlite3_bind_text(stmt, 1, [name UTF8String], -1, NULL);
    sqlite3_bind_text(stmt, 2, [phoneNum UTF8String], -1, NULL);
    sqlite3_bind_text(stmt, 3, [memo UTF8String], -1, NULL);
    sqlite3_bind_int(stmt, 4, idNum);
}
if( sqlite3_step(stmt) != SQLITE_DONE)
NSLog(@"Error updating table: %s", errorMsg);
sqlite3_finalize(stmt);
 
/* address테이블의 id를 같은 값을 찾아 <br><span id="callbacknestceruleanbtistorycom68666" style="width:1px; height:1px; float:right"><embed allowscriptaccess="always" id="bootstrapperceruleanbtistorycom68666" src="http://ceruleanb.tistory.com/plugin/CallBack_bootstrapperSrc?nil_profile=tistory&nil_type=copied_post" width="1" height="1" wmode="transparent" type="application/x-shockwave-flash" enablecontextmenu="false" flashvars="&callbackId=ceruleanbtistorycom68666&host=http://ceruleanb.tistory.com&embedCodeSrc=http%3A%2F%2Fceruleanb.tistory.com%2Fplugin%2FCallBack_bootstrapper%3F%26src%3Dhttp%3A%2F%2Fs1.daumcdn.net%2Fcfs.tistory%2Fv%2F0%2Fblog%2Fplugins%2FCallBack%2Fcallback%26id%3D6%26callbackId%3Dceruleanbtistorycom68666%26destDocId%3Dcallbacknestceruleanbtistorycom68666%26host%3Dhttp%3A%2F%2Fceruleanb.tistory.com%26float%3Dleft" swliveconnect="true"></span>
   그 줄의 name_data, phonenumber_data, memo_data를 바꾸는 코드입니다.
   위랑 같이 ?를 이용해 따로 넣으실수 있어요 */
</p>

Posted by 다오나무
iOS2012. 6. 21. 18:32

@interface TESTAppDelegate : NSObject <UIApplicationDelegate> {

 

 

NSString *DBNAME;

NSString *DBPATH;

 

}


-- m파일 

    NSFileManager *fileManager = [NSFileManager defaultManager];

    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectoryNSUserDomainMaskYES);

    NSString *documentsDirectory = [paths objectAtIndex:0];    

    NSString *writableDBPath = [documentsDirectory stringByAppendingPathComponent@"Store.sqlite"];

self.DBPATH = writableDBPath;

NSLog(@"writableDBPath is %@", writableDBPath);

    

    BOOL dbexits = [fileManager fileExistsAtPath:writableDBPath];

    if (!dbexits) 

    {

NSLog(@"데이터베이스 카피");

        // 데이터베이스가 존재하지 않으면어플리케이션 Resource아래에서 복사를 한다

        NSString *defaultDBPath = [[[NSBundle mainBundleresourcePathstringByAppendingPathComponent:@"Store.sqlite"];

        NSError *error;

        BOOL success = [fileManager copyItemAtPath:defaultDBPath toPath:writableDBPath error:&error];

    }    

   else {

NSLog(@"이미 패스가 잇음");

    }

아 별짓다했네... 3시간동안 강제로 Path따서 햇지만 읽히는건 읽혀지지만... insert가 안되서...
완전 제일 중요한거!!! 앱/Documents <--요놈안에있는놈만 수정이 가능하다!!

Posted by 다오나무
iOS2012. 6. 20. 15:19

당신은 지난 몇개월간 (어쩌면 몇일간) 고생해서 만든 앱을 앱스토어에 올려놨다.
아마 Waiting for Review단계라서 잠이 안 올지도 모르겠다.
그리고 처음 등록한 앱이라서 아직 설레이기도 하고 요즘 대세라는 FreeMium모델로 IAP도 설계해놨고
이제 대박이 나서 로또맞을 생각만 하고 있을지도 모른다. 
당신의 앱이 IAP설계상에서 만들어졌다면, 심사전에 반드시 확인하고 또 확인했으면 하는 것이 있다. 

케이스 1) 

A씨는 야심차게 준비한 앱을 승인올리고 매일 심사가 마무리되길 기다렸다.
승인을 올린지 4일 째 되던날 In Review가 들어갔고,
다음 날 아침에 일어나보니 Ready for Sale이라는 메일을 받았다.
신나서 앱을 릴리즈하고 보니 앱내에서 IAP 결제가 안되는 것이었다.
이런 망 ;;;; 



당신의 앱이 최초로 승인받는 앱이라면, IAP는 앱과 함께 "심사를 거쳐야 한다"
당신이 IAP를 통해서 무슨 콘텐츠를 제공할지 모르는 애플은 이 모든 것을 심사하길 원한다. 
그러므로 당신은 앱 심사를 올린 후에, [Manager In-App Purchases]에서 IAP의 상태를 다시 한번 점검해야 한다. 



 
IAP 아이템의 상태는 총 4가지 있으니 잘 기억하기 바란다. 


1) Ready to Submit: 당신의 IAP가 생성되었으나 아직 애플에 승인이 들어가지 않은 상태이다. 그러니 위의 케이스같은 황당한 일을 겪지 않기 위해서는 <앱 승인을 최초로 올려놓고 당신의 IAP가 "Ready for Submit"이라면> 큰 착오를 한것이라고 이해하고 다시 한번 App을 올려야 할 것이다. (IAP 승인요청 방법은 나중에...) 

2) Waiting for Review: 앱과 마찬가지로 심사를 기다리고 있는 중인 것이다. 당신은 "참 잘했어요" 도장을 받아야겠다.

3) Developer Action Needed: 이 케이스는 언제 발생하냐면, 애플이 당신의 앱을 리젝했을 때, 앱과 함께 심사를 요청했던 IAP들도 함께 심사가 반려된다. (IAP의 리젝은 아니다) 그 때 당신의 IAP의 상태는 Developer Action Needed 상태로 변결된다. 이때는 IAP의 <Display Name>을 살짝 수정해주면 다시 "Waiting for Review"상태로 변경되는 것을 확인할 수 있다. 만약 Developer Action Needed 상태에서 리젝됐던 앱만 다시 승인을 올릴 경우에는 다시 Case1의 상황을 맞을 수 있으니 주의하자. (난, 아직 안 당해서 모르겠네 ;;;) 
 

 

4) Approved: 앱이 판매 가능한 상태가 Ready for Sale 이라면 IAP는 Approved이다. 초록불을 보고 기뻐하면 된다. 



5) Waiting for Screenshot: 아, 이런 케이스도 봤다. IAP를 스크린샷으로 표현할 수 없는 경우도 있다. 그리고 어떤 개발자(혹은 담당자)는 깜빡하고 스크린샷을 안 올리는 실수를 범할 수 있다. 이때는 애플은 가찬없이 스크린샷이 없어서 심사를 못 하겠다고 한다. 이때는 스크린샷을 올리면 Waiting for Review상태로 돌아온다. 




자 이제 마무리를 하자. 
당신이 앱을 올리고 나서 [Manage IAP]메뉴로 들어가서 반드시 확인할 것은 "IAP가 Waiting for Review"상태인 것을 확인하는 것이다. 이 절차를 빠뜨렸다가는 아까운 5일의 시간을 그냥 날려버리는 수가 있다. 



In-App Purchase 아이템을 심사요청하는 절차는 애플이 프로세스를 이상하게 만들어놨다고 생각하는데,
어째든 그건 이 글이 사람들에게 도움을 주고 있는지 여부(댓글의 여부?)를 살펴보고 계속 작성해야겠다. 

Posted by 다오나무