Skip to content

Basic Usage of JSPatch

Kt Zhang edited this page Aug 1, 2016 · 8 revisions

0. Index

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

##1. require

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()

2.Invoke OC method

Invoke class method

var redColor = UIColor.redColor();

Invoke instance method

var view = UIView.alloc().init();
view.setNeedsLayout();

Pass parameters

You can pass parameters into method just like in OC:

var view = UIView.alloc().init();
var superView = UIView.alloc().init()
superView.addSubview(view)

Property

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();

Convert name of method

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()

3. defineClass

API

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.

Override method

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();
  },
})

Override class method

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() {
    ...
  },
})

Override category method

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() {
  }
});

Super

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();
  }
})

Property

Get/Modify property declared in OC

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
  },
})

Add property dynamically

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()
  },
})

Private member variable

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
  },
})

Add new method

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.

Protocol

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)
  }
})

4. Special type

Struct

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

Selector

In JS, you can use string to represent a selector:

//Obj-C
[self performSelector:@selector(viewWillAppear:) withObject:@(YES)];
//JS
self.performSelector_withObject("viewWillAppear:", 1)

nil

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

5.NSArray / NSString / NSDictionary

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

6.Block

Pass block

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.

Use self in block

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();
    });
  }
}

Limitation

You will get two limitations when pass blocks from JS to OC:

  1. The block has at most 6 parameters. You can modify the source code to support more parameters.
  2. 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");   
    }
});

7. __weak / __strong

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();
}))

8.GCD

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
})

Pass parameter of type id*

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:

  1. Call malloc(sizeof(id)) to create a new pointer
  2. Pass the pointer into method as a parameter
  3. Call pval() to get the new object after calling the method
  4. Call releaseTmpObj() to release this object after using it.
  5. 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、Enum、Macros、Global variable

Constant/Enum

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

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 variable

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() //拿到全局变量值

11.Swift

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:

  1. You can only use those swift classes who inherit from NSObject
  2. For those swift classes who inherit from NSObject, you can only call methods inherited from superclass or swift method with dynamic keyword.
  3. If the type of parameters only exists in swift(such as Character or Tuple), you can't call it in JS
  4. It is the same to add new class in swift project as in OC project.

You can read this article for more details

12.Link dynamic library

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.

13.Debug

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

Clone this wiki locally