Contenu connexe Similaire à H3 2011 하이브리드 앱 아키텍쳐 및 개발방법 Similaire à H3 2011 하이브리드 앱 아키텍쳐 및 개발방법 (20) H3 2011 하이브리드 앱 아키텍쳐 및 개발방법4. 하이브리드앱아키텍쳐구성요소
네이티브 하이브리드 웹
UI툴킷 웹UI툴킷 자바스크립트프레임웍/라이브러리
웹표준기술
프레임웍
HTML5 CSS 자바스크립트
네이티브라이브러리 비표준DeviceAPIs 표준DeviceAPIs
개발도구 웹브라우져“엔진” 웹브라우져“앱”
플랫폼SDK
안드로이드
iOSSDK 윈폰7SDK …⋯
SDK
4
9. 네이티브와웹의결합
Flash/Flex?
Active-X?
JavaApplet? 문제는....
다리!!
Native-Web
네이티브 Bridge 웹
9
10. 네이티브와웹의결합
WebView
WebViewClientWebChromeClient
loadUrl
addJavascriptInterface
UIWebView
UIWebViewDelegate
loadRequest
stringByEvaluatingJavascriptFromString
10
11. 네이티브와웹의결합
자바스크립트
캐시
그래봤자,
문자열~
URL 쿠키
어차피,
꼼수
그리고...
HTTP!
그림 출처: http://petticoatsandpistols.com/2010/05/12/
11
13. 네이티브지향하이브리드앱
사실상네이티브,
웹은거들뿐...
· 제한적이고직관적인네이티브와웹의결합
· 웹브라우져as-aUI컴포넌트
· 도움말,앱/개발사소개,공지사항/새소식...
· 웹기반사용자인증(OAuth)...
13
16. 예제코드(안드로이드)
웹서버 컨텐츠 불러오기
public class NoticeActivity extends Activity {
...
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
WebView webView = (WebView)findViewById(R.id.webView);
webView.getSettings().setJavaScriptEnabled(true);
webView.setWebChromeClient(new WebChromeClient());
...
webView.loadUrl(“http://m.pudding.kr/pud/mNotice.kth”);
...
}
...
}
앱에 포함된 정적 컨텐츠 불러오기
public class HelpActivity extends Activity {
...
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
WebView webView = (WebView)findViewById(R.id.webView);
webView.getSettings().setJavaScriptEnabled(true);
webView.setWebChromeClient(new WebChromeClient());
...
webView.loadUrl(“file:///android_asset/www/
help.html”);
...
}
...
}
16
17. 예제코드(iOS)
웹 서버 컨텐츠 불러오기
@interface NoticeViewController : UIViewController {
IBOutlet UIWebView *webView;
...
@end
@implementation HelpViewController
...
- (void)viewDidLoad {
...
NSURL *url = [NSURL URLWithString:@”http://m.pudding.kr/pud/mNotice.kth”];
NSURLRequest *requestObj = [NSURLRequest requestWithURL:url];
[webView loadRequest:requestObj];
...
}
...
@end 앱에 포함된 정적 컨텐츠 불러오기
@interface HelpViewController : UIViewController {
IBOutlet UIWebView *webView;
...
@end
@implementation HelpViewController
...
- (void)viewDidLoad {
...
NSString *bundlePath = [[NSBundle mainBundle] bundlePath];
NSString *path = [bundlePath stringByAppendingPathComponent:@”/www/help.html”];
NSURL *url = [NSURL fileURLWithPath:path];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[webView loadRequest:request];
...
}
...
@end
17
21. 예제코드(안드로이드)
링크 클릭 가로채기
...
WebView webView = (WebView)findViewById(R.id.webView);
webView.getSettings().setJavaScriptEnabled(true);
webView.setWebChromeClient(new WebChromeClient());
webView.setWebViewClient(new WebViewClient() {
public boolean shouldOverrideUrlLoading(WebView webView, String url) {
if(!url.startsWith(“http://m.pudding.kr/pud/”) {
new AndroidDialog.Builder(NoticeActivity.this)
.setMessage(“딴데로 갈라구?? -_-+”)
.setPositiveButton(“아니... 여기 있을께 ㅠㅠ”, new DialogInterface.OnClickListener() {
dialog.dismiss();
})
.setNegativeButton(“갈꼬얌!”, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int witch) {
dialog.dismiss();
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
NoticeActivity.this.startActivity(intent);
}
}).show();
return false;
} else if ...
...이러쿵 저러쿵...
} else if ...
...어쩌구 저쩌구...
} else if ...
...구시렁 구시렁...
}
view.loadUrl(url);
return true;
}
});
webView.loadUrl(“http://m.pudding.kr/pud/mNotice.kth”);
...
21
22. 예제코드(iOS)
링크 클릭 가로채기
@interface NoticeViewController : UIViewControllerUIWebViewDelegate, UIAlertViewDelegate {
IBOutlet UIWebView *webView;
NSString *externalUrl;
...
@implementation HelpViewController
...
- (void)viewDidLoad {
NSURL *requestUrl = [NSURL URLWithString:@”http://m.pudding.kr/pud/mNotice.kth”];
[webView loadRequest:[NSURLRequest requestWithURL:requestUrl]];
[webView setDelegate:self]
}
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request
navigationType:(UIWebViewNavigationType)navigationType {
NSString *url = [[request URL] absoluteString];
if(![url hasPrefix:@”http://m.pudding.kr/pud/mNotice.kth”]) {
self.externalUrl = url;
UIAlertView *alertView = [UIAlertView alloc] initWithTitle:nil
message:@”딴데로 갈라구?? -_-+” delegate:self
cancelButtonTitle:@”아니... 여기 있을께 ㅠㅠ“ otherButtonTitles:@”갈꼬얌!”, nil];
[alertView show];
[alertView release];
return NO;
} else if ...
...이러쿵 저러쿵...
} else if ...
...어쩌구 저쩌구...
} else if ...
...구시렁 구시렁...
}
return YES;
}
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
if(buttonIndex == YES self.externalUrl) {
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:self.externalUrl];
}
}
...
22
23. 예제코드(안드로이드)
URL을 이용한 네이티브와 웹의 통신::자바스크립트
function getFieldValue(fieldId) {
var fieldValue = document.getElementById(fieldId).value;
location.href = ‘custom://getFieldValue?fieldId=’ + fieldId + ‘fieldValue=’ + fieldValue;
}
function setFieldValue(fieldId, fieldValue) {
document.getElementById(fieldId).value = fieldValue;
}
URL을 이용한 네이티브와 웹의 통신
webView.loadUrl(“javascrpt:getFieldValue(‘userName’)”); // 결과는 나중에... 비동기!! -_-;
...
webView.loadUrl(“javascrpt:setFieldValue(‘userName’, ‘“ + userName + “‘“);
...
webView.setWebViewClient(new WebViewClient() {
public boolean shouldOverrideUrlLoading(WebView webView, String url) {
if(!url.startsWith(“custom://getFieldValue”) {
Uri uri = Uri.parse(url);
String fieldId = uri.getQueryParameter(“fieldId”);
String fieldValue = uri.getQueryParameter(“fieldValue”);
if(fieldId.equals(“userName”)) {
userName = fieldValue; // 결과가 도착했다! 이제 어떡하지? 비동기!! OTL
} else if ...
} else if ...
} else if ... // 나는 엘시프가 씨러요! ㅠㅠ
}
return false;
} else if ...
} else if ...
} else if ... // 나는 엘시프가 씨러요! ㅠㅠ
}
view.loadUrl(url);
return true;
}
});
...
23
24. 예제코드(iOS)
URL을 이용한 네이티브와 웹의 통신
NSString *script = [NSString stringWithFormat:@“getFieldValue(‘%@’)”, fieldId];
[webView stringByEvaluatingJavaScriptString:script];
...
NSString *script = [NSString stringWithFormat:@“setFieldValue(‘%@’, ‘%@‘)“, fieldId, fieldValue];
[webView stringByEvaluatingJavaScriptString:script];
...
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request
navigationType:(UIWebViewNavigationType)navigationType {
NSString *url = [[request URL] absoluteString];
if(![url hasPrefix:@”custom://getFieldName”]) {
NSDictionary *params = [HttUtils decodeQueryString:[[request URL] query]];
NSString *fieldId = [params objectForKey:@”fieldId”];
NSString *fieldValue = [params objectForKey:@”fieldValue”];
if(fieldId isEqualToString:@”userName”) {
self.userName = fieldValue;
} else if ...
} else if ...
} else if ... // 나는 엘시프가 씨러요! ㅠㅠ
}
[paramArray release];
return NO;
} else if ...
} else if ...
} else if ... // 나는 엘시프가 씨러요! ㅠㅠ
}
return YES;
}
...
24
25. 웹지향하이브리드앱
사실상웹,
네이티브는거들뿐...
· 광범위하지만일관성있는네이티브와웹의결합
· 클라이언트사이드“웹앱”
· 기존웹서버+RESTfulAPI서버
· 기본적인웹컨텐츠는앱에포함
25
29. “푸딩얼굴인식앱”소개
푸딩얼굴인식앱은...
· 600만+다운로드!
· @iolothebard와@seti222
· 5500+줄의자바스립트
· 2700+줄의CSS
· 200+줄의네이티브코드
· 앱스프레소0.9+내부개발버전
29
30. 단일페이지인터페이스
index.html #pageId
pageId.css
pageId.js
Active
Page
show/hide
30
31. 단일페이지인터페이스
웹앱도MVC가필요해!
자바스크립트가컨트롤러!
· 웹서버도없는데...페이지이동은왜?!
· (GUI)애플리케이션스타일의“상태”관리
· 빠른화면전환화면전환효과
· 체감성능UP!
· 메모리사용량UP!
· 그런데...공동작업은어떻게?-_-;
31
32. 단일페이지인터페이스
HTML마크업: section#faceFoundPage
section id=”faceFoundPage” class=”jj-page”
header
button class=”jj-left jj-back”span data-nls=”common.back”이전/span/button
button class=”jj-right jj-home”span data-nls=”common.home”홈/span/button
h1span data-nls=”faceFoundPage.title”얼굴인식 결과/span/h1
/header
article CSS스타일:faceFoundPage.css
... #faceFoundPage {
/article background-color:rgb(255,255,255);
footer }
... #faceFoundPage article, #faceFoundPage footer {
/footer background-color:rgb(138,135,136);
/section }
...
자바스크립트:faceFoundPage.js
var jj.ui = jj.require(‘jj.ui’);
export.FaceFoundPage = new jj.defclass(jj.ui.Page, {
onInit: function() {
$(this.pageNode).bind(‘onpagebeforeshow’, function() { … });
$(this.pageNode).bind(‘onpageaftershow’, function() { … });
$(this.pageNode).bind(‘onpagebeforehide’, function() { … });
$(this.pageNode).bind(‘onpageaftershow’, function() { … });
...
}, 자바스크립트를 이용한 화면 전환
... // 화면을 수동으로 초기화
}); var faceFoundPage = new FaceFoundPage(
$(‘#faceFoundPage’), // 화면을 구성하는 DOM 노드
facePickerPage, // 화면내의 .jj-back 노드를 클릭할 때 전환될 페이지
mainPage); // 화면내의 .jj-home 노드를 클릭할 때 전환될 페이지
// 화면 표시에 필요한 상태 정보를 전달
faceFoundPag.setFaceInfo(faceInfo);
// 현재 화면이 왼쪽으로 사라지면서, 새 화면이 오른쪽에서(slideleft) 나타남
faceFoundPage.show(‘slideleft’);
// 현재 화면을 유지한 채, 새 화면을 아래에서 위로(slideup) 나타남(modal)
//faceFoundPage.show(‘slideup’, true);
32
33. “Placeholder”HTMLMarkup
자바스크립트 API서버 웹서버
HTML
로컬
캐시 스토리지 캐시 앱
StaticHTMLFragment
Placeholder Data Template
+
이름 클래스 레벨 ★ ♥ 이름 클래스 레벨
iolo bard 만렙 iolo bard 만렙 foreach
장동수 개발자 쪼렙 장동수 개발자 쪼렙 ★ ♥
... ... ... ... ... ... end
33
34. “Placeholder”HTMLMarkup
웹앱도MVC가필요해!
뷰와모델의분리
· HTML마크업을위한“변수”
· 웹서버개발에서클라이언트에적용
smarty,...)을웹
널리쓰이는템플릿(velocity,
· 서버부하는DOWN!
· 데이터전송량도DOWN!
· 캐시히트율은UP!
34
35. “Placeholder”HTMLMarkup
div id=”faceFound_starTemplate” class=”jj-template”
div class=”face” data-fastclick=”true”/div
div class=”rank”/div
p class=”rate”span class=”rateText”/spansmall%/small/p
p class=”nameText” data-fastclick=”true”/p
p class=”info”/p
p class=”descriptionText”/p
/div
div id=”faceFound_star_wrapper”
div id=”faceFound_star_scroller”
ul
li class=”nomatch”
div id=”faceFound_star_nomatch”
h5 data-nls=”0ah002”닮은 연예인을 찾지 못했습니다./h5
p data-nls=”0ah003”얼굴이 가까이 나온br /정면 사진으로 다시 시도해 보세요./p
div
button id=”faceFound_retryBtn” class=”y” data-fastclick=”true”
span data-nls=”0ah005”다시 찾기/span
/button
/div
/div
/li
li
div id=”faceFound_star1” class=”jj-placeholder”
data-template=”#faceFound_starTemplate”/div
/li
li
div id=”faceFound_star2” class=”jj-placeholder” data-template=”#faceFound_starTemplate”/div
div id=”faceFound_star3” class=”jj-placeholder”/div
/li
li
div id=”faceFound_star4” class=”jj-placeholder” data-template=”#faceFound_starTemplate”/div
div id=”faceFound_star5” class=”jj-placeholder” data-template=”#faceFound_starTemplate”/div
/li
/ul
/div!-- star_scroller --
/div!-- star_wrapper --
35
36. “Marker”CSSClass
웹앱도MVC가필요해!
뷰와컨트롤러의분리
· CSS스타일시트를위한“조건문”
· 프론트엔드UI개발자와자바스크립트개발자의약속
· 화면의동적인변화를자바스크립트없이확인제어
· HTML/CSS는“쬐끔”복잡해지고...-_-;
· 자바스크립트는“쬐끔”단순해지고...-_-;
· 함께일하기는더좋아지고~^O^
36
37. “Marker”CSSClass
스타일시트
/* 일치하는 연예인이 하나라도 있으면 안내문을 표시하지 않는다 */
#faceFoundPage .nomatch {
display:none;
}
/* 일치하는 연예인이 없으면 그에 따른 안내문을 표시한다 */
#faceFoundPage.nomatch .nomatch {
display:block;
}
/* 일치하는 연예인이 없으면 carousel 페이지 인디케이터를 숨긴다 */
#faceFoundPage.nomatch ul li {
display:none;
}
/* 일치하는 연예인이 없으면 하단의 공유 버튼들을 비활성화 */
#faceFoundPage.nomatch footer button {
opacity:0.5;
}
/* 일치하는 연예인 숫자에 따라 carousel 영역(iscroll)의 너비를 조절한다
#faceFoundPage.nomatch #faceFound_star_scroller,
#faceFoundPage.match_1 #faceFound_star_scroller {
width:320px;/*320*1pages*/
}
#faceFoundPage.match_2 #faceFound_star_scroller,
#faceFoundPage.match_3 #faceFound_star_scroller {
width:640px;/*320*2pages*/
}
#faceFoundPage.match_4 #faceFound_star_scroller,
#faceFoundPage.match_5 #faceFound_star_scroller {
width:960px;/*320x3pages*/
}
자바스크립트
if(matchingCelebs 1) {
$(‘#faceFoundPage’).addClass(‘nomatch’);
} else {
$(‘#faceFoundPage’).removeClass(‘nomatch’).addClass(‘match_’ + matchingCelebs);
}
37
38. 단말해상도별최적화
ResponsiveWebDesign?
지금은곤란하니,기다려달라~
· HTML태그에마커CSS클래스추가/활용
· 기본적으로해상도에자유로운프론트엔드UI개발
· 널리쓰이는해상도는꼼꼼하게“미세”조정
· 특이한해상도는최소한의“미세”조정
· 이한몸희생해서...모두가행복할수있다면...ㅠㅠ
38
39. “Marker”CSSClass
단말 플랫폼/해상도 마커 클래스 초기화 스크립트
var platformCls = (/iOS/.test(navigator.userAgent)) ? ‘ios’ :
(/Android/.test(navigator.userAgent) ? android : ‘generic’);
var screenCls =‘screen’, screen.width, ‘x’, screen.height).join(‘’);
var orientationCls = (screen.width screen.height) ? ‘portrait’ : ‘landscape’;
$(window).addClass(platformCls, screenCls, orientationCls);
단말 플랫폼 마커 클래스를 활용한 스타일 최적화
/* 플랫폼 고유의 체크박스(on/off 스위치) 이미지를 사용 */
input[type=”checkbox”] {
background-repeat:no-repeat;
background-size:100%;
}
.ios input[type=”checkbox”] {
background-image:@url(‘img/ios/check.png’);
}
단말 해상도 마커 클래스를 활용한 스타일 최적화 .android input[type=”checkbox”] {
/* 해상도 독립적인 레이아웃 */ background-image:@url(‘img/android/check.png’);
section { width:100%; height:100%; } }
header, footer { height:10%; } ...
article { height:80%; }
article.noheader, article.nofooter { height:90%; }
article.noheader.nofooter { height:100%; }
...
/* 아이폰 해상도에 맞춰 미세 조정 */
.screen320x480 header { height:44px; }
.screen320x480 footer { height:49px; }
.screen320x480 article { height:387px;/*480-44-49*/ }
.screen320x480 article.noheader { height:436px;/*480-44*/ }
.screen320x480 article.nofooter { height:431px;/*480-49*/ }
...
/* 갤러시탭에 해상도에 맞춰 미세 조정 */
.screen600x1024 header { height:80px; }
...
39
40. 다국어처리
I18N?L10N?A11Y?
이제는선택이아닌필수!
· HTML태그에마커CSS클래스추가/활용
· “그림글자”사용최소화:국제화접근성
· 일관성있는번역어식별자
· 언어별어순/길이/너비차이고려
· 언어별UI“미세”조정
40
42. 다국어처리
다국어 지원 초기화 스크립트
var lang = navigator.language.substring(0, 2);
if(lang !== ‘en’ lang !== ‘ja’ lang !== ‘zh’) { lang = ‘en’; }
$.get(‘locales/’ + lang + ‘/messages.json’, function(messages) {
$(document.documentElement).addClass(‘jj-nls-’ + lang);//언어 식별 마커 클래스 추가
$.each(document.body).find(‘*[data-nls]’).each(function(index, node) {
var key = node.attr(‘data-nls’);
var message = messages[key];
if(message) {
node.html(message);
} else {
console.error(’missing nls message:’ + key);
} 다국어 번역 텍스트 파일(locales/언어/messages.json)
}); “common.back”: “Back”,
...
“setupTwitterPage.title”: “Setup - Twitter”,
...
다국어 지원 자리 잡기 태그
buttonspan data-nls=”common.back”이전/span/button
...
h1span data-nls=”setupTwitterPage.title”설정 - 트위터/span/h1
...
다국어 번역 텍스트 치환 결과
buttonspan data-nls=”common.back”Back/span/button
...
h1span data-nls=”setupTwitterPage.title”Setup - Twitter/span/h1
...
마커 클래스를 활용한 언어별 스타일 최적화
.jj-nls-zh #intro_title {
/*locales/zh/img/intro_title.png*/
background-image:@url(‘../img/intro_title.png’);
}
.jj-nls-zh #find_cameraBtn .text,
.jj-nls-zh #find_albumBtn .text {
! width:70px; display:inline-block;
}
...
42
44. Ant를이용한빌드자동화
target name=”verify_js” depends=”init”
jslint jslint=”${jslint.js}” encoding=”${js.encoding}”
options=”${jslint.options}” haltOnFailure=”${jslint.haltOnFailure}”
predef${jslint.predef}/predef
formatter type=”plain”/
filelist refid=”js.src.files”/
/jslint
/target
target name=”merge_js” depends=”verify_js” if=”build.release”
!-- pudface --
concat destfile=”${js.out.merged}” encoding=”${js.encoding}”
outputencoding=”${js.encoding}” fixlastline=”no” eol=”unix”
filelist refid=”js.src.files”/
filterchain
deletecharacters chars=”#xFEFF;”/
/filterchain
/concat
echo message=”merged into ${js.out.merged}”/
/target
target name=”compress_js_yuicompressor” depends=”verify_merged_js” if=”build.release”
!-- pudface --
java classname=”${yuicompressor.mainclass}” classpathref=”yuicompressor.classpath”
fork=”true” failonerror=”true”
arg value=”--verbose”/
arg value=”--charset”/
arg value=”${js.encoding}”/
arg value=”--type”/
arg value=”js”/
arg value=”-o”/
arg file=”${js.out.compressed}”/
arg file=”${js.out.merged}”/
/java
delete file=”${js.out.merged}”/
echo message=”compressed into ${js.out.compressed}”/
/target
44
45. Ant를이용한빌드자동화
target name=”build_debug” depends=”clean,copy_apis” if=”build.debug”
copy todir=”${out.dir}” verbose=”true”
fileset dir=”${src.dir}”
exclude name=”index.html”/
/fileset
/copy
copy todir=”${out.dir}” encoding=”${html.encoding}”
outputencoding=”${html.encoding}” verbose=”true” overwrite=”true”
fileset dir=”${src.dir}”
include name=”index.html”/
/fileset
filterchain
linecontains negate=”true”contains value=”@@RELEASE”//linecontains
/filterchain
/copy
/target
target name=”build_release” depends=”clean,compress_js,compress_css” if=”build.release”
copy todir=”${out.dir}” verbose=”true”
fileset dir=”${src.dir}”
exclude name=”index.html”/
exclude name=”js/pudface/**”/
exclude name=”css/**”/
/fileset
/copy
copy todir=”${out.dir}” encoding=”${html.encoding}”
outputencoding=”${html.encoding}” verbose=”true” overwrite=”true”
fileset dir=”${src.dir}”
include name=”index.html”/
/fileset
filterchain
linecontains negate=”true”contains value=”@@DEBUG”//linecontains
/filterchain
/copy
/target
45
46. 네이티브와의결합
사용한앱스프레소플러그인
· deviceapis.filesystem:파일입출력
· deviceapis.deviceinteraction:화면꺼짐방지/진동
· ax.ext.media:카메라/포토앨범/효과음
· ax.ext.net:업로드/다운로드
· ax.ext.ui:네이티브UI/차일드브라우져
· ax.ext.ga:구글통계
· ax.ext.admob:애드몹광고
· kth.puddingface:푸딩얼굴인식앱전용*^^*
46
48. 앱스프레소플러그인구조
AppspressoPlugin Appspresso
Appspresso Project Plugin
Application
Archive
Project axplugin.xml axplugin.js
(*.axp)
res overlay
link export
run lib*.a *.jar
share
iOSNativeModule AndroidNativeModule
(XcodeStaticLibraryProject) (AndroidLibraryProject)
48
49. 앱스프레소플러그인API
AxPlugin AxPluginContext
intgetId()
/*YOURCODEHERE*/ 2
StringgetMethod()
activate(AxRuntimeContext) Object[]getParams()
deactivate(AxRuntimeContext) ...
execute(AxPluginContext) sendResult([result])
... sendError(code[,message])
2 4
AxRuntimeContext 1
자바스크립트API
getWebView()
getWidget()
...
requirePlugin(pluginId) 플랫폼확장API
executeJavaScript(script)
... Android iOS
49
50. 앱스프레소플러그인플랫폼확장API
·안드로이드전용
· Activity
· ActivityListener
· WebViewListener
· WebViewClientListener/WebChromeClientListener
· iOS전용
· UIViewController
· UIApplicationDelegate
· UIWebViewDelegate
· AxViewControllerDelegate
50
51. 앱스프레소플러그인자바스크립트API
·AxPlugin의자바스크립트“stub”
· functionexecSync(method,params)
· functionexecAsync(method,successCallback,
errorCallback,params)
· params:arrayofarguments
· successCallback:function(result){...}
· errorCallback:function(error){...}
· ax,ax.error,ax.util,ax.console,
ax.request,ax.bridge,ax.plugin,...
51
52. 앱스프레소플러그인개발실습
네이티브주소록UI플러그인
· deviceapis.pim.contact는...
· 너무어려워+너무느려+뽀대도안나...x3
· 그래서,@bluenmad 가만들었습니다~
ax.ext.contact.pickContact(function(contact) {
if(contact contact.phoneNumbers) {
var firstPhoneNumber = phoneNumbers.split(‘,’)[0];
if(confirm(contact.firstName + ‘에게 전화걸까요?’)) {
location.href = ‘tel:’ + firstPhoneNumber;
}
}
}, function(error) { ... })
· 어떻게?
52
53. axplugin.xml
?xml version=”1.0” encoding=”UTF-8”?
axplugin id=”ax.ext.contact” version=”1.0”
! descriptionContact Extension API Appspresso Plugin
! /description
! urlhttp://appspresso.com/url
! authorAppspresso Dev. Team/author
! licenseCopyright (c) 2011, KT Hitel Co., LTD. All Rights Reserved.
! /license
! feature id=”http://appspresso.com/api/ax.ext.contact“
! ! category=”Extension” /
! module platform=”android” platform-version=”8”
! ! min-platform-version=”7” max-platform-version=””
! ! class=”com.appspresso.screw.contact.ContactPlugin”
! ! property name=”permission” value=”android.permission.READ_CONTACTS” /
! /module
! module platform=”ios” platform-version=”4.1”
! ! min-platform-version=”4.0” max-platform-version=””
! ! class=”ax_ext_contact_MyPlugin”
! ! property name=”framework” value=”AddressBook.framework, AddressBookUI.framework” /
! /module
/axplugin
53
54. axplugin.js
/*jslint browser:true, confusion:true, /**
debug:true, devel:true, nomen:true, * pick a contact
plusplus:true, vars:true */ *
/** * @param {function} callback
* @fileOverview Contact Extension API * @param {function} errback
* @author blueNmad * @param {ax.ext.contact.ContactOpts} opts
* @version 1.0 * @return AxRequest
*/ * @methodOf ax.ext.contact
(function () { */
“use strict”;
function pickContact(callback, errback,
var NS_CONTACT = “ax.ext.contact”; opts) {
var PREFIX_CONTACT = “ax.ext.contact”; onPickContactCallback = callback;
/** return this.execAsync(‘pickContact’,
* Contact Extension API ax.nop, errback, [opts || {}]);
* }
* @namespace
* @name ax.ext.contact function onPickContact(contact) {
*/ if ( !! onPickContactCallback) {
onPickContactCallback(eval(contact));
/** }
* @class
* @name ContactOpts onPickContactCallback = undefined;
* @memberOf ax.ext.contact }
*/
ax.plugin(PREFIX_CONTACT, {
/** ‘pickContact’: pickContact,
* native callback for pickContact. ‘onPickContact’: onPickContact
* }, NS_CONTACT);
* @param result })();
* @memberOf ax.ext.contact
* @private
*/
var onPickContactCallback = undefined;
54
55. com...contact.ContactPlugin.java
package com.appspresso.screw.contact; return super.onActivityResult(activity,
requestCode, resultCode, data);
import android.app.Activity; }
import android.content.Intent; };
import android.provider.ContactsContract;
public void activate(
import com.appspresso.api.AxPluginContext; AxRuntimeContext runtimeContext) {
import com.appspresso.api.AxRuntimeContext; super.activate(runtimeContext);
import com.appspresso.api.DefaultAxPlugin;
... runtimeContext.addActivityListener(
중간생략 activityListener);
... }
/** public void deactivate(
* Appspresso Plugin Android Module AxRuntimeContext runtimeContext) {
* runtimeContext.removeActivityListener(
* id: ax.ext.contact activityListener);
* version: 1.0.0
* super.deactivate(runtimeContext);
*/ }
public class ContactPlugin extends
DefaultAxPlugin { public void pickContact(
private static final int AxPluginContext context) {
REQ_PICK_CONTACT = 62000; Intent intent = new Intent(
Intent.ACTION_PICK,
private ActivityListener activityListener = ContactsContract.Contacts.CONTENT_URI);
new ActivityAdapter() { runtimeContext.getActivity()
public boolean onActivityResult( .startActivityForResult(intent,
Activity activity, int requestCode, int REQ_PICK_CONTACT);
resultCode, Intent data) { context.sendResult();
if (ContactPlugin.REQ_PICK_CONTACT == }
requestCode data != null) { }
return ContactUtils.onPickContact(
runtimeContext, data);
}
55
56. com...contact.ContactUtils.java
package com.appspresso.screw.contact; String contactId =
data.getData().getLastPathSegment();
import java.util.ArrayList;
import java.util.List; JSONObject contact =
ContactUtils.getContactWithContactId(
import org.apache.commons.logging.Log; activity, contactId);
import org.json.JSONArray;
import org.json.JSONObject; if (contact != null) {
runtimeContex.invokeJavaScriptFunction(
import android.app.Activity; JS_CALLBACK_ONPICKCONTACT, contact);
import android.content.Intent; }
import android.database.Cursor; return true;
import android.provider.ContactsContract; }
...
중략 static JSONObject getContactWithContactId(
... Activity activity, String contactId) {
import android.webkit.WebView; JSONObject contact = new JSONObject();
...
import com.appspresso.api.AxLog; Cursor cursor = null;
Cursor rawContactIdsCursor = null;
class ContactUtils { try {
private static Log L = String[] rawContactIds = null;
AxLog.getLog(“ContactPlugin”); rawContactIdsCursor = activity
.getContentResolver().query(
private static final String RawContacts.CONTENT_URI,
JS_CALLBACK_ONPICKCONTACT = new String[] { RawContacts._ID },
“ax.ext.contact.onPickContact”; RawContacts.CONTACT_ID + “ = ?”,
new String[] { contactId },
public static boolean onPickContact( null);
RuntimeContext runtimeContext, ...
Intent data) { 중략
if (data == null) { ...
return false; }
} }
56
57. ax_ext_contact_MyPlugin.h
#import Foundation/Foundation.h
#import UIKit/UIKit.h
#import AddressBook/AddressBook.h
#import AddressBookUI/AddressBookUI.h
#import “AxPlugin.h”
@protocol AxContext;
@protocol AxPluginContext;
@interface ax_ext_contact_MyPlugin : NSObjectAxPlugin, ABPeoplePickerNavigationControllerDelegate {
@private
NSObjectAxRuntimeContext *_runtimeContext;
}
@property (nonatomic,readonly,retain) NSObjectAxRuntimeContext* runtimeContext;
- (void)activate:(NSObjectAxRuntimeContext*)runtimeContext;
- (void)deactivate:(NSObjectAxRuntimeContext*)runtimeContext;
- (void)execute:(NSObjectAxPluginContext*)context;
- (IBAction)presentABPeoplePickerNavigationController;
// ABPeoplePickerNavigationControllerDelegate method
- (BOOL)peoplePickerNavigationController:(ABPeoplePickerNavigationController *)peoplePicker
shouldContinueAfterSelectingPerson:(ABRecordRef)person;
- (BOOL)peoplePickerNavigationController:(ABPeoplePickerNavigationController *)peoplePicker
shouldContinueAfterSelectingPerson:(ABRecordRef)person
property:(ABPropertyID)property
identifier:(ABMultiValueIdentifier)identifier;
- (void)peoplePickerNavigationControllerDidCancel:(ABPeoplePickerNavigationController *)peoplePicker;
@end
57
58. ax_ext_contact_MyPlugin.m
// [self
// ax_ext_contact_MyPlugin.m presentABPeoplePickerNavigationController];
// [context sendResult];
// Copyright 2011 none. All rights reserved. }
// else {
[context sendError:AX_NOT_AVAILABLE_ERR];
#import “AxRuntimeContext.h” }
#import “AxPluginContext.h” }
#import “AxError.h”
#import “AxLog.h” -
#import “ax_ext_contact_MyPlugin.h” (IBAction)presentABPeoplePickerNavigationContr
oller {
#define JS_CALLBACK_ONPICKCONTACT dispatch_async(dispatch_get_main_queue(), ^{
@”ax.ext.contact.onPickContact” ABPeoplePickerNavigationController *picker
= [[[ABPeoplePickerNavigationController alloc]
@implementation ax_ext_contact_MyPlugin init] autorelease];
picker.peoplePickerDelegate = self;
@synthesize runtimeContext = _runtimeContext; [[self.runtimeContext getViewController]
presentModalViewController:picker
- (void)activate: animated:YES];
(NSObjectAxRuntimeContext*)runtimeContext { // [picker release];
_runtimeContext = [runtimeContext retain]; });
} }
...
- (void)deactivate: 중략
(NSObjectAxRuntimeContext*)runtimeContext { ...
[_runtimeContext release]; -
_runtimeContext = nil; (void)peoplePickerNavigationControllerDidCance
} l:(ABPeoplePickerNavigationController
*)peoplePicker {
- (void)execute:(idAxPluginContext)context { [[self.runtimeContext getViewController]
NSString* method = [context getMethod]; dismissModalViewControllerAnimated:YES];
}
AX_LOG_TRACE(@”ContactView_ios_method : %s”,
method); @end
if([method isEqualToString:@”pickContact”]){
58
62. References
· 하이브리드모바일앱프레임웍http://slideshare.net/iolo/hybrid-mobile-application-framework
· 단일페이지인터페이스웹/앱개발http://slideshare.net/iolo/ss-7719322
· AndroidSDKWebView레퍼런스http://goo.gl/iqr9H
· iOSSDKUIWebView레퍼런스http://goo.gl/U8XGy
· Android용하이브리드앱템플릿https://github.com/iolo/hellowebapp-android
· HowtobuildAndroidAppwithHTML/CSS/JavaScripthttp://youtube.com/watch?v=uVqp1zcMfbE
· iOS용하이브리드앱템플릿https://github.com/iolo/hellowebapp-ios
· HowtobuildiOSAppwithHTML/CSS/JavaScripthttp://youtube.com/watch?v=L28lGkoSQ2c
· 푸딩얼굴인식앱(Android)https://market.android.com/details?id=com.kth.puddingface
· 푸딩얼굴인식다국어앱(iOS)http://itunes.apple.com/us/app/id378461555?mt=8
· 앱스프레소홈페이지http://appspresso.com/
· 폰갭홈페이지http://phonegap.com/
· 티타늄홈페이지http://appcelerator.com/
· jQuery홈페이지http://jquery.com/
· iScroll홈페이지http://cubiq.org/iscroll/
· JSLint홈페이지http://jslint.com/
· YUICompressor홈페이지http://developer.yahoo.com/yui/compressor/
62