This chapter mainly introduces oc and JS interaction, divided into UIWebView and WKWebView- case demo

UIWebView

UIWebView is an earlier webView, in ios8 launched WKWebView to replace it, and the fluency is higher, and ios12 has abandoned UIWebView, so now use webView directly use WKWebView, but some old code needs to be adapted to the new, Or if you’re still using UIWebView until ios12, you can see UIWebView below

Refer to the WebViewController file in the demo

The basic functions of UIWebView

Load a webView with the following lines of code

self.webView = [[UIWebView alloc] initWithFrame:self.view.frame];
NSURL *url = [[NSBundle mainBundle] URLForResource:@"index.html" withExtension:nil];
[self.webView loadRequest:[NSURLRequest requestWithURL:url]];
self.webView.delegate = self;
[self.view addSubview:self.webView];
Copy the code

UIWebView protocol calls js and OC

Through the implementation of the webView protocol UIWebViewDelegate, you can detect and intercept webView events

ShouldStartLoadWithRequest method can intercept routing directly, when click the route events can intercept directly, and performs functions of oc in oc method, namely routing calls can be achieved with js oc method, as shown below

The route is shown below, as shown in the HTML file <a href= in the demo file"marshal://getSum/helloword/js"> </a>// Whether to load the corresponding content, which can be used to intercept relevant information
- (BOOL)webView:(UIWebView *)webView 
    shouldStartLoadWithRequest:(NSURLRequest *)request 
    navigationType:(UIWebViewNavigationType)navigationType {
    NSLog(@"% @", request);
    NSURL *url = request.URL; //url
    NSArray *pathComponents = url.pathComponents; // Address set
    NSString *scheme = url.scheme; // An identifier starting with scheme
    if ([scheme isEqualToString:@"marshal"]) {
        // You can set the route and intercept the content according to the route
        // This implements a js call to the oc method, passing parameters can be written later
        //NSLog(@"%@", url.port);
        if ([url.host isEqualToString:@"getSum"]) {
            // Call the oc method by intercepting the route
            [self getSum];
        }else if ([url.host isEqualToString:@"getMessage"]) {
            [self getMessage:webView];
        }
        return NO;
    }
    return true;
}

- (void)getSum {
    NSLog(@"GetSum is called.");
}

- (void)getMessage:(UIWebView *)webView {
    NSLog(@"GetMessage is called"); Through oc call js method [webView stringByEvaluatingJavaScriptFromString: @"handleMessage()"];
}

// Start loading
- (void)webViewDidStartLoad:(UIWebView *)webView {
    NSLog(@"Start loading");
}

// The height of the page can be calculated after loading successfully, etc
- (void)webViewDidFinishLoad:(UIWebView *)webView {
    NSLog(@"Loaded");
// [self callJSMethod: webView];
    [self callJSMethodByJSCore];
}

// Load failed
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error {
    NSLog(@"Load failed");
}
Copy the code

The OC call js method is simple, through stringByEvaluatingJavaScriptFromString webView call

[webView stringByEvaluatingJavaScriptFromString:@"handleMessage()"];
Copy the code

JavaScriptCore implements JAVASCRIPT and OC interaction

Using the JavaScriptCore framework, you can get the JSContext object

Once obtained, you can use the evaluateScript method of the JSContext object to call js functions directly or execute some JS script code (for example, declare some basic objects)

Through JSContext object, through the following methods JSContext @ “showMessage” this way, you can directly declare the js can directly call the method, can be understood as the statement in js, the proposed method in js showMessage as its function can be called directly

- (void)callJSMethodByJSCore {
    // Get the page title
    NSString *naviTitle = [self.webView 
    stringByEvaluatingJavaScriptFromString: @"document.title"];
    NSLog(@"naviTitle : %@", naviTitle);
    
    // Get the js context object
    JSContext *jsContext = [self.webView 
    valueForKeyPath: @"documentView.webView.mainFrame.javaScriptContext"];
    // Execute javascript code according to context object, declare array object (var variable promotion)
    [jsContext evaluateScript:@Var arr = [' arr ', 'arr ',' arr '];];
    // Call the js method directly
    / / [jsContext evaluateScript: @ "showAlert () 'test' (' handsome ')");
    // Bridge js to save the handleMessage function
    // Note: if the function is different, do not overwrite the method in JS with the same name as the method in js.
    jsContext[@"showMessage"] = ^(id value){
        NSLog(@"All information displayed: %@", value);
        // You can view variable parameters
        NSArray *args = [JSContext currentArguments];
        NSLog(@"args = %@",args);
    };
}

Copy the code

The above realizes the interaction between js and oc. WebViewJavascriptBridge will be introduced later

WKWebView

WKWebView as the mainstream WebView display tool, we can not do without him, the following introduction to his basic use, as well as js interaction

Refer to the TestWKWebViewController file in demo

** Note: **WKWebView will have a cookie loss problem, please refer to the cookie solution

Basic functions of WKWebView

WKWebView is different from UIWebView. If you do not add a script in front of you, the page size will be very uncomfortable (small font, etc.), and it will look like UIWebView

// There is a problem with not adding webView display size
    NSString *jScript = @"var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', 'width=device-width'); document.getElementsByTagName('head')[0].appendChild(meta);";
    WKUserScript *wkUScript = [[WKUserScript alloc] initWithSource:jScript injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
    WKUserContentController *wkUController = [[WKUserContentController alloc] init];
    [wkUController addUserScript:wkUScript];
    
    WKWebViewConfiguration *wkWebConfig = [[WKWebViewConfiguration alloc] init];
    wkWebConfig.userContentController = wkUController;
    
    _wkWebView = [[WKWebView alloc]initWithFrame:self.view.frame configuration:wkWebConfig];
    _wkWebView.UIDelegate = self;
    _wkWebView.navigationDelegate = self;
    // Use kVO to monitor progress
// [_wkWebView addObserver:self forKeyPath:NSStringFromSelector(@selector(estimatedProgress)) options:NSKeyValueObservingOptionNew context:nil];
    // Gesture touch swipe
    _wkWebView.allowsBackForwardNavigationGestures = YES;
    
    NSURL *url = [[NSBundle mainBundle] URLForResource:@"index.html" withExtension:nil];
    NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:15];
    [_wkWebView loadRequest:request];
    
    [self.view addSubview:_wkWebView];
Copy the code

WKWebView agreement

If you want to use WKWebView well, you need to implement WKUIDelegate, WKNavigationDelegate two protocols

Note: js cannot alert the default popover if the following protocol is not implemented in WKWebView WKUIDelegate

- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void(^) (void))completionHandler
{
    
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Remind" message:message preferredStyle:UIAlertControllerStyleAlert];
    [alert addAction:[UIAlertAction actionWithTitle:@"Got it." style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
        completionHandler();
    }]];

    [self presentViewController:alert animated:YES completion:nil];
}
Copy the code

NaivigationDelegate agreement

This protocol is similar to the UIWebView protocol and works similarly, as shown below

This method is called when the user clicks on a link on a web page and needs to open a new page, just like webView interception
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    NSURL *url = navigationAction.request.URL; //url
    NSArray *pathComponents = url.pathComponents; // Address set
    NSString *scheme = url.scheme; // An identifier starting with scheme
    if ([scheme isEqualToString:@"marshal"]) {
        // You can set the route and intercept the content according to the route
        // This implements a js call to the oc method, passing parameters can be written later
        //NSLog(@"%@", url.port);
        if ([url.host isEqualToString:@"getSum"]) {
            [self getSum];
        }else if ([url.host isEqualToString:@"getMessage"]) {}else if ([url.host isEqualToString:@"enterWk"]) {
            
        }
        decisionHandler(WKNavigationActionPolicyCancel);
    }else{ decisionHandler(WKNavigationActionPolicyAllow); }}Copy the code

The following protocol usage scenarios are similar to UIWebView, and can be implemented according to the actual usage scenarios. The most used ones are Finish and fail

