JavaScriptCore.frameworkで
できること
岸川克己
http://kishikawakatsumi.com
@k_katsumi
24/7 twenty-four seven
http://d.hatena.ne.jp/KishikawaKatsumi/
アジェンダ
•
JavaScriptCore.frameworkの概要と使い方
•
Objective-C Runtime APIの活用方法
関連記事
•JavaScriptだけでiOSのUIを書いてみる
•http://d.hatena.ne.jp/KishikawaKatsumi/20131222/1387734162
•JavaScriptでiOSアプリが書けるライブラリJavaScriptBridgeを
公開しました
•http://d.hatena.ne.jp/KishikawaKatsumi/20140104/1388848644
サンプルコード
•
https://github.com/kishikawakatsumi/downloadable-ios-apps
•https://github.com/kishikawakatsumi/JavaScriptBridge
Objective-C <-> JavaScript ブリッジ
•
Objective-CからJavaScriptを実行する
•
JavaScriptからObjective-Cを実行する
•
Objective-CとJavaScriptのオブジェクトの
!
JSContext
*context = [[
JSContext
alloc
]
init
];
!
JSValue
*result = [context
evaluateScript
:
@"2 + 8"
];
JSContext
*context = [[
JSContext
alloc
]
init
];
!
context[
@"factorial"
] = ^(
int
x) {
int
factorial =
1
;
for
(; x >
1
; x--) {
factorial *= x;
}
return
factorial;
};
!
JSValue
*result = [context
evaluateScript
:
@"factorial(5);"
];
JSContext
*context = [[
JSContext
alloc
]
init
];
!
context[
@"factorial"
] = ^(
int
x) {
int
factorial =
1
;
for
(; x >
1
; x--) {
factorial *= x;
}
return
factorial;
};
!
JSValue
*function = context[
@“factorial"
];
!
JSValue
*result = [function
callWithArguments
:
@[@(5)]
];
JSContext
*context = [[
JSContext
alloc
]
init
];
[context
evaluateScript
:
@"function sum(a, b) { return a + b; }"
];
!
JSValue
*function = context[
@“sum"
];
!
JSValue
*result = [function
callWithArguments
:
@[@(2)
,
@(3)]
];
•
JSVirtualMachine
•
JSContext
•
JSValue
•
JSManagedValue
•
JSExport
•
JSVirtualMachine
-
シングルスレッドのJS仮想マシン
•
JSContext
-
JSの実行コンテキスト(オブジェクトの管理)
•
JSValue
-
JSオブジェクトのラッパー
JSVirtualMachine
JSContext
JSValue
JSVirtualMachine
JSVirtualMachine
JSValue
JSContext
JSValue
JSValue
Objective-C -> JavaScript
•
一部のロジックをJavaScriptで記述
-
WebやAndroidなど別のプラットフォームと共通化するとか
•
はてな記法 Parser
-text-hatena.js
- http://tech.nitoyon.com/javascript/application/texthatena/download.html •Textile Parser
-textile.js
- https://github.com/borgar/textile-js •JSONの圧縮
-JSONH
-https://github.com/WebReflection/JSONH
JavaScriptで書かれたライブラリを使う
NSBundle *bundle = [NSBundle mainBundle];
NSString *path = [bundle pathForResource:@"text-hatena" ofType:@"js"];
NSString *script = [NSString stringWithContentsOfFile:path
encoding:NSUTF8StringEncoding error:nil];
!
JSContext *context = [[JSContext alloc] init]; [context evaluateScript:script];
[context evaluateScript:@"var parser = new TextHatena();"];
!
JSValue *parser = context[@"parser"];
!
NSString *text = [NSString stringWithContentsOfFile:
[bundle pathForResource:@“sample” ofType:@“txt"]
encoding:NSUTF8StringEncoding error:nil];
!
NSLog(@"%@", [parser invokeMethod:@"parse" withArguments:@[text]]);
!
*1384756611*[Objective-C][iOS]iOSアプリケーションでキーボードショートカットに対応する ↓ より丁寧な記事はこちらで公開しています。 - [http://mobiletou.ch/2013/11/ ios%E3%82%A2%E3%83%97%E3%83%AA%E3%81%A7%E5%A4%96%E9%83%A8%E3%82%AD%E3%83%BC%E3%83%9C%E3 %83%BC%E3%83%89%E3%81%8B%E3%82%89%E3%81%AE%E3%82%B7%E3%83%A7%E3%83%BC%E3%83%88%E3%82%AB %E3%83%83%E3%83%88:title=iOSアプリで外部キーボードからのショートカットに対応する方法 - iOSアプリ開 発こぼれ話]
!
!
iOS 7のSafariやメールでは外部キーボードを使用した際に利用できるできるショートカットが[http:// www.appbank.net/2013/11/08/iphone-news/696998.php:title=以前より充実したことが話題になりまし た。]!
!
iOS 7ではキーボードショートカットを実装するためのAPIが追加されているので、サードパーティのアプリケーショ ンもキーボードショートカットに対応することができます。!
!
特定のキーボードショートカットに応答するには下記のプロパティを実装します。 >|objc|@property(nonatomic, readonly) NSArray *keyCommands ||<
<div class="section">
<h3><a href="#1384756611" name="1384756611"><span class="sanchor">o-</span></a> [<a class="sectioncategory" href="searchdiary?word=*[Objective-C]">Objective-C</a>][<a class="sectioncategory" href="searchdiary?word=*[iOS]">iOS</a>]iOSアプリケーションでキーボードショート カットに対応する</h3> <p>↓ より丁寧な記事はこちらで公開しています。</p> <ul> <li> [http://mobiletou.ch/2013/11/ ios%E3%82%A2%E3%83%97%E3%83%AA%E3%81%A7%E5%A4%96%E9%83%A8%E3%82%AD%E3%83%BC%E3%83%9C%E3%83%BC%E3%8 3%89%E3%81%8B%E3%82%89%E3%81%AE%E3%82%B7%E3%83%A7%E3%83%BC%E3%83%88%E3%82%AB%E3%83%83%E3%83%88:tit le=iOSアプリで外部キーボードからのショートカットに対応する方法 - iOSアプリ開発こぼれ話]</li> </ul> <br> <p>iOS 7のSafariやメールでは外部キーボードを使用した際に利用できるできるショートカットが[http:// www.appbank.net/2013/11/08/iphone-news/696998.php:title=以前より充実したことが話題になりました。]</p> <br> <p>iOS 7ではキーボードショートカットを実装するためのAPIが追加されているので、サードパーティのアプリケーションも キーボードショートカットに対応することができます。</p> <br> <p>特定のキーボードショートカットに応答するには下記のプロパティを実装します。</p> <pre class="syntax-highlight prettyprint lang-objc">
@property(nonatomic, readonly) NSArray *keyCommands </pre>
NSBundle *bundle = [NSBundle mainBundle];
NSString *path = [bundle pathForResource:@"textile" ofType:@"js"];
NSString *script = [NSString stringWithContentsOfFile:path
encoding:NSUTF8StringEncoding error:nil];
!
JSContext *context = [[JSContext alloc] init]; [context evaluateScript:script];
!
JSValue *func = context[@"textile"];
!
NSString *text = [NSString stringWithContentsOfFile:
[bundle pathForResource:@"sample" ofType:@“textile"] encoding:NSUTF8StringEncoding error:nil];
!
今回は、スペシャルセッションとしてEvernote本社からMac版EvernoteのUIのリニューアルの指揮をさ れました["Jack Hirsch":https://twitter.com/Jackolicious]さんにきていただき、背景や開 発手法などをお話いただきます。 すべての開発者にとって非常に有意義な時間になると思いますのでぜひお越しください。
!
!
h3. スタッフ募集!
当日、会場の準備や受付などを手伝ってくださるかたを募集いたします。手伝ってもいいよ、というかた がいらっしゃいましたらご連絡くださいませ。!
["lesamoureuses":https://twitter.com/lesamoureuses] ["huin":https://twitter.com/huin] ["myb":https://twitter.com/myb] ["nun_":https://twitter.com/nun_] ["KohsakuNishida":https://twitter.com/KohsakuNishida] ["森嶋大樹":https://www.facebook.com/hiroki.with.omnia]!
!
*開場 12:30* **場所** 株式会社VOYAGE GROUP (東京都渋谷区神泉町8-16 渋谷ファーストプレイス8F)<p>今回は、スペシャルセッションとしてEvernote本社からMac版EvernoteのUIのリニューア ルの指揮をされました<a href="https://twitter.com/Jackolicious">Jack Hirsch</a>さんにきていただき、背景や開発手法などをお話いただきます。<br /> すべての開発者にとって非常に有意義な時間になると思いますのでぜひお越しください。</p> <h3>スタッフ募集</h3> <p>当日、会場の準備や受付などを手伝ってくださるかたを募集いたします。手伝ってもいい よ、というかたがいらっしゃいましたらご連絡くださいませ。</p> <p><a href="https://twitter.com/lesamoureuses">lesamoureuses</a><br /> <a href="https://twitter.com/huin">huin</a><br /> <a href="https://twitter.com/myb">myb</a><br /> <a href="https://twitter.com/nun_">nun_</a><br /> <a href="https://twitter.com/KohsakuNishida">KohsakuNishida</a><br /> <a href="https://www.facebook.com/hiroki.with.omnia">森嶋大樹</a> </p> <p><strong>開場 12:30</strong><br /> <b>場所</b><br />
株式会社<span class="caps">VOYAGE</span> <span class="caps">GROUP</ span><br />
JavaScriptに公開する
@protocol JSUIWindow <
JSExport
>
!
@property (nonatomic)
CGRect
frame;
@property (nonatomic)
UIColor
*backgroundColor;
!
+ (id)new;
- (void)makeKeyAndVisible;
!
@interface
JSUIWindow :
UIWindow
<JSUIWindow>
!
@end
!
@implementation
JSUIWindow
!
@end
JSContext
*context = [[
JSContext
alloc
]
init
];
context[
@"JSUIWindow"
] = [
JSUIWindow
class
];
JSContext
*context = [[
JSContext
alloc
]
init
];
context[
@"JSUIWindow"
] = [
JSUIWindow
class
];
!
[context
evaluateScript
:
@"var window = JSUIWindow.new();"
];
!
JSValue
*value = context[
@"window"
];
既存のクラスをJSExportに
適合させる
!
class_addProtocol([UIWindow class],
@protocol
(JSUIWindow));
JSContext
*context = [[
JSContext
alloc
]
init
];
context[
@"UIWindow"
] = [
UIWindow
class
];
[context
evaluateScript
:
@"var window = UIWindow.new();"
];
!
JSValue
*value = context[
@"window"
];
動的にやってみる
(できない)
static void setup(JSContext *context) {
const char *prefix = "JSUI";
unsigned int numberOfClasses;
Class *classes = objc_copyClassList(&numberOfClasses); for (unsigned int i = 0; i < numberOfClasses; i++) { Class cls = classes[i];
const char *className = class_getName(cls);
NSString *name = @(className);
char protocolName[512];
snprintf(protocolName, sizeof(protocolName), "%s%s", prefix, className);
Protocol *proto = objc_allocateProtocol(protocolName);
traverce_class(proto, cls);
protocol_addProtocol(proto, @protocol(JSExport)); objc_registerProtocol(proto); class_addProtocol(cls, proto); context[name] = cls; } free(classes); }
SIGABRT
@protocol JSBUIView;
!
@protocol JSBUIAlertView <JSExport, JSBUIView>
!
@property (nonatomic, readonly) NSInteger numberOfButtons;
@property (nonatomic, readonly, getter = isVisible) BOOL visible; @property (nonatomic) NSInteger cancelButtonIndex;
@property (nonatomic, copy) NSString *title; @property (nonatomic, copy) NSString *message; @property (nonatomic, assign) id delegate;
@property (nonatomic, assign) UIAlertViewStyle alertViewStyle; @property (nonatomic, readonly) NSInteger firstOtherButtonIndex;
!
- (id)initWithTitle:(NSString *)title message:(NSString *)message
delegate:(id)delegate cancelButtonTitle:(NSString *)cancelButtonTitle otherButtonTitles:(NSString *)otherButtonTitles , ...;
- (NSInteger)addButtonWithTitle:(NSString *)title;
- (NSString *)buttonTitleAtIndex:(NSInteger)buttonIndex; - (void)show;
- (void)dismissWithClickedButtonIndex:(NSInteger)buttonIndex animated: (BOOL)animated;
- (UITextField *)textFieldAtIndex:(NSInteger)textFieldIndex;
!
#pragma clang diagnostic pop
!
@protocol
JSBUIView <
JSExport
, JSBUIResponder>
!
@property
(
nonatomic
,
getter
= isOpaque)
BOOL
opaque;
@property
(
nonatomic
)
CGRect
frame;
@property
(
nonatomic
,
getter
= isHidden)
BOOL
hidden;
@property
(
nonatomic
)
BOOL
autoresizesSubviews;
@property
(
nonatomic
,
readonly
,
copy
)
NSArray
*subviews;
!
!
+ (Class)layerClass;
+ (
void
)beginAnimations:(
NSString
*)animationID context:(
void
*)context;
@protocol JSBNSObject <JSExport, NSObject>
!
- (BOOL)isEqual:(id)object; - (NSUInteger)hash;
!
- (Class)superclass; - (Class)class;!
- (BOOL)isProxy;!
- (BOOL)isKindOfClass:(Class)aClass; - (BOOL)isMemberOfClass:(Class)aClass;- (BOOL)conformsToProtocol:(Protocol *)aProtocol;
!
- (NSString *)description; - (NSString *)debugDescription;!
+ (void)load;!
+ (void)initialize; - (id)init;!
+ (id)new;JSContext
*context = [
JSBScriptingSupport
globalContext
];
!
[context
addScriptingSupport
:
@“AssetsLibrary"
];
[context
addScriptingSupport
:
@"Accounts"
];
[context
addScriptingSupport
:
@"Social"
];
[context
addScriptingSupport
:
@“MapKit"
];
[context evaluateScript:
@"var window = UIWindow.alloc().initWithFrame(UIScreen.mainScreen().bounds);" @"window.backgroundColor = UIColor.whiteColor();"
@""
@"var navigationController = UINavigationController.new();" @""
@"var tableViewController = UITableViewController.new();"
@"tableViewController.navigationItem.title = 'JavaScriptBridge';" @"navigationController.viewControllers = [tableViewController];" @"" @"window.rootViewController = navigationController;" @"" @"window.makeKeyAndVisible();" @""
@"var alertView = UIAlertView.new();"
@"alertView.message = 'Hello JavaScriptBridge!';" @"alertView.addButtonWithTitle('OK');"
@"alertView.show();"
UISlider
*slider = [[UISlider alloc] initWithFrame:frame];
slider.backgroundColor = [UIColor clearColor];
slider.minimumValue =
0.0
;
slider.maximumValue =
100.0
;
slider.continuous =
YES
;
slider.value =
50.0
;
var
slider = UISlider.alloc().initWithFrame(frame);
slider.backgroundColor = UIColor.clearColor();
slider.minimumValue =
0.0
;
slider.maximumValue =
100.0
;
slider.continuous =
true
;
slider.value =
50.0
;
プロパティアクセス
UIWindow *window =
[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
var
window =
UIWindow.alloc().initWithFrame(UIScreen.mainScreen().bounds);
UIView
*view = [
UIView
new
];
view.frame = CGRectMake(
20
,
80
,
280
,
80
);
!
CGFloat
x =
view
.
frame
.
origin
.
x
;
CGFloat
width =
view
.
frame
.
size
.
width
;
var
view = UIView.new();
view.frame = {x:
20
, y:
80
, width:
280
, height:
80
};
!
var
x = view.frame.x;
var width = view.frame.width;
var MainViewController =
JSB.defineClass(
'MainViewController : UITableViewController'
, {
// Instance Method Definitions
viewDidLoad: function() {
self
.navigationItem.title =
'UICatalog'
;
},
viewWillAppear: function(animated) {
self
.tableView.reloadData();
}
}, {
// Class Method Definitions
attemptRotationToDeviceOrientation: function() {
...
}
});
Class cls = objc_allocateClassPair(NSClassFromString(parentClassName), className.UTF8String, 0);
objc_registerClassPair(cls);
!
Class superClass = class_getSuperclass(cls); if (superClass) {
setupForwardingImplementations(cls, superClass, instanceMembers, staticMembers); }
!
NSString *types; BOOL result;
!
Class metaClass = objc_getMetaClass(className.UTF8String);
!
types = [NSString stringWithFormat: @"%s%s%s%s", @encode(NSMethodSignature), @encode(id), @encode(SEL), @encode(SEL)]; result = class_addMethod(cls, @selector(methodSignatureForSelector:), (IMP)methodSignatureForSelector, types.UTF8String);
result = class_addMethod(metaClass, @selector(methodSignatureForSelector:), (IMP)methodSignatureForSelector, types.UTF8String);
!
types = [NSString stringWithFormat: @"%s%s%s%s", @encode(void), @encode(id), @encode(SEL), @encode(NSInvocation)]; result = class_addMethod(cls, @selector(forwardInvocation:), (IMP)forwardInvocation, types.UTF8String);
result = class_addMethod(metaClass, @selector(forwardInvocation:), (IMP)forwardInvocation, types.UTF8String);
!
types = [NSString stringWithFormat: @"%s%s%s%s", @encode(BOOL), @encode(id), @encode(SEL), @encode(SEL)]; result = class_addMethod(cls, @selector(respondsToSelector:), (IMP)respondsToSelector, types.UTF8String);
result = class_addMethod(metaClass, @selector(respondsToSelector:), (IMP)respondsToSelector, types.UTF8String);
!
for (NSString *protocol in [protocols componentsSeparatedByString:@","]) {
class_addProtocol(cls, NSProtocolFromString([protocol stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]])); }
!
class_addProtocol(cls, @protocol(JSBNSObject));
!
NSString *key = mangledNameFromClass(cls);
globalContext[key] = cls;
globalContext[key][JSBInstanceMembersKey] = instanceMembers;
globalContext[key][JSBStaticMembersKey] = staticMembers;
!
Class cls = objc_allocateClassPair(NSClassFromString(parentClassName), className.UTF8String,
0);
objc_registerClassPair(cls);
NSString
*types;
BOOL
result;
!
types = [
NSString
stringWithFormat
:
@“%s%s%s%s"
,
@encode
(
NSMethodSignature
),
@encode
(
id
),
@encode
(
SEL
),
@encode
(
SEL
)];
!
result =
class_addMethod
(cls,
@selector
(methodSignatureForSelector:),
(
IMP
)
methodSignatureForSelector
,
types.
UTF8String
);
クラスにメソッドを追加
(インスタンスメソッド)
methodSignatureForSelector:
forwardInvocation:
respondsToSelector:
NSMethodSignature *methodSignatureForSelector(id self, SEL _cmd, SEL selector) {
NSMethodSignature *methodSignature = nil;
Class cls = object_getClass(self);
if (class_isMetaClass(cls)) {
methodSignature = [cls instanceMethodSignatureForSelector:selector]; if (methodSignature) {
return methodSignature; }
} else {
methodSignature = [cls instanceMethodSignatureForSelector:selector]; if (methodSignature) {
return methodSignature; }
}
NSUInteger numberOfArguments = [[NSStringFromSelector(selector)
componentsSeparatedByString:@":"] count] - 1;
return [NSMethodSignature signatureWithObjCTypes:[[@"@@:"
stringByPaddingToLength:numberOfArguments + 3 withString:@"@" startingAtIndex:0]
UTF8String]]; }
BOOL
respondsToSelector(
id
self
,
SEL
_cmd
,
SEL
selector)
{
NSString
*propertyName =
propertyNameFromSelector
(selector);
JSValue
*function =
propertyForObject
(
self
, propertyName);
return
!function.
isUndefined
;
}
void forwardInvocation(id self, SEL _cmd, NSInvocation *invocation) {
JSContext *context = [JSBScriptingSupport globalContext];
if ([[self superclass] instancesRespondToSelector:invocation.selector]) {
invokeSuper(invocation);
}
id currentSelf = context[@"self"]; context[@"self"] = self;
NSString *propertyName = propertyNameFromSelector(invocation.selector); JSValue *function = propertyForObject(self, propertyName);
if (!function.isUndefined) {
NSArray *arguments = extractArguments(invocation);
JSValue *returnValue = [function callWithArguments:arguments];
setReturnValue(returnValue, invocation);
}
context[@"self"] = currentSelf; }
NSString
*types;
BOOL
result;
!
Class metaClass =
objc_getMetaClass
(className.
UTF8String
);
!
types = [
NSString
stringWithFormat
:
@"%s%s%s%s"
,
@encode
(
NSMethodSignature
),
@encode
(
id
),
@encode
(
SEL
),
@encode
(
SEL
)];
!
result =
class_addMethod
(metaClass,
@selector
(methodSignatureForSelector:),
(
IMP
)
methodSignatureForSelector
,
types.
UTF8String
);
クラスにメソッドを追加
(クラスメソッド)
JSValue *propertyForObject(id obj, NSString *propertyName) {
JSContext *context = [JSBScriptingSupport globalContext];
JSValue *properties = nil;
Class cls = object_getClass(obj); if (class_isMetaClass(cls)) {
properties = context[mangledNameFromClass(obj)][JSBStaticMembersKey]; } else {
properties = context[mangledNameFromClass(cls)][JSBInstanceMembersKey]; } return properties[propertyName]; }
id型のオブジェクトが
クラスオブジェクトかどうか
unsigned int numberOfInstanceMethods = 0;
Method *instanceMethods = class_copyMethodList(cls, &numberOfInstanceMethods);
for (unsigned int i = 0; i < numberOfInstanceMethods; i++) { Method method = instanceMethods[i];
struct objc_method_description *description = method_getDescription(method);
NSString *propertyName = propertyNameFromSelector(description->name); JSValue *function = instanceFunctions[propertyName];
if (!function.isUndefined) { class_addMethod(targetClass, description->name, _objc_msgForward, description->types); } } if (instanceMethods) { free(instanceMethods); }
クラスにメソッドを追加
(オーバーライド)
class_addMethod(targetClass,
description->name, _objc_msgForward, description->types);
!
OBJC_EXPORT
id
_objc_msgForward(
id
receiver,
SEL
sel, ...)
__OSX_AVAILABLE_STARTING
(__MAC_10_0, __IPHONE_2_0);
void forwardInvocation(id self, SEL _cmd, NSInvocation *invocation) {
JSContext *context = [JSBScriptingSupport globalContext];
if ([[self superclass] instancesRespondToSelector:invocation.selector]) {
invokeSuper(invocation); }
id currentSelf = context[@"self"]; context[@"self"] = self;
NSString *propertyName = propertyNameFromSelector(invocation.selector); JSValue *function = propertyForObject(self, propertyName);
if (!function.isUndefined) {
NSArray *arguments = extractArguments(invocation);
JSValue *returnValue = [function callWithArguments:arguments];
setReturnValue(returnValue, invocation); }
context[@"self"] = currentSelf; }
モジュール
var ButtonsViewController = JSB.require('buttonsViewController'); var ControlsViewController = JSB.require('controlsViewController'); var WebViewController = JSB.require('webViewController');
var MapViewController = JSB.require('mapViewController');
!
var MainViewController = JSB.defineClass('MainViewController : UITableViewController', { viewDidLoad: function() {
self.navigationItem.title = 'UICatalog';
... });