-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Basic Usage of JSPatch
1.require
2.Invoke OC method
Invoke class method
Invoke instance method
Pass parameters
Property
Convert name of method
3.defineClass
API
Override method
Override class method
Override category method
Super
Property
Get/Modify property declared in OC
Add property dynamically
Private member variable
Add new method
Protocol
4.Special type
Struct
Selector
nil
5.NSArray / NSString / NSDictionary
6.Block
Pass block
Use self in block
Limitation
7.__weak / __strong
8.GCD
9.Pass parameter of type id*
10.Constant、Enum、Macros、Global variable
Constant/Enum
Macros
Global variable
11.Swift
12.Link dynamic library
13.Debug
You should call require('className’)
before using Objective-C classes:
require('UIView')
var view = UIView.alloc().init()
You can also import several classes at one time with comma as the seperator:
require('UIView, UIColor')
var view = UIView.alloc().init()
var red = UIColor.redColor()
Or you can call require()
method just before calling OC method:
require('UIView').alloc().init()
var redColor = UIColor.redColor();
var view = UIView.alloc().init();
view.setNeedsLayout();
You can pass parameters into method just like in OC:
var view = UIView.alloc().init();
var superView = UIView.alloc().init()
superView.addSubview(view)
Don't forget to add ()
when accessing or modifying a property and you are actually calling its getter and setter method:
view.setBackgroundColor(redColor);
var bgColor = view.backgroundColor();
If the method in OC has several parameters, you should use _
as seperator:
var indexPath = require('NSIndexPath').indexPathForRow_inSection(0, 1);
You should use double underline __
in case a single underline exists in the name of OC method:
// Obj-C: [JPObject _privateMethod];
JPObject.__privateMethod()
defineClass(classDeclaration, [properties,] instanceMethods, classMethods)
- @param
classDeclaration
: It is a string which represents the name of a class, super class or protocol. - @param
properties
: Properties you want to add, an array whose elements are string, it's optional. - @param
instanceMethods
: Instance methods you want to add or override. - @param
classMethods
: Class methods you want to add or override.
1.You can define an OC method in defineClass
to override it, and just like calling a method, you should use underline _
as the seperator:
// OC
@implementation JPTestObject
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
}
@end
// JS
defineClass("JPTableViewController", {
tableView_didSelectRowAtIndexPath: function(tableView, indexPath) {
...
},
})
2.You should use double underline __
to represent the original underline in OC method:
// OC
@implementation JPTableViewController
- (NSArray *) _dataSource {
}
@end
// JS
defineClass("JPTableViewController", {
__dataSource: function() {
},
})
3.You can add prefix ORIG
to call the original method:
// OC
@implementation JPTableViewController
- (void)viewDidLoad {
}
@end
// JS
defineClass("JPTableViewController", {
viewDidLoad: function() {
self.ORIGviewDidLoad();
},
})
The third parameter of defineClass()
is the class method you want to define or override. You should also follow the rules described above:
// OC
@implementation JPTestObject
+ (void)shareInstance
{
}
@end
defineClass("JPTableViewController", {
//实例方法
}, {
//类方法
shareInstance: function() {
...
},
})
You can override method declared in category just like they were in the class itself:
@implementation UIView (custom)
- (void)methodA {
}
+ (void)clsMethodB {
}
@end
defineClass('UIView', {
methodA: function() {
}
}, {
clsMethodB: function() {
}
});
You can use self.super
as the keyword super
in OC, therefore you can call method of super class:
// JS
defineClass("JPTableViewController", {
viewDidLoad: function() {
self.super().viewDidLoad();
}
})
You can use getter and setter method to access or modify property which is defined in OC:
// OC
@interface JPTableViewController
@property (nonatomic) NSArray *data;
@end
@implementation JPTableViewController
@end
// JS
defineClass("JPTableViewController", {
viewDidLoad: function() {
var data = self.data(); //get property value
self.setData(data.toJS().push("JSPatch")); //set property value
},
})
The second parameter of defineClass()
is the properties you want to add. It is an array whose elements are string. These new properties can be accessed and modified in the same way as those defined in OC:
defineClass("JPTableViewController", ['data', 'totalCount'], {
init: function() {
self = self.super().init()
self.setData(["a", "b"]) //Set value of new property (id data)
self.setTotalCount(2)
return self
},
viewDidLoad: function() {
var data = self.data() //Get the value of property
var totalCount = self.totalCount()
},
})
You can use valueForKey()
and setValue_forKey()
to access and modify private member variables:
// OC
@implementation JPTableViewController {
NSArray *_data;
}
@end
// JS
defineClass("JPTableViewController", {
viewDidLoad: function() {
var data = self.valueForKey("_data") //get member variables
self.setValue_forKey(["JSPatch"], "_data") //set member variables
},
})
You can add a new method to any class but type of all values is id
:
// OC
@implementation JPTableViewController
- (void)viewDidLoad
{
NSString* data = [self dataAtIndex:@(1)];
NSLog(@"%@", data); //output: Patch
}
@end
// JS
var data = ["JS", "Patch"]
defineClass("JPTableViewController", {
dataAtIndex: function(idx) {
return idx < data.length ? data[idx]: ""
}
})
If you are implementing a method declared in a protocol, you must specify which protocol you are implementing in defineClass()
. You can get more information about this in next section.
When defining a class, you can force it to conform to a protocol. The syntax looks like OC:
defineClass("JPViewController: UIViewController<UIScrollViewDelegate, UITextViewDelegate>", {
})
As we talked before, type of all values is id when adding a new method to a class. Here, the advantage of doing so is that your types can be inferred from the method definition in protocol:
@protocol UIAlertViewDelegate <NSObject>
...
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex;
...
@end
defineClass("JPViewController: UIViewController <UIAlertViewDelegate>", {
viewDidAppear: function(animated) {
var alertView = require('UIAlertView')
.alloc()
.initWithTitle_message_delegate_cancelButtonTitle_otherButtonTitles(
"Alert",
self.dataSource().objectAtIndex(indexPath.row()),
self,
"OK",
null
)
alertView.show()
}
alertView_clickedButtonAtIndex: function(alertView, buttonIndex) {
console.log('clicked index ' + buttonIndex)
}
})
JSPatch supports the following four struct types: CGRect/CGPoint/CGSize/NSRange, here is how we represent them in JS:
// Obj-C
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(20, 20, 100, 100)];
[view setCenter:CGPointMake(10,10)];
[view sizeThatFits:CGSizeMake(100, 100)];
CGFloat x = view.frame.origin.x;
NSRange range = NSMakeRange(0, 1);
// JS
var view = UIView.alloc().initWithFrame({x:20, y:20, width:100, height:100})
view.setCenter({x: 10, y: 10})
view.sizeThatFits({width: 100, height:100})
var x = view.frame().x
var range = {location: 0, length: 1}
For other Struct types, pleas take a look at this article Add support to struct type
In JS, you can use string to represent a selector:
//Obj-C
[self performSelector:@selector(viewWillAppear:) withObject:@(YES)];
//JS
self.performSelector_withObject("viewWillAppear:", 1)
In JS, you can use both null
and undefined
to represent nil
in OC while nsnull
means NSNull
in OC and null
means NULL
in OC:
//Obj-C
@implemention JPTestObject
+ (BOOL)testNull(NSNull *null) {
return [null isKindOfClass:[NSNull class]]
}
@end
//JS
require('JPTestObject').testNull(nsnull) //return 1
require('JPTestObject').testNull(null) //return 0
If you want to know if an object is nil or not, compare it to false rather than nil:
var url = "";
var rawData = NSData.dataWithContentsOfURL(NSURL.URLWithString(url));
if (rawData != null) {} // This is wrong
// You should compare like this:
if (!rawData){}
//In the source code of JSPatch.js, undefined, null, isNil are converted to false in _formatOCToJS
NSArray, NSString and NSDictionary won't be converted to corresponding JS type, you can use them just like normal NSObject:
//Obj-C
@implementation JPObject
+ (NSArray *)data
{
return @[[NSMutableString stringWithString:@"JS"]]
}
+ (NSMutableDictionary *)dict
{
return [[NSMutableDictionary alloc] init];
}
@end
// JS
require('JPObject')
var ocStr = JPObject.data().objectAtIndex(0)
ocStr.appendString("Patch")
var dict = JPObject.dict()
dict.setObject_forKey(ocStr, 'name')
console.log(dict.objectForKey('name')) //output: JSPatch
If you really need to convert them to corresponding JS type, please use .toJS()
method:
// JS
var data = require('JPObject').data().toJS()
//data instanceof Array === true
data.push("Patch")
var dict = JPObject.dict()
dict.setObject_forKey(data.join(''), 'name')
dict = dict.toJS()
console.log(dict['name']) //output: JSPatch
If you want to pass an JS method as a block to OC, you should use block(paramTypes, function)
to wrap it:
// Obj-C
@implementation JPObject
+ (void)request:(void(^)(NSString *content, BOOL success))callback
{
callback(@"I'm content", YES);
}
@end
// JS
require('JPObject').request(block("NSString *, BOOL", function(ctn, succ) {
if (succ) log(ctn) //output: I'm content
}))
Types of parameters in block are represented as string and separated by comma. You can use id
to represent NSObject like NSString *
and NSArray *
while you should use NSBlock *
to represent a block object.
Blocks given by OC will be converted to JS functions automatically which means you can call it directly(For detailed reason, take a look at issue #155:
// Obj-C
@implementation JPObject
typedef void (^JSBlock)(NSDictionary *dict);
+ (JSBlock)genBlock
{
NSString *ctn = @"JSPatch";
JSBlock block = ^(NSDictionary *dict) {
NSLog(@"I'm %@, version: %@", ctn, dict[@"v"])
};
return block;
}
+ (void)execBlock:(JSBlock)blk
{
}
@end
// JS
var blk = require('JPObject').genBlock();
blk({v: "0.0.1"}); //output: I'm JSPatch, version: 0.0.1
If this block will be passed back to OC, you need to use block()
to wrap it again since the block is a normal JS function now:
// JS
var blk = require('JPObject').genBlock();
blk({v: "0.0.1"}); //output: I'm JSPatch, version: 0.0.1
require('JPObject').execBlock(block("id", blk));
In conclusion: JS doesn't have a block type and blocks in OC will be converted to JS function which means you have to use block()
to wrap it when you are going to pass a JS function to OC.
You can't use self
in block, instead, you have to use a temp variable to store self outside block:
defineClass("JPViewController", {
viewDidLoad: function() {
var slf = self;
require("JPTestObject").callBlock(block(function(){
//`self` is not available here, use `slf` instead.
slf.doSomething();
});
}
}
You will get two limitations when pass blocks from JS to OC:
- The block has at most 6 parameters. You can modify the source code to support more parameters.
- Type of parameters in block can't be double.
Besides, if a block is wrapped in JS and passed to OC, you can't use it in JS when it is given back:
- (void)callBlock:(void(^)(NSString *str))block {
}
defineClass('JPTestObject', {
run: function() {
self.callBlock(block('NSString*', function(str) {
console.log(str);
}));
},
callBlock: function(blk) {
// The blk was defined in run method and passed to OC, you can't use it when given back
blk("test block");
}
});
You can declare a weak variable in JS by calling __weak()
method, so that you can break the retain cycle.
For example, to avoid the retain cycle, our code in OC may look like this:
- (void)test {
__weak id weakSelf = self;
[self setCompleteBlock:^(){
[weakSelf blabla];
}]
}
The corresponding syntax in JS is shown below:
var weakSelf = __weak(self)
self.setCompleteBlock(block(function(){
weakSelf.blabla();
}))
You can call __strong()
method if you want to use a strong variable instead of weak:
var weakSelf = __weak(self)
self.setCompleteBlock(block(function(){
var strongSelf = __strong(weakSelf)
strongSelf.blabla();
}))
The following method can be used to call GCD funcions: dispatch_after()
,dispatch_async_main()
,dispatch_sync_main()
,dispatch_async_global_queue()
.
// Obj-C
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// do something
});
dispatch_async(dispatch_get_main_queue(), ^{
// do something
});
// JS
dispatch_after(1.0, function(){
// do something
})
dispatch_async_main(function(){
// do something
})
dispatch_sync_main(function(){
// do something
})
dispatch_async_global_queue(function(){
// do something
})
If you want to pass a parameter of type id *
, for example, NSError **
parameter of the following method in NSURLConnection
:
+ (NSData *)sendSynchronousRequest:(NSURLRequest *)request returningResponse:(NSURLResponse **)response error:(NSError **)error;
Here, the parameter is a pointer to a NSObject. If you point it to a new object inside the method, the caller can get the object you pointed to after calling the method. To handle this kind of parameter, you have to include JSMemory extension, and follow the guides below:
- Call
malloc(sizeof(id))
to create a new pointer - Pass the pointer into method as a parameter
- Call
pval()
to get the new object after calling the method - Call
releaseTmpObj()
to release this object after using it. - Call
free()
to release the pointer
For an example:
//OC
- (void)testPointer:(NSError **)error {
NSError *err = [[NSError alloc]initWithDomain:@"com.jspatch" code:42 userInfo:nil];
*error = err;
}
//JS
//malloc() pval() free() is provided by JPMemory extension
require('JPEngine').addExtensions(['JPMemory'])
var pError = malloc(sizeof("id"))
self.testPointer(pError)
var error = pval(pError)
if (!error) {
console.log("success")
} else {
console.log(error)
}
releaseTmpObj(pError)
free(pError)
On the other hand,if you want to replace -testPointer:
method in JS and make the pointer parameter refer to a new NSError
object, your code would be like this:
defineClass('JPClassName', {
testPointer: function(error){
var tmp = require('NSError').errorWithDomain_code_userInfo("test", 1, null);
var newErrorPointer = getPointer(tmp)
memcpy(error, newErrorPointer, sizeof('id'))
}
);
Constant and enum in OC can't be used in JS, you have to use the real value instead:
//OC
[btn addTarget:self action:@selector(handleBtn) forControlEvents:UIControlEventTouchUpInside];
//UIControlEventTouchUpInside的值是1<<6
btn.addTarget_action_forControlEvents(self, "handleBtn", 1<<6);
You can define a global variable with the same name as well:
//js
var UIControlEventTouchUpInside = 1 << 6;
btn.addTarget_action_forControlEvents(self, "handleBtn", UIControlEventTouchUpInside);
Values of some constant strings are unknown until you print them out:
//OC
[[NSAttributedString alloc].initWithString:@"str" attributes:@{NSForegroundColorAttributeName: [UIColor redColor]];
The NSForegroundColorAttributeName
in the code above is a static constant value whose value is unknown from the source code. So you can print its value out with NSLog
can use this value in JS:
//OC
NSLog(@"%@", NSForegroundColorAttributeName) //output 'NSColor'
NSAttributedString.alloc().initWithString_attributes("无效啊", {'NSColor': UIColor.redColor()});
Macros in OC can't be used in JS either. You can use the real value if the macro defines a constant value, or you can unwrap the function defined by macro:
#define TABBAR_HEIGHT 40
#define SCREEN_WIDTH [[UIScreen mainScreen] bounds].size.height
[view setWidth:SCREEN_WIDTH height:TABBAR_HEIGHT];
//JS
view.setWidth_height(UIScreen.mainScreen().bounds().height, 40);
If the default value of macro is unknown and may be different in different system, you can define a method to return it and add an extension:
@implementation JPMacroSupport
+ (void)main:(JSContext *)context
{
context[@"CGFLOAT_MIN"] = ^CGFloat() {
return CGFLOAT_MIN;
}
}
@end
require('JPEngine').addExtensions(['JPMacroSupport'])
var floatMin = CGFLOAT_MIN();
Global static variable defined inside the class is not accessible in JS, you have to declare a class method or instance method to return it:
static NSString *name;
@implementation JPTestObject
+ (NSString *)name {
return name;
}
@end
var name = JPTestObject.name() //拿到全局变量值
When overriding swift class, you should use projectName.originalClass
instead of the class name. For example, your code will look like this if you are going to override a swift class called ViewController:
defineClass('demo.ViewController', {})
It is the same when using an existed swift class:
require('demo.ViewController')
Here are some notes you should pay attention to:
- You can only use those swift classes who inherit from
NSObject
- For those swift classes who inherit from
NSObject
, you can only call methods inherited from superclass or swift method withdynamic
keyword. - If the type of parameters only exists in swift(such as Character or Tuple), you can't call it in JS
- It is the same to add new class in swift project as in OC project.
You can read this article for more details
If a dynamic framework exist in iOS system but is not loaded in app, you can load it with the code below. Let's try to load SafariServices.framework
and take it as an example:
var bundle = NSBundle.bundleWithPath("/System/Library/Frameworks/SafariServices.framework");
bundle.load();
You can use SafariServices.framework
after loading it.
You can call console.log()
to print an object to the console of Xcode, just works like NSLog()
.
You can put any parameter into console.log()
except string concatenation with a placeholder: NSLog(@"num:%f", 1.0)
:
var view = UIView.alloc().init();
var str = "test";
var num = 1;
console.log(view, str, num)
console.log(str + num); //You should do the string concatenation in JS
You can also use the debug tools of Safari to set a breakpoint for easier debugging. For more details, please read Use breakpoint to debug JS