WKWebView profile

WKWebView is the core control of WebKit launched after Apple iOS8, which is used to replace UIWebView. Compared with UIWebView, WKWebView has the following advantages:

  1. More support for HTML5 features
  2. Up to 60fps scrolling refresh rate and built-in gestures
  3. Nitro javascript engine, same as Safari
  4. Split UIWebViewDelegate and UIWebView into 14 classes and 3 protocols
  5. You can get the load progress without calling a private API

WKWebView basic usage

create

There are two ways to create a WKWebView

/*-initWithFrame: to initialize an instance with the default configuration. If The initWithFrame method is used, The default configuration The Initializer copies The Specified Configuration will be used. so mutating the configuration after invoking the initializer has no effect on the web view. We need to set configuration and then call init, */ - (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration NS_DESIGNATED_INITIALIZER; - (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER;Copy the code

WKWebView has one more configuration than UIWebView. You can configure many things, such as whether JS support, picture in picture, whether inline video playback, preference Settings, etc. You can check the details in WKWebViewConfiguration.

  • The main point is websiteDataStore
/ *! @abstract The website data store to be used by the web view. */ @property (nonatomic, Strong) WKWebsiteDataStore *websiteDataStore API_AVAILABLE(MacOSx (10.11), ios(9.0));Copy the code

It is generally believed that WKWebView has its own private storage, and some cached data is stored in the websiteDataStore. You can add, delete, change, or insert data by using the methods provided in wkWebSiteDatastore.h. To clear the cache, you need to delete the cache file from the sandbox

  • userContentController
/ *! @abstract The user content controller to associate with the web view. */ @property (nonatomic, strong) WKUserContentController *userContentController;Copy the code

This property is important for JS interaction with OC and JS code injection. A few methods can be found in the WKUserContentController header

@interface WKUserContentController : NSObject <NSCoding> @property (nonatomic, readonly, copy) NSArray<WKUserScript *> *userScripts; // add a script - (void)addUserScript:(WKUserScript *)userScript; // Remove all added scripts - (void)removeAllUserScripts; / / by the window. Its. MessageHandlers. < name >. PostMessage (< messageBody >) to implement the js - > oc message, Add handler - (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name; / / remove handler - (void) removeScriptMessageHandlerForName (nsstrings *) name; @endCopy the code

Create WKWebView code in project:

WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init]; WKUserContentController *controller = [[WKUserContentController alloc] init]; configuration.userContentController = controller; self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:configuration]; self.webView.allowsBackForwardNavigationGestures = YES; / / allow the right slide back on a link, left slide forward self. WebView. AllowsLinkPreview = YES; / / allow link 3 d Touch self. WebView. CustomUserAgent = @ "WebViewDemo / 1.0.0"; Self. WebView.UIDelegate = self; self.webView.navigationDelegate = self; [self.view addSubview:self.webView];Copy the code

Dynamically injected JS

Add WKUserScript to userContentController to implement dynamic JS injection. Such as injecting a script to add cookies to the page

// Inject a Cookie WKUserScript *newCookieScript = [[WKUserScript alloc] initWithSource:@"document. Cookie = 'DarkAngelCookie=DarkAngel;'" injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO]; [controller addUserScript:newCookieScript];Copy the code

Then inject a script that will alert the current page cookie whenever the page loads, implemented in OC

WKUserScript *cookieScript = [[WKUserScript alloc] initWithSource:@"alert(document.cookie);" injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:NO]; // Add the script [controller addUserScript:script];Copy the code

The injected JS source can be any JS string or js file. For example, if there are many JS methods available for h5 use, you can have native_functions.js, which can be added in the following way

Static NSString *jsSource; static NSString *jsSource; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ jsSource = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"native_functions" ofType:@"js"] encoding:NSUTF8StringEncoding error:nil]; }); // Add custom script WKUserScript *js = [[WKUserScript alloc] initWithSource:jsSource injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:NO]; [self.configuration.userContentController addUserScript:js];Copy the code

loading

The API for loading the page

