WKWebView is the browser component used to replace UIWebView after iOS 8.0. Compared with UIWebView, WKWebView has higher performance, supports more HTML5 features and has more detailed control. This article briefly introduces the use of UIWebView and the synchronous interaction between JS and native APP.

WKWebView

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@interface WKWebView : UIView

//重要属性
@property (nonatomic, readonly, copy) WKWebViewConfiguration *configuration;
@property (nullable, nonatomic, weak) id <WKNavigationDelegate> navigationDelegate;
@property (nullable, nonatomic, weak) id <WKUIDelegate> UIDelegate;
@property (nonatomic, readonly, strong) WKBackForwardList *backForwardList;

@property (nonatomic, readonly, nullable) SecTrustRef serverTrust;

@property (nullable, nonatomic, copy) NSString *customUserAgent;
@property (nonatomic) BOOL allowsLinkPreview;
@property (nonatomic, readonly, strong) UIScrollView *scrollView;
...

//加载方法
- (nullable WKNavigation *)loadRequest:(NSURLRequest *)request;
- (nullable WKNavigation *)loadFileURL:(NSURL *)URL allowingReadAccessToURL:(NSURL *)readAccessURL;
...

- (nullable WKNavigation *)goBack;
- (nullable WKNavigation *)goForward;

- (nullable WKNavigation *)reload;
- (nullable WKNavigation *)reloadFromOrigin;

//类方法
+ (BOOL)handlesURLScheme:(NSString *)urlScheme;

//与JS交互接口
- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))completionHandler;
//下面两个是iOS 14新引入API
- (void)evaluateJavaScript:(NSString *)javaScriptString inFrame:(nullable WKFrameInfo *)frame inContentWorld:(WKContentWorld *)contentWorld completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))completionHandler;
- (void)callAsyncJavaScript:(NSString *)functionBody arguments:(nullable NSDictionary<NSString *, id> *)arguments inFrame:(nullable WKFrameInfo *)frame inContentWorld:(WKContentWorld *)contentWorld completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))completionHandler;

@end

WKBackForwardList

History of visited web pages.

WKNavigation

WKNavigation object can be used to understand the loading progress of a web page. A WKNavigation object will be returned when the page is loaded by methods such as loadRequest, goBack, etc. The following methods of WKNavigationDelegate proxy can be used to know the loading status of the page.

1
2
3
4
5
6
7
//开始加载
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation;
//加载完成
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation;
//加载失败
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error
- (void)webView:(WKWebView *)webView didFailNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error;

WKNavigationDelegate

WKNavigationDelegate has some important interfaces in addition to the above methods.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
//在尝试加载内容之前调用,确定是否加载请求
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;

//在请求响应后调用,决定是否加载内容,在这里可以针对特定HTTP状态码的处理
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler {

    if ([navigationResponse.response isKindOfClass:[NSHTTPURLResponse class]]) {
        NSHTTPURLResponse *response = (NSHTTPURLResponse *)navigationResponse.response;
        if (response.statusCode != 200) {
           //非200状态码不加载
            decisionHandler(WKNavigationResponsePolicyCancel);
            return;
        }
    }
    decisionHandler(WKNavigationResponsePolicyAllow);
}

//参考:Authentication Challenge的内容:/blog/blog/137-ssl-pinning.html
- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler;

WKNavigationAction

Contains web navigation information, according to which the corresponding action screen needs to be displayed.

WKFrameInfo

Object that identifies information about the current page content.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@interface WKFrameInfo : NSObject <NSCopying>

/*! @abstract A Boolean value indicating whether the frame is the main frame
 or a subframe.
 */
@property (nonatomic, readonly, getter=isMainFrame) BOOL mainFrame;

/*! @abstract The frame's current request.
 */
@property (nonatomic, readonly, copy) NSURLRequest *request;

/*! @abstract The frame's current security origin.
 */
@property (nonatomic, readonly) WKSecurityOrigin *securityOrigin API_AVAILABLE(macos(10.11), ios(9.0));

/*! @abstract The web view of the webpage that contains this frame.
 */
@property (nonatomic, readonly, weak) WKWebView *webView API_AVAILABLE(macos(10.13), ios(11.0));

@end

WKWebViewConfiguration

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
@interface WKWebViewConfiguration : NSObject <NSSecureCoding, NSCopying>

@property (nonatomic, strong) WKProcessPool *processPool;

/*! @abstract The preference settings to be used by the web view.
*/
@property (nonatomic, strong) WKPreferences *preferences;

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

/*! @abstract The website data store to be used by the web view.
 */
@property (nonatomic, strong) WKWebsiteDataStore *websiteDataStore API_AVAILABLE(macos(10.11), ios(9.0));

/*! @abstract The name of the application as used in the user agent string.
*/
@property (nullable, nonatomic, copy) NSString *applicationNameForUserAgent API_AVAILABLE(macos(10.11), ios(9.0));
...

@end

WKWebViewConfiguration indicates the configuration information for initializing WKWebVie.

WKProcessPool

1
2
@interface WKProcessPool : NSObject <NSSecureCoding>
@end

WKProcessPool represents a separate process used to manage web content, and for security and stability reasons, WKWebView allocates a separate process for each WKWebView instance (instead of using the APP process space directly). WKWebViews with the same WKProcessPool object share the same process space. This is also a big difference between WKWebView and UIWebView.

As you can see the WKProcessPool class does not leak any interface, which means we can only create and read the object and determine if it is in the same process by the object address.

WKUserContentController

WKUserScript represents a JavaScript script that needs to be injected into the web page.

WKPreferences

Preference settings.

WKUIDelegate