// After receiving the corresponding data, decide whether to jump
//- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler;
//// is called when the page starts loading
//- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation;
//// called when the host address is redirected
//- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(null_unspecified WKNavigation *)navigation;
//// called when the page fails to load
//- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error;
//// called when content starts to return
//- (void)webView:(WKWebView *)webView didCommitNavigation:(null_unspecified WKNavigation *)navigation;
//// is called when the page is loaded
//- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation;
//// called when the jump fails
//- (void)webView:(WKWebView *)webView didFailNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error;
//// If certificate authentication is required, it is the same as HTTPS certificate authentication using AFN
//- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *__nullable credential))completionHandler;
////9.0, triggered when web content processing is interrupted
//- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView NS_AVAILABLE(10_11, 9_0);
Copy the code

WKWebView implements OC and JS interaction

Default js calls the oc and the webView similar, in NaivigationDelegate protocol based on routing intercept method, and then the method to call oc, refer to the above decidePolicyForNavigationAction agreement

Oc transfer JS to call the following method, you can directly call the function and declare variables

[self.wkWebView evaluateJavaScript:@Var arr = [' arr ', 'arr ',' arr ']; 
    completionHandler:^(id _Nullable returnObject, NSError * _Nullable error) {
    //returnObject returns value content
    if (error) {
        NSLog(@"% @", error.localizedDescription); // Error message}}]; [self.wkWebView evaluateJavaScript:@"ShowAlert (' You're really ')(' Gorgeous ')" 
    completionHandler:^(id _Nullable returnObject, NSError * _Nullable error) {
    //returnObject returns value content
    if (error) {
        NSLog(@"% @", error.localizedDescription); // Error message}}];Copy the code

Oc and JS interaction is implemented through MessageHandler

When using MessageHandle, you need to register MessageHandle and remove it at an appropriate time, similar to using Timer

// Register MessageHandle in oc
[self.wkWebView.configuration.userContentController 
    addScriptMessageHandler:self name:@"OCHandleMessage"]; Note that adding and removing handles must occur in pairs; otherwise, memory leaks will occur// Remove the handler. If you do not remove it, it will leak memory. You can remove the Handle at an appropriate time
[self.wkWebView.configuration.userContentController 
    removeScriptMessageHandlerForName: @"OCHandleMessage"];
Copy the code

Oc calls the js methods, as shown below

[self.wkWebView evaluateJavaScript:@"showMessage()" 
    completionHandler:^(id _Nullable returnObject, NSError * _Nullable error) {
}];
Copy the code

You also need to implement the WKScriptMessageHandler protocol to implement the interaction

// This is where you can receive data from JS via Handle
- (void)userContentController:(WKUserContentController *)userContentController 
    didReceiveScriptMessage:(WKScriptMessage *)message {
// NSLog(@"%@ -- %@", message.name, message.body);
    // If you want to feedback after execution, you can call back the method in js
    [self.wkWebView evaluateJavaScript:@"showMessage()" 
        completionHandler:^(id _Nullable returnObject, NSError * _Nullable error) {
            
    }];
}
Copy the code

Handler is set to OCHandleMessage via the addScriptMessageHandler method, as shown above. (It can also be set to anything else, which is the name of the mediation object for an interaction setting. You can set more than one.)

After this is set, you can use this name in JS to send messages directly to oc. If you want to separate messages, you can switch Messagehandle or add labels to separate messages

The parameters passed in the following method go to the userContentController protocol above, where the content is in message

// method calls in js
function messageHandle(){
    // Handle sends messages to the OC. OCHandleMessage is the handle name set
    window.webkit.messageHandlers.OCHandleMessage.postMessage("MessageHandle news");
}
Copy the code

This allows oc and JS to interact

Use WebViewJavascriptBridge to implement oc and js interaction

WebViewJavascriptBridge is a component that Bridges UIWebView and WKWebView interaction. It is very convenient to use. Of course, if you only use WKWebView now, you can directly discard it to reduce the code size

The principle is that the WebViewJavascriptBridge acts as a bridge in the middle and is responsible for bridging the data interaction between the two parts of the translation

Add WebViewJavascriptBridge to your podfile and install

pod 'WebViewJavascriptBridge'
Copy the code

Declare the bridge object in oc before use, then bridge webView(UIWebView and wkWebView), similar to WkWekView total Messagehandler

@property (nonatomic, strong) WebViewJavascriptBridge *wjb;

self.wjb = [WebViewJavascriptBridge bridgeForWebView:self.webView];
// If you want to implement UIWebView proxy methods in VC, set the proxy, otherwise omit
[self.wjb setWebViewDelegate:self];

[self.wjb registerHandler:@"jsCallsOC" handler:^(id data, WVJBResponseCallback responseCallback) {
    NSLog(@"data == %@ -- %@",data,responseCallback);
}];
Copy the code

Use WebViewJavascriptBridge to register functions and call functions in js as shown below

// Set it to the handler of the js call
- (void)registerJShandle {
    [self.wjb registerHandler:@"jsCallsOC" handler:^(id data, WVJBResponseCallback responseCallback) {
        NSLog(@"data == %@ -- %@",data,responseCallback);
        responseCallback(@"Test data"); // Callback data response
    }];
    [self.wjb registerHandler:@"jsCallsOC1" handler:^(id data, WVJBResponseCallback responseCallback) {
        NSLog(@"data == %@ -- %@",data,responseCallback);
        responseCallback(@"Test data"); // Callback data response
    }];
}
// Call handle registered in a function in js
- (void)callJSHandle {
    [self.wjb callHandler:@"OCCallJS" data:@"Test data ha." responseCallback:^(id responseData) {
        NSLog(@"responseData == %@",responseData);
    }];
    [self.wjb callHandler:@"OCCallJS1" data:@"Test data ha." responseCallback:^(id responseData) {
        NSLog(@"responseData == %@",responseData);
    }];
}
Copy the code

Js similar, need to achieve the following code

In JS, bridge registration and handler calls are used to achieve interaction, and there is no distinction between UIWebView and WKWebView

// Call each other to add this method
function setupWebViewJavascriptBridge(callback) {
    if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
    if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
    window.WVJBCallbacks = [callback];
    var WVJBIframe = document.createElement('iframe');
    WVJBIframe.style.display = 'none';
    WVJBIframe.src = 'wvjbscheme://__BRIDGE_LOADED__';
    document.documentElement.appendChild(WVJBIframe);
    setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}

setupWebViewJavascriptBridge(function(bridge) {
    // Register the handle where JS is called for oc calls
    bridge.registerHandler('OCCallJS'.function(data, responseCallback) {
        alert(The OCCallJS method is called:+data);
        responseCallback('JS executed');
    })
    bridge.registerHandler('OCCallJS1'.function(data, responseCallback) {
        alert(The OCCallJS1 method is called:+data);
        responseCallback('JS executed');
    })
    //oc calls handle registered in js and can pass receive arguments
    bridge.callHandler('jsCallsOC'The @"Test data ha.".function(response) {
         alert(response);
    })
    bridge.callHandler('jsCallsOC1'The @"Test data ha.".function(response) { alert(response); })})Copy the code

Cookies in WebView

Refer to this article for an introduction to cookies below

Cookies, sometimes used in the plural, refer to data stored (usually encrypted) on a user’s local terminal by some web sites for identification and session tracking.

UIWebView cookies

UIWebView’s cookies are generally managed by the singleton [NSHTTPCookieStorage sharedHTTPCookieStorage], and UIWebView will automatically synchronize the cookies in the singleton. Special cases can be achieved by adding Cookie differentiation in the following ways

Add header implementation

NSMutableURLRequest *request = [NSMutableURLRequest 
    requestWithURL:[NSURL URLWithString:@"http://www.LynkCo.com"]];
[request addValue:@"cookieName=123456;" forHTTPHeaderField:@"Set-Cookie"];
[self.webView loadRequest:request];
Copy the code

Add a custom Cookie by manipulating NSHTTPCookieStorage

NSHTTPCookie *cookie = [NSHTTPCookie 
    cookieWithProperties: @ {NSHTTPCookieName: @"cookieName".NSHTTPCookieDomain: @".juejin.cn".NSHTTPCookiePath: @"/"
    NSHTTPCookieValue: @"123456",}]; [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie: Cookie];Copy the code

All cookies are read, converted to HTTPHeaderFields, and added to the header of the request

NSArray *cookies = [NSHTTPCookieStorage sharedHTTPCookieStorage].cookies;
//Cookies array is converted to requestHeaderFields
NSDictionary *requestHeaderFields = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies];
// Set the request header
request.allHTTPHeaderFields = requestHeaderFields
Copy the code

WKWebView cookie loss problem

The cookies that WKWebView gets when it loads the page are synchronized to NSHTTPCookieStorage.

WKWebView does not synchronize existing cookies in NSHTTPCookieStorage when it loads the request.

Sharing a WKProcessPool does not solve the Cookie synchronization problem and may result in Cookie loss.

WKWebView failed to load cookies for the first time

Add a cookie to the request header, so as long as your cookie is in [NSHTTPCookieStorage sharedHTTPCookieStorage] (set as UIWebView), the first request will not be a problem.

NSMutableURLRequest *request = [NSMutableURLRequest 
    requestWithURL:[NSURL URLWithString:@"http://www.juejin.cn"]];

NSArray *cookies = [NSHTTPCookieStorage sharedHTTPCookieStorage].cookies;

//Cookies array is converted to requestHeaderFields
NSDictionary *requestHeaderFields = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies];

// Set the request header
request.allHTTPHeaderFields = requestHeaderFields;
[self.webView loadRequest:request];
Copy the code

Ajax request cookie loss problem

Just add WKUserScript and make sure your Cookie exists in sharedHTTPCookieStorage and subsequent Ajax requests will be fine.

Update webView cookie - (void)updateWebViewCookie
{
    WKUserScript * cookieScript = [[WKUserScript alloc] initWithSource:[self cookieString] 
        injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
    / / add a Cookie
    [self.configuration.userContentController addUserScript:cookieScript];
}

- (NSString *)cookieString
{
    NSMutableString *script = [NSMutableString string];
    [script appendString:
    @"var cookieNames = document.cookie.split('; ').map(function(cookie) { return cookie.split('=')[0] } ); \n"];
    for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]) {
        // Skip cookies that will break our script
        if ([cookie.value rangeOfString:@"'"].location ! = NSNotFound) {continue;
        }
        // Create a line that appends this cookie to the web view's document's cookies
        [script appendFormat:
            @"if (cookieNames.indexOf('%@') == -1) { document.cookie='%@'; }; \n",
            cookie.name, cookie.da_javascriptString];
    }
    return script;
}
Copy the code

Cookies on the redirect page are lost

- (NSURLRequest *)fixRequest:(NSURLRequest *)request {NSMutableURLRequest *fixedRequest;if ([request isKindOfClass:[NSMutableURLRequest class]]) {
        fixedRequest = (NSMutableURLRequest *)request;
    } else {
        fixedRequest = request.mutableCopy;
    }
    // Prevent Cookie loss
    NSDictionary *dict = [NSHTTPCookie 
        requestHeaderFieldsWithCookies:[
            NSHTTPCookieStorage sharedHTTPCookieStorage].cookies];
    if (dict.count) {
        NSMutableDictionary *mDict = request.allHTTPHeaderFields.mutableCopy;
        [mDict setValuesForKeysWithDictionary:dict];
        fixedRequest.allHTTPHeaderFields = mDict;
    }
    return fixedRequest;
}

#pragma mark - WKNavigationDelegate 
- (void)webView:(WKWebView *)webView 
    decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction 
    decisionHandler: (void(^)(WKNavigationActionPolicy))decisionHandler {# Warning important// Resolve Cookie loss
    NSURLRequest *originalRequest = navigationAction.request;
    [self fixRequest:originalRequest];
    // If originalRequest is NSMutableURLRequest, it can jump to originalRequest if necessary cookies are added to originalRequest
    // Allow a jump
    decisionHandler(WKNavigationActionPolicyAllow);
    // If originalRequest is NSURLRequest, the Cookie cannot be added to the originalRequest. Otherwise there will be an infinite loop, specific, you can use the local HTML test
    NSLog(@"% @", NSStringFromSelector(_cmd));
}

#pragma mark - WKUIDelegate
- (WKWebView *)webView:(WKWebView *)webView 
        createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration 
        forNavigationAction:(WKNavigationAction *)navigationAction 
        windowFeatures(WKWindowFeatures *)windowFeatures {# Warning important// Do not open a new window here
    [self.webView loadRequest:[self fixRequest:navigationAction.request]];
    return nil;
}
Copy the code