- (nullable WKNavigation *)loadRequest:(NSURLRequest *)request; - (nullable WKNavigation *)loadFileURL:(NSURL *)URL allowingReadAccessToURL:(NSURL *)readAccessURL API_AVAILABLE (macosx (10.11), the ios (9.0)); - (nullable WKNavigation *)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL; - (nullable WKNavigation *)loadData:(NSData *)data MIMEType:(NSString *)MIMEType characterEncodingName:(NSString *)characterEncodingName baseURL:(NSURL *)baseURL API_AVAILABLE(macosx(10.11), ios(9.0));Copy the code

Loading an HTML file locally requires the loadRequest: method. Using the loadHTMLString:baseURL: method can be problematic

[self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"test" ofType:@"html"]]]];
Copy the code

The agent

In the WKWebView header file, you can see two agents

@protocol WKNavigationDelegate; @protocol WKUIDelegate; @protocol WKUIDelegate; // Mainly some alert, open a new window and so onCopy the code

UIWebView’s proxy is split into a jump protocol and a UI protocol. While both agreements are Optional, there are some problems. Let’s start with common usage

/ / the following two methods corresponding to the UIWebView together - (BOOL) webView: (UIWebView *) webView shouldStartLoadWithRequest (NSURLRequest *) request navigationType:(UIWebViewNavigationType)navigationType; / / first: an action to determine whether to allow a jump, the action of access request, allow or not need to call decisionHandler, such as decisionHandler (WKNavigationActionPolicyCancel); - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction DecisionHandler (void (^) (WKNavigationActionPolicy)) decisionHandler; After the / / : according to the response to decide, whether to allow a jump, allow or not need to call decisionHandler, such as decisionHandler (WKNavigationResponsePolicyAllow); - (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler; // Start loading, corresponding to UIWebView - (void)webViewDidStartLoad (UIWebView *)webView; - (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation; UIWebView - (void)webViewDidFinishLoad:(UIWebView *)webView; - (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation; UIWebView - (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error; - (void)webView:(WKWebView *)webView didFailNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error;Copy the code

The new attribute

Wkwebview.h defines the following common readonly attributes:

@property (nullable, nonatomic, readonly, copy) NSString *title; @property (nullable, nonatomic, readOnly, copy) NSURL *URL; // webView URL @property (nonatomic, ReadOnly, getter=isLoading) BOOL loading; // Whether @property (nonatomic, readonly) double estimatedProgress is loading; @property (nonatomic, readonly) BOOL canGoBack; @property (nonatomic, readOnly) BOOL canGoForward; // Can I advance? Same as UIWebViewCopy the code

These attributes all support KVO, which you can use to observe changes in these values

JavaScript interaction with Objective-C

OC -> JS

This one is relatively simple. WKWebView provides a JavaScriptCore like method

// Execute a section of js and return the result. If something goes wrong, EvaluateJavaScript (NSString *)javaScriptString completionHandler (void (^ _Nullable)(_Nullable ID) result, NSError * _Nullable error))completionHandler;Copy the code

This method is very good to solve the mentioned in previous articles use UIWebView stringByEvaluatingJavaScriptFromString: method of two shortcomings

  1. The return value can only be NSString.
  2. An error message could not be caught.

For example, if I want to get the title from the page, instead of directly self.webview. title, I can use this method:

[self.webView evaluateJavaScript:@"document.title" completionHandler:^(id _Nullable title, NSError * _Nullable error) {NSLog(@" evaluateJavaScript: %@, title);}];Copy the code

JS -> OC

  • The url to intercept

It is the same with the URL interception method introduced by UIWebView, which is through a custom Scheme. When the link is activated, the URL is intercepted, the parameter is obtained, and the OC method is called. The disadvantages are still obvious.

  • ScriptMessageHandler, this method is actually used in the project
/ *! @abstract Adds a script message handler. @param scriptMessageHandler The message handler to add. @param name The name of  the message handler. @discussion Adding a scriptMessageHandler adds a function window.webkit.messageHandlers.<name>.postMessage(<messageBody>) for all frames. */ - (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name; / *! @abstract Removes a script message handler. @param name The name of the message handler to remove. */ - (void)removeScriptMessageHandlerForName:(NSString *)name;Copy the code

Add a scriptMessageHandler in oc, will be added in the all frames a js function: Windows. Its. MessageHandlers.. PostMessage (messageBody), such as:

[controller addScriptMessageHandler:self name:@"currentCookies"]; // Here self follows the cooperative WKScriptMessageHandlerCopy the code

When I call the following method in js

window.webkit.messageHandlers.currentCookies.postMessage(document.cookie);
Copy the code

I will receive a callback from WKScriptMessageHandler at OC

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { if ([message.name isEqualToString:@"currentCookies"]) { NSString *cookiesStr = message.body; //message.body returns an object of type ID, so it can support many js argument types (except js function) NSLog(@" currently cookie: %@", cookiesStr); }}Copy the code

Call in delloc removeScriptMessageHandler

- (void) dealloc {/ / remember to remove [self. WebView. Configuration. UserContentController removeScriptMessageHandlerForName:@"currentCookies"]; }Copy the code

Cookie management

WKWebView cookie problem is really pit, after more than half a year, always encounter some weird problems, the following will sum up matters for attention: When we use UIWebView, we don’t have to worry about using cookies, we just need to write a method to inject our user’s HTTPCookie into our HTTPCookie. Cookies are automatically synchronized between different UIWebViews in the same application. It can also be accessed by other network classes such as NSURLConnection and AFNetworking. WKWebView ignores the default network storage, such as NSURLCache, NSHTTPCookieStorage, and NSCredentialStorage. NSHTTPCookieStorage does not actively store cookies, network classes cannot access WKWebView’s own managed cookies, There is also no way to fetch cookies from NSHTTPCookieStorage for WKWebView requests. So when WKWebView needs to initiate a network request, we need to manually inject cookies. Store the cookie information in NSHTTPCookieStorage, and then get the cookie from NSHTTPCookieStorage at the appropriate time to inject WKWebView. Here are some specific questions: What is the Cookie injection scheme of our client app now? What kind of problems have been solved? Why use NSHTTPCookieStorage to access cookies if you can’t share cookies in NSHTTPCookieStorage?

What is the client’s current Cookie injection scheme? What kind of problems have been solved?

  • Scheme 1: WKWebView initiates a network request to inject cookies into the request header, which solves the problem that cookies cannot be obtained for the first time
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://www.baidu.com"]]; NSArray *cookies = [NSHTTPCookieStorage sharedHTTPCookieStorage].cookies; //Cookies array converted to requestHeaderFields NSDictionary *requestHeaderFields = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies]; / / set the request header request. AllHTTPHeaderFields = requestHeaderFields; [self.webView loadRequest:request];Copy the code

That way, as long as you ensure that your Cookie exists in sharedHTTPCookieStorage, the first time you visit a page, you won’t have a problem.

  • Scheme 2: In WKWebView WKNavigationDelegate agent method decidePolicyForNavigationAction intercepting network requests to the request header into the Cookie into the Cookie, Solved the problem that cookies could not be brought to a new page
  • Solution 3: In WKWebView WKNavigationDelegate agent method decidePolicyForNavigationAction intercepting network requests into the Cookie, solved the Cookie content change, open a new page Returns the problem that the Cookie of the parent page is not updated

After the test, scheme 3 can solve the problem that scheme 2 can solve at the same time, so the client’s current Cookie injection scheme is the same as scheme 1 and Scheme 3

Why use WKHTTPCookieStore to access cookies if you can’t share cookies in NSHTTPCookieStorage?

  • Apple provides two classes NSHTTPCookieStorage and NSHTTPCookie to store cookies.
  • Cookie information is not only limited to the network request of WKWebView, but also required by the network request of some native pages. In addition, network classes such as NSURLConnection and AFNetworking obtain Cookie information by accessing NSHTTPCookieStorage.
  • If Cookie information is stored in other ways, we have to manage not only WKWebView’s cookies, but also whether non-WKWebView web requests can get cookies.

Cookies can not be carried after login

  • Symptom: If you do not log in to the H5 page, click the button to pop up the login page. After successful login, click the button again to pop up the login page, and the captured cookie is empty
  • Cause: After login, login status is written into cookie and saved in NSHTTPCookieStorage, but not synchronized to WKHTTPCookieStore, so H5 cannot obtain cookie
  • If no, log in and reload loadRequest

Cookie exception loss

Due to the fact that WKWebView loads web pages and gets cookies that are synchronized to NSHTTPCookieStorage, sometimes the cookies you forcibly add will get lost in the synchronization process. If you click on a link, the Request header contains the set-cookie field, but the Cookie is already missing

  • Solution: Take the initiative to save the Cookie you need, every time you call [NSHTTPCookieStorage sharedHTTPCookieStorage]. Cookies, ensure that the returned array has the Cookie you need, The runtime Method Swizzling code is as follows:

Save cookies when appropriate

NSArray *allCookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]; for (NSHTTPCookie *cookie in allCookies) { if ([cookie.name isEqualToString:DAServerSessionCookieName]) { NSDictionary *dict = [[NSUserDefaults standardUserDefaults] dictionaryForKey:DAUserDefaultsCookieStorageKey]; if (dict) { NSHTTPCookie *localCookie = [NSHTTPCookie cookieWithProperties:dict]; if (! [the cookie value isEqual: localCookie value]) {NSLog (@ "local cookies have update"); } } [[NSUserDefaults standardUserDefaults] setObject:cookie.properties forKey:DAUserDefaultsCookieStorageKey]; [[NSUserDefaults standardUserDefaults] synchronize]; break; }}Copy the code

Add if cookie is empty when read

@implementation NSHTTPCookieStorage (Utils) + (void)load { class_methodSwizzling(self, @selector(cookies), @selector(da_cookies)); } - (NSArray<NSHTTPCookie *> *)da_cookies { NSArray *cookies = [self da_cookies]; BOOL isExist = NO; for (NSHTTPCookie *cookie in cookies) { if ([cookie.name isEqualToString:DAServerSessionCookieName]) { isExist = YES; break; } } if (! IsExist) {//CookieStroage add NSDictionary *dict = [[NSUserDefaults standardUserDefaults] dictionaryForKey:DAUserDefaultsCookieStorageKey]; if (dict) { NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties:dict]; [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie]; NSMutableArray *mCookies = cookies.mutableCopy; [mCookies addObject:cookie]; cookies = mCookies.copy; } } return cookies; } @endCopy the code

This ensures that cookies exist in sharedHTTPCookieStorage.

Bad problems

When WKWebView loads pages that take up too much memory, a blank screen appears.

  • Solution:
/ *! @abstract Invoked when the web view's web content process is terminated. @param webView The web view whose underlying web content process was terminated. */ - (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView { [webView reload]; // Refresh it}Copy the code

Custom UserAgent

WKWebView provides an Api, as described earlier, that calls the customUserAgent property.

Self. WebView. CustomUserAgent = @ "WebViewDemo / 1.0.0"; // Custom UA, WKWebView onlyCopy the code

Native shares login status with H5

Access_token writes to cookies. Cookie management has been described above

Native preview image in H5 page

  1. Scheme 1: Use WKUserScript and scriptMessageHandler
  2. Scheme 2: Scheme interaction

Native loads and caches the image in H5 page

Intercepting requests, at its core, is implemented in the NSURLProtocol subclass

+ (BOOL) canInitWithRequest: (NSURLRequest *) request {/ / treatment had no longer the if ([NSURLProtocol propertyForKey: DAURLProtocolHandledKey  inRequest:request]) { return NO; /* {" accept "= "image/ PNG,image/ SVG + XML,image/*; Q = 0.8 * \ / *; Q = 0.5 \ "; "The user-agent" = "Mozilla / 5.0 (iPhone; CPU iPhone OS 10_3 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Mobile/14E269 WebViewDemo/1.0.0"; } */ NSDictionary *headers = request.allHTTPHeaderFields; NSString *accept = headers[@"Accept"]; if (accept.length >= @"image".length && [accept rangeOfString:@"image"].location ! = NSNotFound) { return YES; } return NO; }Copy the code