Handles the agent that interacts with the user. There are three methods that need to be highlighted.

1
2
3
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler;
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler;
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable result))completionHandler;

The above three methods will be called when the web page executes the alert, confirm, and prompt methods of JavaScript, respectively.

WKScriptMessageHandler and WKScriptMessageHandlerWithReply

1
2
3
4
5
6
7
8
9
@protocol WKScriptMessageHandler <NSObject>
@required
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;
@end

//iOS 14
@protocol WKScriptMessageHandlerWithReply <NSObject>
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message replyHandler:(void (^)(id _Nullable reply, NSString *_Nullable errorMessage))replyHandler;
@end

WKScriptMessageHandler and WKScriptMessageHandlerWithReply are proxy protocols exposed by WKUserContentController and contain a must-implement method for responding to messages sent by the web’s JavaScript code.

WKScriptMessageHandler and WKScriptMessageHandlerWithReply are proxy protocols exposed by WKUserContentController and contain a must-implement method for responding to messages sent by the web’s JavaScript code.

WKContentWorld

WKContentWorld is a new addition to iOS 14 and can be interpreted as a different namespace with a different runtime environment. Obviously, logically, there is a possibility of name conflict between native APP JS environment and web JS runtime environment. WKContentWorld has two class attributes defaultClientWorld , pageWorld , which represent the JS runtime space of native APP and web container respectively. Developers can also create a separate JS runtime environment by using: + (WKContentWorld *)worldWithName:(NSString *)name factory method.

Basic use of WKWebView

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
WKUserContentController *userContentController = [[WKUserContentController alloc] init];
//注册处理器的名称
[userContentController addScriptMessageHandler:self name:@"easeapiHandler"];

WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
configuration.userContentController = userContentController;

self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:configuration];
self.webView.UIDelegate = self;
[self.view addSubview:self.webView];

NSURLRequest *request = [[NSURLRequest alloc] initWithURL:[NSURL URLWithString:@"https://easeapi.com/blog"]];
[self.webView loadRequest:request];

native APP and JS interaction

JS passing data to native APP

After registering the unique name through addScriptMessageHandler, data can be sent in the js code by

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
//js侧发送消息
let params = { "success": false }      
window.webkit.messageHandlers.easeapiHandler.postMessage(params)

//native APP接收消息
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
    if ([message.name isEqualToString:@"easeapiHandler"]) {
        NSDictionary *body = message.body;
    }
}

native APP executes js code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
//在js侧定义方法
jsFunc = function(msg) {
 console.log(msg)
 return "ok"
};

//native APP执行js方法并获得返回结果
[self.webView evaluateJavaScript:@"jsFunc('hello world!')" completionHandler:^(id _Nullable result, NSError * _Nullable error) {
 NSLog(@"result = %@", result);
}];

WKWebView and JS interaction synchronization problem

As you can see, the interaction using window.webkit.messageHandlers.[name].postMessage sometimes doesn’t work well, and it doesn’t work well when you need to synchronize the JS side to get the data from the native APP before you can continue executing the JS code. Because postMessag has no callback interface, it cannot bring back the execution result of native APP. Unlike this, the evaluateJavaScript interface of native APP executing js code has a completionHandler callback to get the execution result of js on the native APP side.

Then the question arises: when executing postMessage on JS side, what if we get the execution result of native APP?

I remember this problem did not exist in UIWebView era, but it is a problem to be considered in WKWebView. At present, I have not found an elegant solution for this problem, so I have two options for reference.

Option 1: With the help of runJavaScriptTextInputPanelWithPrompt method

The above mentioned runJavaScriptTextInputPanelWithPrompt method when introducing WKUIDelegate, this method is meant to give native APP a time to realize prompt pop-up window by itself when executing prompt method, notice that this method has a completionHandler, that is, native APP will return data to JS side after processing.

The js prompt() method is used to display a dialog box for the user to make input. The definition is as follows.

1
2
3
4
let msg = prompt(text, defaultText)
//text:标题文案
//defaultText:输入框默认文案
//返回用户输入的文案

When the prompt method is executed in the WKWebView environment, it calls.

1
2
3
4
5
6
7
8
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable result))completionHandler {
 //针对特定prompt单独处理
 if ([prompt isEqualToString:@"cmd"]) {
  //将处理结果返回给JS。
  completionHandler("result");
 }
 ...
}

Since the input and return are strings, it can be extended by JSON wrapping so that the response from the native APP can be synchronized by calling a specific name prompt in the js layer.

Option 2: Use the new API added by iOS 14

Probably Apple also found this problem, so in iOS 14, there are many new optimized APIs for WKWebView, including the optimization for addScriptMessageHandler. A new didReceiveScriptMessage API with replyHandler has been added.

1
2
3
4
5
6
[self.webView.configuration.userContentController addScriptMessageHandlerWithReply:self contentWorld:WKContentWorld.pageWorld name:@"easeapiHandler"];

//WKScriptMessageHandlerWithReply
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message replyHandler:(void (^)(id _Nullable reply, NSString *_Nullable errorMessage))replyHandler {
    replyHandler(@"success", nil);
}

Use the promise asynchronous callback on the JS side to get the result.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
authSuccess = function() {
  let params = { "result": true }      
  let promise = window.webkit.messageHandlers.easeapiHandler.postMessage(params)
  promise.then(
   function(result) {
    prompt('result', result)
   },
   function(err) {
           console.log(err)
         }
  )
 };

WKWebView addScriptMessageHandler circular reference

addScriptMessageHandler/addScriptMessageHandlerWithReply will strongly hold the object, you need to removeScriptMessageHandlerForName operation at the right time, otherwise it will cause circular reference.