前言
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;
//添加監(jiān)聽方法
- (void)addMessageHandlerName:(NSString *)messageHandlerName callBack:(MTWKJSCallBack)callBack;
//添加js方法 內(nèi)部注冊監(jiān)聽 JS只需要調(diào)用方法即可
- (void)addScriptFuncName:(NSString *)name callBack:(MTWKJSCallBack)callBack;
//注入js
- (void)addScriptSource:(NSString *)scriptSource;
//添加所有監(jiān)聽
- (void)registScriptMessage;
//移除所有監(jiān)聽
- (void)removeScriptMessage;
@end
@protocol MTWKWebViewDelegate <NSObject>
@optional
//網(wǎng)頁內(nèi)容開始加載到web view的時候調(diào)用
- (BOOL)wkWebView:(MTWKWebView *)wkWebView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(WKNavigationType)navigationType;
//根據(jù)導航的返回信息來判斷是否加載網(wǎng)頁
- (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
//添加監(jiān)聽方法
- (void)addMessageHandlerName:(NSString *)messageHandlerName callBack:(MTWKJSCallBack)callBack {
[self _addMessageHandlerName:messageHandlerName callBack:callBack];
}
//添加js方法 內(nèi)部注冊監(jiān)聽 JS只需要調(diào)用方法即可
- (void)addScriptFuncName:(NSString *)name callBack:(MTWKJSCallBack)callBack {
[self _addScriptFuncName:name callBack:callBack];
}
//注入js
- (void)addScriptSource:(NSString *)scriptSource{
[self _addScriptSource:scriptSource];
}
//添加所有監(jiān)聽
- (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];
}];
}
//移除所有監(jiān)聽
- (void)removeScriptMessage {
[self.jsHook enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, MTWKJSCallBack _Nonnull obj, BOOL * _Nonnull stop) {
[self.configuration.userContentController removeScriptMessageHandlerForName:key];
}];
}
#pragma mark - Private
//添加監(jiān)聽方法
- (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方法 內(nèi)部注冊監(jiān)聽 JS只需要調(diào)用方法即可
- (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];
//注冊監(jiān)聽
[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 {
}
//網(wǎng)頁由于某些原因加載失敗
- (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];
}
}
//網(wǎng)頁開始接收網(wǎng)頁內(nèi)容
- (void)webView:(WKWebView *)webView didCommitNavigation:(null_unspecified WKNavigation *)navigation {
}
//網(wǎng)頁導航加載完畢
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation {
if ([self.wkDelegate respondsToSelector:@selector(wkWebViewDidFinishLoad:)]) {
[self.wkDelegate wkWebViewDidFinishLoad:self];
}
}
//網(wǎng)頁導航加載失敗
- (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];
}
}
//網(wǎng)頁加載內(nèi)容進程終止
- (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直接調(diào)用方法,比如
onclick="getImg(0)"
或者
window.getImg(0)
2.ZSSRichTextEditor
雖然ZSSRichTextEditor
已經(jīng)適配了WKWebView,仍然有不少問題踩寇。而且項目加了一些業(yè)務代碼退腥,所以不能簡單的移植激况,記錄一下修改的地方
去掉鍵盤自帶的工具條
/**
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
//先創(chuàng)建一個<span></span>標簽
//延遲0.3s等待動態(tài)增加的標簽<span>加入到DOM中,再向其中新增圖片
//為什么不直接創(chuàng)建<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');//創(chuàng)建一個標簽
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圖片時創(chuàng)建的<span></span>標簽
}
//刪除插入url圖片時創(chuàng)建的<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是在等待動態(tài)增加的標簽<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;
}
輸入回調(diào)
//注冊監(jiān)聽
[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];
創(chuàng)建WKWebView
//創(chuàng)建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];
//注冊監(jiān)聽
__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];
僅做參考
參考資料: