JS&iOS native interaction

Keywords: Javascript Attribute JSON iOS

There is no discussion about the dispute between hybid and native. The main topic is about JS and OC interaction.


Attach a bullish third party to the lecture
JavascriptBridge

OC executes JS code

  • 1.stringByEvaluatingJavaScriptFromString
    This method is the one in UIWebView and the simplest way to interact with JS.
- (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;

Usage is relatively simple, generally used in proxy method - (void)webViewDidFinishLoad:(UIWebView*)webView

// Get the title of the current page
NSString *title = [webview stringByEvaluatingJavaScriptFromString:@"document.title"];

// Get the url of the current page
NSString *url = [webview stringByEvaluatingJavaScriptFromString:@"document.location.href"];

OC Executes JS & JS Executes OC

  • 1.JavaScriptCore
    This is introduced after iOS 7. It can be used easily or more complex.
    Familiarize yourself with several common objects and protocols
JSContext: Provides a context for JavaScript to run. A JS code can be executed using the - evaluateScript: method
 JSValue: A bridge between JavaScript and Objective-C data and methods, encapsulating the corresponding types of JS and ObjC, and calling the API of JS, etc.
JSManagedValue: Classes for managing data and methods
 JSVirtual Machine: Processing thread-related, less used
 JSExport: This is a protocol, and if you interact in a protocol-based way, the protocol you define must comply with it.

Object Profile


Simple way: Call JS code directly

// A JSContext object is similar to window in Js.
// It only needs to be created once.
self.jsContext = [[JSContext alloc] init];

//  jscontext can execute JS code directly.
[self.jsContext evaluateScript:@"var num = 10"];
[self.jsContext evaluateScript:@"var squareFunc = function(value) { return value * 2 }"];
// Calculate the area of a square
JSValue *square = [self.jsContext evaluateScript:@"squareFunc(num)"];

// Method can also be obtained by subscribing
JSValue *squareFunc = self.jsContext[@"squareFunc"];
JSValue *value = [squareFunc callWithArguments:@[@"20"]];
NSLog(@"%@", square.toNumber);
NSLog(@"%@", value.toNumber);

Quick call to Block, passable parameters JS passes in parameters to OC
Various data types can be converted, and the Block of Objective-C can also be passed into JSContext as a JavaScript method. Although JavaScritpCore doesn't come with it (after all, it doesn't run on a Web page, and naturally there won't be classes like window, document, console), you can still define a Block method to call NSLog to simulate:

JSContext *context = [[JSContext alloc] init];
context[@"log"] = ^() {
NSLog(@"+++++++Begin Log+++++++");
NSArray *args = [JSContext currentArguments];
for (JSValue *jsVal in args) {
NSLog(@"%@", jsVal);
}
JSValue *this = [JSContext currentThis];
NSLog(@"this: %@",this);
NSLog(@"-------End Log-------");
};
[context evaluateScript:@"log('ider', [7, 21], { hello:'world', js:100 });"];
//
// Output:
// +++++++Begin Log+++++++
// ider
// 7,21
// [object Object]
// this: [object GlobalObject]
// -------End Log-------

Block successfully calls methods back to Objective-C in JavaScript, and still follows the characteristics of JavaScript methods, such as the method parameters are not fixed. Because of this, JSContext provides a class method to get a list of parameters(+ (JSContext *) current Arguments;) and the object currently calling the method (+ (JSValue) *) CurrtThis). For "this"
The output content is GlobalObject, which is also the JSContext object method - (JSValue *)globalObject;
The content returned. Because we know that in JavaScript, all global variables and methods are attributes of a global variable. In browsers, window s are attributes of a global variable. What is in JavaScript Core is unknown. Block can be passed in as a JSContext method, but JSValue does not have a toBlock method to turn a JavaScript method into a Block for use in Objetive-C. After all, the number of parameters and the type that Block has returned are fixed. Although methods cannot be extracted, JSValue provides- (JSValue *)callWithArguments:(NSArray *)arguments;
A method can in turn pass in parameters to invoke a method. OC parameters are passed into JS

JSContext *context = [[JSContext alloc] init];
[context evaluateScript:@"function add(a, b) { return a + b; }"];
JSValue *add = context[@"add"];
NSLog(@"Func: %@", add);
JSValue *sum = [add callWithArguments:@[@(7), @(21)]];
NSLog(@"Sum: %d",[sum toInt32]);
// OutPut:
// Func: function add(a, b) { return a + b; }
// Sum: 28
  • JSValue
    It also provides -(JSValue *) invokeMethod:(NSString *) method with Arguments:(NSArray) *)arguments;
    Let's call methods on objects directly and simply. But if the method is defined as a global function, it is obvious that the method should be invoked on the globalObject object of JSContext; if it is a method on a JavaScript object, the corresponding JSValue should be used.

Object invocation.
Exceptions to Objective-C exception handling are captured by Xcode at runtime, while JavaScript executed in JSContext, if an exception occurs, is captured by JSContext and stored on the exception attribute without throwing it out. Check JSContext all the time
Whether the object exception is not nil is obviously inappropriate, and the more reasonable way is to give JSContext
Object setting exception handler, which accepts ^ (JSContext context, JSValue exceptionValue)
Formal Block. The default value is to assign the incoming exceptionValue to the exception property of the incoming context:

^(JSContext *context, JSValue *exceptionValue) {
context.exception = exceptionValue;
};

We can also give exceptionHandler
Give a new Block to let us know immediately when an exception occurs to JavaScript running:

JSContext *context = [[JSContext alloc] init];
context.exceptionHandler = ^(JSContext *con, JSValue *exception) {
NSLog(@"%@", exception);
con.exception = exception;
};
[context evaluateScript:@"ider.zheng = 21"];
//Output:
// ReferenceError: Can't find variable: ider
  • Notes for using Block
    From the previous examples and introductions, we should realize that Block plays a powerful role in JavaScript Core, which builds more bridges between JavaScript and Objective-C, and makes interoperability more convenient. However, it should be noted that whether you pass a Block to a JSContext object to make it a JavaScript method or assign it to an exceptionHandler attribute, you should not directly use its externally defined JSContext object or JSValue in a Block. You should pass it into a Block as a parameter or through JSContext's class method+ (JSCon Handler attribute). Text) current Context; to get. Otherwise, it will cause circular references, which will prevent memory from being freed correctly. For example, the custom exception handling method above is assigned to the incoming JSContext object con rather than the context object created outside it, although they are actually the same object. This is because blocks make strong references to objects created by external definitions used internally, and JSContext also makes strong references to given blocks, so that circular references are formed between them and memory cannot be freed properly. For JSValue, it is not possible to refer directly to the block from outside, because JSContext exists on each JSValue.
    References (@property(readonly, retain) JSContext context;), and JSContext re-referencing blocks can also form reference loops.
    The former is very simple and efficient, but it is limited to the basic types of numeric, Boolean, string, array and so on.
    For convenience, all JSContext objects in the following code add the following log
    Method and EvetHandler
    :
JSContext *context = [[JSContext alloc] init];
context.exceptionHandler = ^(JSContext *con, JSValue *exception) {
NSLog(@"%@", exception);
con.exception = exception;
};
context[@"log"] = ^() {
NSArray *args = [JSContext currentArguments];
for (id obj in args) {
NSLog(@"%@",obj);
}
};

Complex but powerful way: through protocol, model implementation (pre-negotiation and front-end negotiation format)
First, we need to define a protocol, and this protocol must comply with the JSExport protocol.
Note how to write parameters when calling oc method in js

@protocol JavaScriptObjectiveCDelegate <JSExport>

// JS calls this method to call OC's system album method
- (void)callSystemCamera;

// When called in JS, the function name should be showAlert Msg (arg1, arg2)
// There are only two parameters.
- (void)showAlert:(NSString *)title msg:(NSString *)msg;

// Through JSON
- (void)callWithDict:(NSDictionary *)params;

// JS calls Oc, and then passes the value to JS by calling JS method in OC.
- (void)jsCallObjcAndObjcCallJsWithDict:(NSDictionary *)params;

@end

To call the corresponding JS method after calling the native method, write two JS methods:

 var jsFunc = function() {
   alert('Objective-C call js to show alert');
 }

 var jsParamFunc = function(argument) {
   document.getElementById('jsParamFuncSpan').innerHTML
   = argument['name'];
 }

Next, we need to define a model to implement the method of defining the protocol above.

// This model is used to inject the JS model so that methods can be invoked through the model.
@interface ObjCModel : NSObject <JavaScriptObjectiveCDelegate>

@property (nonatomic, weak) JSContext *jsContext;
@property (nonatomic, weak) UIWebView *webView;

@end

Implement this model:

@implementation ObjCModel

- (void)callWithDict:(NSDictionary *)params {
 NSLog(@"Js Called OC The parameters of the method are as follows:%@", params);
}

// Js calls System Camera
- (void)callSystemCamera {
 NSLog(@"JS Called OC Method to call up the system album");

 // After the call of JS, OC calls JS through OC, but there is no parameter passed.
 JSValue *jsFunc = self.jsContext[@"jsFunc"];
 [jsFunc callWithArguments:nil];
}

- (void)jsCallObjcAndObjcCallJsWithDict:(NSDictionary *)params {
 NSLog(@"jsCallObjcAndObjcCallJsWithDict was called, params is %@", params);

 // Method of calling JS
 JSValue *jsParamFunc = self.jsContext[@"jsParamFunc"];
 [jsParamFunc callWithArguments:@[@{@"age": @10, @"name": @"lili", @"height": @158}]];
}

- (void)showAlert:(NSString *)title msg:(NSString *)msg {
 dispatch_async(dispatch_get_main_queue(), ^{
   UIAlertView *a = [[UIAlertView alloc] initWithTitle:title message:msg delegate:nil cancelButtonTitle:@"Ok" otherButtonTitles:nil, nil];
   [a show];
 });
}

@end

Next, we inject the JS model into the proxy loaded by webview in the controller.

Note that the method of getting jsContext for webview is self.jsContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

#pragma mark - UIWebViewDelegate
- (void)webViewDidFinishLoad:(UIWebView *)webView {
 self.jsContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

 // This is a better way to invoke methods through models.
 ObjCModel *model  = [[ObjCModel alloc] init];
 self.jsContext[@"OCModel"] = model;
 model.jsContext = self.jsContext;
 model.webView = self.webView;

 self.jsContext.exceptionHandler = ^(JSContext *context, JSValue *exceptionValue) {
   context.exception = exceptionValue;
   NSLog(@"Exception information:%@", exceptionValue);
 };
}

Get context through KVC with the path documentView.webView.mainFrame.javaScriptContext. In this way, we can get the context of JS and inject our model object into this context.

Here we define two JS methods, one is jsFunc, without parameters.
The other is jsParamFunc, with a parameter.

Next, we add the following code to the body in html:

<div style="margin-top: 100px">
<h1>Test how to use objective-c call js</h1>
<input type="button" value="Call ObjC system camera" onclick="OCModel.callSystemCamera()">
<input type="button" value="Call ObjC system alert" onclick="OCModel.showAlertMsg('js title', 'js message')">
</div>

<div>
<input type="button" value="Call ObjC func with JSON " onclick="OCModel.callWithDict({'name': 'testname', 'age': 10, 'height': 170})">
<input type="button" value="Call ObjC func with JSON and ObjC call js func to pass args." onclick="OCModel.jsCallObjcAndObjcCallJsWithDict({'name': 'testname', 'age': 10, 'height': 170})">
</div>

<div>
<span id="jsParamFuncSpan" style="color: red; font-size: 50px;"></span>
</div>

Now you can test the code.

When we click the first button: Call ObjC system camera,
With OCModel.callSystemCamera(), OC methods can be invoked in HTML through JS.
In the OC code, our callSystem Camera method body adds the following two lines of code, that is, get the JS defined in HTML, go to jsFunc, and then call it.

 JSValue *jsFunc = self.jsContext[@"jsFunc"];
 [jsFunc callWithArguments:nil];

This allows OC to be fed back to JS when it calls OC methods.

Look at the following dictionary parameters:

- (void)jsCallObjcAndObjcCallJsWithDict:(NSDictionary *)params {
 NSLog(@"jsCallObjcAndObjcCallJsWithDict was called, params is %@", params);

 // Method of calling JS
 JSValue *jsParamFunc = self.jsContext[@"jsParamFunc"];
 [jsParamFunc callWithArguments:@[@{@"age": @10, @"name": @"lili", @"height": @158}]];
}

Get the jsParamFunc method we defined in HTML, then call it and pass a dictionary as a parameter.

There are a few steps, but straightening them out is really good.

Notes for JavaScript Core

JavaStript calls local methods are executed in sub-threads, where switching between threads should be considered according to the actual situation. When calling back JavaScript methods, it is better to execute the code of that JavaStript method in the thread that initially calls this method.

Special Processing Based on url Prefix (Protocol Interception) - Simple Passing Parameters

The proxy method in UIWebView:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *) Request Navigation Type: (UIWebView Navigation Type) Navigation Type determines the url prefix, and then performs special processing according to the protocol type.

If we need to accept parameters from the url, we can splice the parameters onto the URL and pass them to the native. However, this method is limited.

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
    NSString *urlStr = request.URL.absoluteString;
    if ([urlStr rangeOfString:@"test://"].location != NSNotFound) {
        // The protocol header of url is the special processing of test
        NSLog(@"test");

        NSURL *url = [NSURL URLWithString:urlStr];
        NSString *scheme  = url.scheme;
        NSString *host = url.host;
        NSString *qurey = url.query;
        NSString *parameter = url.parameterString;

       // Further processing based on parameters
       // TODO

        return NO;
    }
    return YES;
}

This approach also needs to be unified with the server first.

Posted by someone on Fri, 12 Jul 2019 16:02:59 -0700