前言
Apple will no longer accept submissions of new apps that use UIWebView as of April 30, 2020 and app updates that use UIWebView as of December 2020. Instead, use WKWebView for improved security and reliability.
1.首先稍微封装了一下WKWebView
MTWKWebView.h
#import <WebKit/WebKit.h>
typedef void(^MTWKJSCallBack)(WKScriptMessage *message);
@protocol MTWKWebViewDelegate;
@interface MTWKWebView : WKWebView<WKNavigationDelegate,WKScriptMessageHandler>
@property (nonatomic, weak)id<MTWKWebViewDelegate>wkDelegate;
//运行JS
//- (id)evaluatingJavaScriptFromString:(NSString *)script;
//添加监听方法
- (void)addMessageHandlerName:(NSString *)messageHandlerName callBack:(MTWKJSCallBack)callBack;
//添加js方法 内部注册监听 JS只需要调用方法即可
- (void)addScriptFuncName:(NSString *)name callBack:(MTWKJSCallBack)callBack;
//注入js
- (void)addScriptSource:(NSString *)scriptSource;
//添加所有监听
- (void)registScriptMessage;
//移除所有监听
- (void)removeScriptMessage;
@end
@protocol MTWKWebViewDelegate <NSObject>
@optional
//网页内容开始加载到web view的时候调用
- (BOOL)wkWebView:(MTWKWebView *)wkWebView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(WKNavigationType)navigationType;
//根据导航的返回信息来判断是否加载网页
- (BOOL)wkWebView:(MTWKWebView *)wkWebView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse;
- (void)wkWebViewDidStartLoad:(MTWKWebView *)wkWebView;
- (void)wkWebViewDidFinishLoad:(MTWKWebView *)wkWebView;
- (void)wkWebView:(MTWKWebView *)wkWebView didFailLoadWithError:(NSError *)error ;
@end
MTWKWebView.m
#import "MTWKWebView.h"
@interface MTWKWebView ()
@property (nonatomic, strong) NSMutableDictionary <NSString *,MTWKJSCallBack> *jsHook;
@end
@implementation MTWKWebView
- (void)dealloc
{
NSLog(@"MTWKWebView dealloc");
}
#pragma mark - Public
//添加监听方法
- (void)addMessageHandlerName:(NSString *)messageHandlerName callBack:(MTWKJSCallBack)callBack {
[self _addMessageHandlerName:messageHandlerName callBack:callBack];
}
//添加js方法 内部注册监听 JS只需要调用方法即可
- (void)addScriptFuncName:(NSString *)name callBack:(MTWKJSCallBack)callBack {
[self _addScriptFuncName:name callBack:callBack];
}
//注入js
- (void)addScriptSource:(NSString *)scriptSource{
[self _addScriptSource:scriptSource];
}
//添加所有监听
- (void)registScriptMessage {
[self removeScriptMessage];
__weak typeof(self)weakSelf = self;
[self.jsHook enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, MTWKJSCallBack _Nonnull obj, BOOL * _Nonnull stop) {
[weakSelf.configuration.userContentController addScriptMessageHandler:self name:key];
}];
}
//移除所有监听
- (void)removeScriptMessage {
[self.jsHook enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, MTWKJSCallBack _Nonnull obj, BOOL * _Nonnull stop) {
[self.configuration.userContentController removeScriptMessageHandlerForName:key];
}];
}
#pragma mark - Private
//添加监听方法
- (void)_addMessageHandlerName:(NSString *)messageHandlerName callBack:(MTWKJSCallBack)callBack {
[self.configuration.userContentController addScriptMessageHandler:self name:messageHandlerName];
[self.jsHook setObject:callBack forKey:messageHandlerName];
}
//注入js
- (void)_addScriptSource:(NSString *)scriptSource{
WKUserScript *script = [[WKUserScript alloc] initWithSource:scriptSource injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:true];
[self.configuration.userContentController addUserScript:script];
}
//添加js方法 内部注册监听 JS只需要调用方法即可
- (void)_addScriptFuncName:(NSString *)name callBack:(MTWKJSCallBack)callBack{
NSString *messageHandlerName = [NSString stringWithFormat:@"MTScritpFunc_%@",name];
NSString *userScriptSource = [NSString stringWithFormat:@"function %@(s) {window.webkit.messageHandlers.%@.postMessage(s);}",name,messageHandlerName];
//注入JS
[self _addScriptSource:userScriptSource];
//注册监听
[self _addMessageHandlerName:messageHandlerName callBack:callBack];
}
#pragma mark - WKNavigationDelegate
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
if ([self.wkDelegate respondsToSelector:@selector(wkWebView:shouldStartLoadWithRequest:navigationType:)]) {
BOOL shouldStart = [self.wkDelegate wkWebView:self shouldStartLoadWithRequest:navigationAction.request navigationType:navigationAction.navigationType];
if (shouldStart == false) {
decisionHandler(WKNavigationActionPolicyCancel);
return;
}
}
decisionHandler(WKNavigationActionPolicyAllow);
}
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler {
if ([self.wkDelegate respondsToSelector:@selector(wkWebView:decidePolicyForNavigationResponse:)]){
BOOL shouldLoad = [self.wkDelegate wkWebView:self decidePolicyForNavigationResponse:navigationResponse];
if (shouldLoad == false) {
decisionHandler(WKNavigationResponsePolicyCancel);
return;
}
}
decisionHandler(WKNavigationResponsePolicyAllow);
}
//开始加载
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation {
if ([self.wkDelegate respondsToSelector:@selector(wkWebViewDidStartLoad:)]) {
[self.wkDelegate wkWebViewDidStartLoad:self];
}
}
//跳转到其他的服务器
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(null_unspecified WKNavigation *)navigation {
}
//网页由于某些原因加载失败
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error {
if ([self.wkDelegate respondsToSelector:@selector(wkWebView:didFailLoadWithError:)]) {
[self.wkDelegate wkWebView:self didFailLoadWithError:error];
}
}
//网页开始接收网页内容
- (void)webView:(WKWebView *)webView didCommitNavigation:(null_unspecified WKNavigation *)navigation {
}
//网页导航加载完毕
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation {
if ([self.wkDelegate respondsToSelector:@selector(wkWebViewDidFinishLoad:)]) {
[self.wkDelegate wkWebViewDidFinishLoad:self];
}
}
//网页导航加载失败
- (void)webView:(WKWebView *)webView didFailNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error {
if ([self.wkDelegate respondsToSelector:@selector(wkWebView:didFailLoadWithError:)]) {
[self.wkDelegate wkWebView:self didFailLoadWithError:error];
}
}
//网页加载内容进程终止
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView API_AVAILABLE(macosx(10.11), ios(9.0)) {
}
#pragma mark - WKScriptMessageHandler
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
if ([message.name isKindOfClass:[NSString class]] == false) {
return ;
}
if ([self.jsHook objectForKey:message.name]) {
MTWKJSCallBack obj = [self.jsHook objectForKey:message.name];
if (obj) {
obj(message);
}
}
}
#pragma mark - Getter
- (NSMutableDictionary<NSString *,MTWKJSCallBack> *)jsHook {
if (!_jsHook) {
_jsHook = [NSMutableDictionary dictionary];
}
return _jsHook;
}
@end
addScriptFuncName: callBack:
主要是针对JS直接调用方法,比如
onclick="getImg(0)"
或者
window.getImg(0)
2.ZSSRichTextEditor
虽然ZSSRichTextEditor
已经适配了WKWebView,仍然有不少问题。而且项目加了一些业务代码,所以不能简单的移植,记录一下修改的地方
去掉键盘自带的工具条
/**
WKWebView modifications for hiding the inputAccessoryView
**/
@interface WKWebView (HackishAccessoryHiding)
@property (nonatomic, assign) BOOL hidesInputAccessoryView;
@end
@implementation WKWebView (HackishAccessoryHiding)
static const char * const hackishFixClassName = "WKWebBrowserViewMinusAccessoryView";
static Class hackishFixClass = Nil;
- (UIView *)hackishlyFoundBrowserView {
UIScrollView *scrollView = self.scrollView;
UIView *browserView = nil;
for (UIView *subview in scrollView.subviews) {
if ([NSStringFromClass([subview class]) hasPrefix:@"WKWebBrowserView"]) {
browserView = subview;
break;
}
}
return browserView;
}
- (id)methodReturningNil {
return nil;
}
- (void)ensureHackishSubclassExistsOfBrowserViewClass:(Class)browserViewClass {
if (!hackishFixClass) {
Class newClass = objc_allocateClassPair(browserViewClass, hackishFixClassName, 0);
newClass = objc_allocateClassPair(browserViewClass, hackishFixClassName, 0);
IMP nilImp = [self methodForSelector:@selector(methodReturningNil)];
class_addMethod(newClass, @selector(inputAccessoryView), nilImp, "@@:");
objc_registerClassPair(newClass);
hackishFixClass = newClass;
}
}
- (BOOL) hidesInputAccessoryView {
UIView *browserView = [self hackishlyFoundBrowserView];
return [browserView class] == hackishFixClass;
}
- (void) setHidesInputAccessoryView:(BOOL)value {
UIView *browserView = [self hackishlyFoundBrowserView];
if (browserView == nil) {
return;
}
[self ensureHackishSubclassExistsOfBrowserViewClass:[browserView class]];
if (value) {
object_setClass(browserView, hackishFixClass);
}
else {
Class normalClass = objc_getClass("WKWebBrowserView");
object_setClass(browserView, normalClass);
}
[browserView reloadInputViews];
}
@end
使用
wkWebView.hidesInputAccessoryView = YES;
webview显示自动弹出键盘功能
#pragma mark - Convenience replacement for keyboardDisplayRequiresUserAction in WKWebview
+ (void)allowDisplayingKeyboardWithoutUserAction {
Class class = NSClassFromString(@"WKContentView");
NSOperatingSystemVersion iOS_11_3_0 = (NSOperatingSystemVersion){11, 3, 0};
NSOperatingSystemVersion iOS_12_2_0 = (NSOperatingSystemVersion){12, 2, 0};
NSOperatingSystemVersion iOS_13_0_0 = (NSOperatingSystemVersion){13, 0, 0};
if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion: iOS_13_0_0]) {
SEL selector = sel_getUid("_elementDidFocus:userIsInteracting:blurPreviousNode:activityStateChanges:userObject:");
Method method = class_getInstanceMethod(class, selector);
IMP original = method_getImplementation(method);
IMP override = imp_implementationWithBlock(^void(id me, void* arg0, BOOL arg1, BOOL arg2, BOOL arg3, id arg4) {
((void (*)(id, SEL, void*, BOOL, BOOL, BOOL, id))original)(me, selector, arg0, TRUE, arg2, arg3, arg4);
});
method_setImplementation(method, override);
}
else if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion: iOS_12_2_0]) {
SEL selector = sel_getUid("_elementDidFocus:userIsInteracting:blurPreviousNode:changingActivityState:userObject:");
Method method = class_getInstanceMethod(class, selector);
IMP original = method_getImplementation(method);
IMP override = imp_implementationWithBlock(^void(id me, void* arg0, BOOL arg1, BOOL arg2, BOOL arg3, id arg4) {
((void (*)(id, SEL, void*, BOOL, BOOL, BOOL, id))original)(me, selector, arg0, TRUE, arg2, arg3, arg4);
});
method_setImplementation(method, override);
}
else if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion: iOS_11_3_0]) {
SEL selector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:changingActivityState:userObject:");
Method method = class_getInstanceMethod(class, selector);
IMP original = method_getImplementation(method);
IMP override = imp_implementationWithBlock(^void(id me, void* arg0, BOOL arg1, BOOL arg2, BOOL arg3, id arg4) {
((void (*)(id, SEL, void*, BOOL, BOOL, BOOL, id))original)(me, selector, arg0, TRUE, arg2, arg3, arg4);
});
method_setImplementation(method, override);
} else {
SEL selector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:userObject:");
Method method = class_getInstanceMethod(class, selector);
IMP original = method_getImplementation(method);
IMP override = imp_implementationWithBlock(^void(id me, void* arg0, BOOL arg1, BOOL arg2, id arg3) {
((void (*)(id, SEL, void*, BOOL, BOOL, id))original)(me, selector, arg0, TRUE, arg2, arg3);
});
method_setImplementation(method, override);
}
}
//TODO: Is this behavior correct? Is it the right replacement?
// self.editorView.keyboardDisplayRequiresUserAction = NO;
[ZSSRichTextEditor allowDisplayingKeyboardWithoutUserAction];
添加图片空白
这里参考的RichTextEditor的做法
其实我也不太明白原因
修改js
//先创建一个<span></span>标签
//延迟0.3s等待动态增加的标签<span>加入到DOM中,再向其中新增图片
//为什么不直接创建<img> 标签并指定src呢? 因为图片显示不出来,不知道什么原因
zss_editor.priInsertImage = function(){
zss_editor.restorerange();
var html = '<span id="imageSpan"></span>';
zss_editor.insertHTML(html);
zss_editor.enabledEditingItems();
}
//插入url图片
zss_editor.insertImage = function(url, alt) {
var img = document.createElement('img');//创建一个标签
img.setAttribute('src',url);//给标签定义src链接
img.setAttribute('style','width:100%;');//给标签定义宽度
img.setAttribute('alt',alt);//给标签定义alt
document.getElementById('imageSpan').appendChild(img);//放到指定的id里
zss_editor.deletInsertImageSpan();//删除插入url图片时创建的<span></span>标签
}
//删除插入url图片时创建的<span></span>标签
zss_editor.deletInsertImageSpan = function(){
var html = $('#imageSpan').html();
$('#imageSpan').before(html);
$('#imageSpan').remove();
}
修改insertImage: alt:
方法
- (void)insertImage:(NSString *)url alt:(NSString *)alt {
if (alt == nil) {
alt = @"";
}
// NSString *trigger = [NSString stringWithFormat:@"zss_editor.insertImage(\"%@\", \"%@\");", url, alt];
// [self.editorView evaluateJavaScript:trigger completionHandler:nil];
//增加<span>标签
[self.editorView evaluateJavaScript:@"zss_editor.priInsertImage();" completionHandler:nil];
//延迟1s是在等待动态增加的标签<span>加入到DOM中,再向其中新增图片
//延迟1秒太久了 改成0.3
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSString *trigger = [NSString stringWithFormat:@"zss_editor.insertImage(\"%@\", \"%@\");", url, alt];
[self.editorView evaluateJavaScript:trigger completionHandler:nil];
});
}
图片标签宽度属性需要设置
插入链接
修改showInsertLinkDialogWithLink: title:
[self focusTextEditor];
// Save the selection location
[self.editorView evaluateJavaScript:@"zss_editor.prepareInsert();" completionHandler:nil];
if (!self.selectedLinkURL) {
[self insertLink:linkURL.text title:title.text];
} else {
[self updateLink:linkURL.text title:title.text];
}
键盘弹出收回
CGFloat bottomSafeAreaInset = 0.0;
if (self->_alwaysShowToolbar) {
if (@available(iOS 11.0, *)) {
bottomSafeAreaInset = self.view.safeAreaInsets.bottom;
}
frame.origin.y = self.view.frame.size.height - sizeOfToolbar - bottomSafeAreaInset;
} else {
frame.origin.y = self.view.frame.size.height + keyboardHeight;
}
self.toolbarHolder.frame = frame;
// Editor View
CGRect editorFrame = self.editorView.frame;
if (self->_alwaysShowToolbar) {
editorFrame.size.height = ((self.view.frame.size.height - sizeOfToolbar - bottomSafeAreaInset - editorFrame.origin.y) - extraHeight);
} else {
editorFrame.size.height = self.view.frame.size.height;
}
输入回调
//注册监听
[wkWebView addMessageHandlerName:@"contentPasteCallback" callBack:^(WKScriptMessage *message) {
weakSelf.editorPaste = YES;
}];
[wkWebView addMessageHandlerName:@"contentInputCallback" callBack:^(WKScriptMessage *message) {
if (_receiveEditorDidChangeEvents) {
[self updateEditor];
}
[self getText:^(NSString *text) {
[self checkForMentionOrHashtagInText:text];
}];
if (self.editorPaste) {
[self blurTextEditor];
self.editorPaste = NO;
}
}];
//- (void)wkWebViewDidFinishLoad:(MTWKWebView *)wkWebView中添加
[self.editorView evaluateJavaScript:@"document.getElementById('zss_editor_content').addEventListener('paste', function(){window.webkit.messageHandlers.contentPasteCallback.postMessage(null);}, false);" completionHandler:nil];
[self.editorView evaluateJavaScript:@"document.getElementById('zss_editor_content').addEventListener('input', function(){window.webkit.messageHandlers.contentInputCallback.postMessage(null);}, false);" completionHandler:nil];
创建WKWebView
//创建web编辑器
WKUserContentController *userContentController = [[WKUserContentController alloc] init];
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
configuration.userContentController = userContentController;
//set data detection to none so it doesnt conflict
configuration.dataDetectorTypes = WKDataDetectorTypeNone;
MTWKWebView *wkWebView = [[MTWKWebView alloc] initWithFrame:frame configuration:configuration];
wkWebView.UIDelegate = self;
wkWebView.wkDelegate = self;
wkWebView.navigationDelegate = wkWebView;
wkWebView.hidesInputAccessoryView = YES;
wkWebView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin;
wkWebView.scrollView.delegate = self;
wkWebView.scrollView.bounces = NO;
self.editorView = wkWebView;
[self.view addSubview:wkWebView];
//注册监听
__weak typeof(self)weakSelf = self;
NSString *scriptString = @"var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', 'width=device-width'); document.getElementsByTagName('head')[0].appendChild(meta);";
[wkWebView addScriptSource:scriptString];
[wkWebView addMessageHandlerName:@"contentPasteCallback" callBack:^(WKScriptMessage *message) {
weakSelf.editorPaste = YES;
}];
[wkWebView addMessageHandlerName:@"contentInputCallback" callBack:^(WKScriptMessage *message) {
if (_receiveEditorDidChangeEvents) {
[self updateEditor];
}
[self getText:^(NSString *text) {
[self checkForMentionOrHashtagInText:text];
}];
if (self.editorPaste) {
[self blurTextEditor];
self.editorPaste = NO;
}
}];
//TODO: Is this behavior correct? Is it the right replacement?
// self.editorView.keyboardDisplayRequiresUserAction = NO;
[ZSSRichTextEditor allowDisplayingKeyboardWithoutUserAction];
仅做参考
参考资料: