DWKWebView.m 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. #import "DWKWebView.h"
  2. #import "JSBUtil.h"
  3. #import "DSCallInfo.h"
  4. #import "InternalApis.h"
  5. #import <objc/message.h>
  6. @protocol ScriptDelegate <NSObject>
  7. -(NSString *)call:(NSString*) method :(NSString*) argStr;
  8. @end
  9. @interface InternalScriptsHandler : NSObject<WKScriptMessageHandler>
  10. @property (nonatomic, weak) id<ScriptDelegate> handler;
  11. @end
  12. @implementation InternalScriptsHandler
  13. - (instancetype)initWithHandler:(id<ScriptDelegate>)handler {
  14. self = [super init];
  15. if (self) {
  16. _handler = handler;
  17. }
  18. return self;
  19. }
  20. - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
  21. {
  22. NSDictionary *dict = message.body;
  23. if ([dict isKindOfClass:[NSDictionary class]]) {
  24. NSString *method = dict[@"method"];
  25. NSString *arg = dict[@"arg"];
  26. if (self.handler && method && arg) {
  27. [self.handler call:method :arg];
  28. }
  29. }
  30. }
  31. @end
  32. @implementation DWKWebView
  33. {
  34. void (^alertHandler)(void);
  35. void (^confirmHandler)(BOOL);
  36. void (^promptHandler)(NSString *);
  37. void(^javascriptCloseWindowListener)(void);
  38. int dialogType;
  39. int callId;
  40. bool jsDialogBlock;
  41. NSMutableDictionary<NSString *,id> *javaScriptNamespaceInterfaces;
  42. NSMutableDictionary *handerMap;
  43. NSMutableArray<DSCallInfo *> * callInfoList;
  44. NSDictionary<NSString*,NSString*> *dialogTextDic;
  45. UITextField *txtName;
  46. UInt64 lastCallTime ;
  47. NSString *jsCache;
  48. bool isPending;
  49. bool isDebug;
  50. }
  51. -(instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration
  52. {
  53. txtName=nil;
  54. dialogType=0;
  55. callId=0;
  56. alertHandler=nil;
  57. confirmHandler=nil;
  58. promptHandler=nil;
  59. jsDialogBlock=true;
  60. callInfoList=[NSMutableArray array];
  61. javaScriptNamespaceInterfaces=[NSMutableDictionary dictionary];
  62. handerMap=[NSMutableDictionary dictionary];
  63. lastCallTime = 0;
  64. jsCache=@"";
  65. isPending=false;
  66. isDebug=false;
  67. dialogTextDic=@{};
  68. InternalScriptsHandler *internal = [[InternalScriptsHandler alloc] initWithHandler:(id<ScriptDelegate>)self];
  69. WKUserScript *script = [[WKUserScript alloc] initWithSource:@"window._dswk=true;"
  70. injectionTime:WKUserScriptInjectionTimeAtDocumentStart
  71. forMainFrameOnly:YES];
  72. [configuration.userContentController addUserScript:script];
  73. [configuration.userContentController addScriptMessageHandler:internal name:@"asyncBridge"];
  74. self = [super initWithFrame:frame configuration: configuration];
  75. if (self) {
  76. super.UIDelegate=self;
  77. }
  78. // add internal Javascript Object
  79. InternalApis * interalApis= [[InternalApis alloc] init];
  80. interalApis.webview=self;
  81. [self addJavascriptObject:interalApis namespace:@"_dsb"];
  82. return self;
  83. }
  84. - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt
  85. defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame
  86. completionHandler:(void (^)(NSString * _Nullable result))completionHandler
  87. {
  88. NSString * prefix=@"_dsbridge=";
  89. if ([prompt hasPrefix:prefix])
  90. {
  91. NSString *method= [prompt substringFromIndex:[prefix length]];
  92. NSString *result=nil;
  93. if(isDebug){
  94. result =[self call:method :defaultText ];
  95. }else{
  96. @try {
  97. result =[self call:method :defaultText ];
  98. }@catch(NSException *exception){
  99. NSLog(@"%@", exception);
  100. }
  101. }
  102. completionHandler(result);
  103. }else {
  104. if(!jsDialogBlock){
  105. completionHandler(nil);
  106. }
  107. if(self.DSUIDelegate && [self.DSUIDelegate respondsToSelector:
  108. @selector(webView:runJavaScriptTextInputPanelWithPrompt
  109. :defaultText:initiatedByFrame
  110. :completionHandler:)])
  111. {
  112. return [self.DSUIDelegate webView:webView runJavaScriptTextInputPanelWithPrompt:prompt
  113. defaultText:defaultText
  114. initiatedByFrame:frame
  115. completionHandler:completionHandler];
  116. }else{
  117. dialogType=3;
  118. if(jsDialogBlock){
  119. promptHandler=completionHandler;
  120. }
  121. UIAlertView *alert = [[UIAlertView alloc]
  122. initWithTitle:prompt
  123. message:@""
  124. delegate:self
  125. cancelButtonTitle:dialogTextDic[@"promptCancelBtn"]?dialogTextDic[@"promptCancelBtn"]:@"取消"
  126. otherButtonTitles:dialogTextDic[@"promptOkBtn"]?dialogTextDic[@"promptOkBtn"]:@"确定",
  127. nil];
  128. [alert setAlertViewStyle:UIAlertViewStylePlainTextInput];
  129. txtName = [alert textFieldAtIndex:0];
  130. txtName.text=defaultText;
  131. [alert show];
  132. }
  133. }
  134. }
  135. - (WKNavigation *)loadData:(NSData *)data MIMEType:(NSString *)MIMEType characterEncodingName:(NSString *)characterEncodingName baseURL:(NSURL *)baseURL {
  136. [self resetCallInfoList];
  137. return [super loadData:data MIMEType:MIMEType characterEncodingName:characterEncodingName baseURL:baseURL];
  138. }
  139. - (WKNavigation *)loadHTMLString:(NSString *)string baseURL:(NSURL *)baseURL {
  140. [self resetCallInfoList];
  141. return [super loadHTMLString:string baseURL:baseURL];
  142. }
  143. - (WKNavigation *)loadRequest:(NSURLRequest *)request {
  144. [self resetCallInfoList];
  145. return [super loadRequest:request];
  146. }
  147. - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message
  148. initiatedByFrame:(WKFrameInfo *)frame
  149. completionHandler:(void (^)(void))completionHandler
  150. {
  151. if(!jsDialogBlock){
  152. completionHandler();
  153. }
  154. if( self.DSUIDelegate && [self.DSUIDelegate respondsToSelector:
  155. @selector(webView:runJavaScriptAlertPanelWithMessage
  156. :initiatedByFrame:completionHandler:)])
  157. {
  158. return [self.DSUIDelegate webView:webView runJavaScriptAlertPanelWithMessage:message
  159. initiatedByFrame:frame
  160. completionHandler:completionHandler];
  161. }else{
  162. dialogType=1;
  163. if(jsDialogBlock){
  164. alertHandler=completionHandler;
  165. }
  166. UIAlertView *alertView =
  167. [[UIAlertView alloc] initWithTitle:dialogTextDic[@"alertTitle"]?dialogTextDic[@"alertTitle"]:@"提示"
  168. message:message
  169. delegate:self
  170. cancelButtonTitle:dialogTextDic[@"alertBtn"]?dialogTextDic[@"alertBtn"]:@"确定"
  171. otherButtonTitles:nil,nil];
  172. [alertView show];
  173. }
  174. }
  175. -(void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message
  176. initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler
  177. {
  178. if(!jsDialogBlock){
  179. completionHandler(YES);
  180. }
  181. if( self.DSUIDelegate&& [self.DSUIDelegate respondsToSelector:
  182. @selector(webView:runJavaScriptConfirmPanelWithMessage:initiatedByFrame:completionHandler:)])
  183. {
  184. return[self.DSUIDelegate webView:webView runJavaScriptConfirmPanelWithMessage:message
  185. initiatedByFrame:frame
  186. completionHandler:completionHandler];
  187. }else{
  188. dialogType=2;
  189. if(jsDialogBlock){
  190. confirmHandler=completionHandler;
  191. }
  192. UIAlertView *alertView =
  193. [[UIAlertView alloc] initWithTitle:dialogTextDic[@"confirmTitle"]?dialogTextDic[@"confirmTitle"]:@"提示"
  194. message:message
  195. delegate:self
  196. cancelButtonTitle:dialogTextDic[@"confirmCancelBtn"]?dialogTextDic[@"confirmCancelBtn"]:@"取消"
  197. otherButtonTitles:dialogTextDic[@"confirmOkBtn"]?dialogTextDic[@"confirmOkBtn"]:@"确定", nil];
  198. [alertView show];
  199. }
  200. }
  201. - (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures{
  202. if( self.DSUIDelegate && [self.DSUIDelegate respondsToSelector:
  203. @selector(webView:createWebViewWithConfiguration:forNavigationAction:windowFeatures:)]){
  204. return [self.DSUIDelegate webView:webView createWebViewWithConfiguration:configuration forNavigationAction:navigationAction windowFeatures:windowFeatures];
  205. }
  206. return nil;
  207. }
  208. - (void)webViewDidClose:(WKWebView *)webView{
  209. if( self.DSUIDelegate && [self.DSUIDelegate respondsToSelector:
  210. @selector(webViewDidClose:)]){
  211. [self.DSUIDelegate webViewDidClose:webView];
  212. }
  213. }
  214. - (BOOL)webView:(WKWebView *)webView shouldPreviewElement:(WKPreviewElementInfo *)elementInfo{
  215. if( self.DSUIDelegate
  216. && [self.DSUIDelegate respondsToSelector:
  217. @selector(webView:shouldPreviewElement:)]){
  218. return [self.DSUIDelegate webView:webView shouldPreviewElement:elementInfo];
  219. }
  220. return NO;
  221. }
  222. - (UIViewController *)webView:(WKWebView *)webView previewingViewControllerForElement:(WKPreviewElementInfo *)elementInfo defaultActions:(NSArray<id<WKPreviewActionItem>> *)previewActions{
  223. if( self.DSUIDelegate &&
  224. [self.DSUIDelegate respondsToSelector:@selector(webView:previewingViewControllerForElement:defaultActions:)]){
  225. return [self.DSUIDelegate
  226. webView:webView
  227. previewingViewControllerForElement:elementInfo
  228. defaultActions:previewActions
  229. ];
  230. }
  231. return nil;
  232. }
  233. - (void)webView:(WKWebView *)webView commitPreviewingViewController:(UIViewController *)previewingViewController{
  234. if( self.DSUIDelegate
  235. && [self.DSUIDelegate respondsToSelector:@selector(webView:commitPreviewingViewController:)]){
  236. return [self.DSUIDelegate webView:webView commitPreviewingViewController:previewingViewController];
  237. }
  238. }
  239. - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
  240. {
  241. if(dialogType==1 && alertHandler){
  242. alertHandler();
  243. alertHandler=nil;
  244. }else if(dialogType==2 && confirmHandler){
  245. confirmHandler(buttonIndex==1?YES:NO);
  246. confirmHandler=nil;
  247. }else if(dialogType==3 && promptHandler && txtName) {
  248. if(buttonIndex==1){
  249. promptHandler([txtName text]);
  250. }else{
  251. promptHandler(@"");
  252. }
  253. promptHandler=nil;
  254. txtName=nil;
  255. }
  256. }
  257. - (void) evalJavascript:(int) delay{
  258. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{
  259. @synchronized(self){
  260. if([jsCache length]!=0){
  261. [self evaluateJavaScript :jsCache completionHandler:nil];
  262. isPending=false;
  263. jsCache=@"";
  264. lastCallTime=[[NSDate date] timeIntervalSince1970]*1000;
  265. }
  266. }
  267. });
  268. }
  269. -(NSString *)call:(NSString*) method :(NSString*) argStr
  270. {
  271. NSArray *nameStr=[JSBUtil parseNamespace:[method stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]];
  272. id JavascriptInterfaceObject=javaScriptNamespaceInterfaces[nameStr[0]];
  273. NSString *error=[NSString stringWithFormat:@"Error! \n Method %@ is not invoked, since there is not a implementation for it",method];
  274. NSMutableDictionary*result =[NSMutableDictionary dictionaryWithDictionary:@{@"code":@-1,@"data":@""}];
  275. if(!JavascriptInterfaceObject){
  276. NSLog(@"Js bridge called, but can't find a corresponded JavascriptObject , please check your code!");
  277. }else{
  278. method=nameStr[1];
  279. NSString *methodOne = [JSBUtil methodByNameArg:1 selName:method class:[JavascriptInterfaceObject class]];
  280. NSString *methodTwo = [JSBUtil methodByNameArg:2 selName:method class:[JavascriptInterfaceObject class]];
  281. SEL sel=NSSelectorFromString(methodOne);
  282. SEL selasyn=NSSelectorFromString(methodTwo);
  283. NSDictionary * args=[JSBUtil jsonStringToObject:argStr];
  284. id arg=args[@"data"];
  285. if(arg==[NSNull null]){
  286. arg=nil;
  287. }
  288. NSString * cb;
  289. do{
  290. if(args && (cb= args[@"_dscbstub"])){
  291. if([JavascriptInterfaceObject respondsToSelector:selasyn]){
  292. __weak typeof(self) weakSelf = self;
  293. void (^completionHandler)(id,BOOL) = ^(id value,BOOL complete){
  294. NSString *del=@"";
  295. result[@"code"]=@0;
  296. if(value!=nil){
  297. result[@"data"]=value;
  298. }
  299. value=[JSBUtil objToJsonString:result];
  300. value=[value stringByAddingPercentEscapesUsingEncoding: NSUTF8StringEncoding];
  301. if(complete){
  302. del=[@"delete window." stringByAppendingString:cb];
  303. }
  304. NSString*js=[NSString stringWithFormat:@"try {%@(JSON.parse(decodeURIComponent(\"%@\")).data);%@; } catch(e){};",cb,(value == nil) ? @"" : value,del];
  305. __strong typeof(self) strongSelf = weakSelf;
  306. @synchronized(self)
  307. {
  308. UInt64 t=[[NSDate date] timeIntervalSince1970]*1000;
  309. jsCache=[jsCache stringByAppendingString:js];
  310. if(t-lastCallTime<50){
  311. if(!isPending){
  312. [strongSelf evalJavascript:50];
  313. isPending=true;
  314. }
  315. }else{
  316. [strongSelf evalJavascript:0];
  317. }
  318. }
  319. };
  320. void(*action)(id,SEL,id,id) = (void(*)(id,SEL,id,id))objc_msgSend;
  321. action(JavascriptInterfaceObject,selasyn,arg,completionHandler);
  322. break;
  323. }
  324. }else if([JavascriptInterfaceObject respondsToSelector:sel]){
  325. id ret;
  326. id(*action)(id,SEL,id) = (id(*)(id,SEL,id))objc_msgSend;
  327. ret=action(JavascriptInterfaceObject,sel,arg);
  328. [result setValue:@0 forKey:@"code"];
  329. if(ret!=nil){
  330. [result setValue:ret forKey:@"data"];
  331. }
  332. break;
  333. }
  334. NSString*js=[error stringByAddingPercentEscapesUsingEncoding: NSUTF8StringEncoding];
  335. if(isDebug){
  336. js=[NSString stringWithFormat:@"window.alert(decodeURIComponent(\"%@\"));",js];
  337. [self evaluateJavaScript :js completionHandler:nil];
  338. }
  339. NSLog(@"%@",error);
  340. }while (0);
  341. }
  342. return [JSBUtil objToJsonString:result];
  343. }
  344. - (void)setJavascriptCloseWindowListener:(void (^)(void))callback
  345. {
  346. javascriptCloseWindowListener=callback;
  347. }
  348. - (void)setDebugMode:(bool)debug{
  349. isDebug=debug;
  350. }
  351. - (void)resetCallInfoList {
  352. callInfoList = [NSMutableArray array];
  353. }
  354. - (void)loadUrl: (NSString *)url
  355. {
  356. NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]];
  357. [self loadRequest:request];
  358. }
  359. - (void)callHandler:(NSString *)methodName arguments:(NSArray *)args{
  360. [self callHandler:methodName arguments:args completionHandler:nil];
  361. }
  362. - (void)callHandler:(NSString *)methodName completionHandler:(void (^)(id _Nullable))completionHandler{
  363. [self callHandler:methodName arguments:nil completionHandler:completionHandler];
  364. }
  365. -(void)callHandler:(NSString *)methodName arguments:(NSArray *)args completionHandler:(void (^)(id _Nullable value))completionHandler
  366. {
  367. DSCallInfo *callInfo=[[DSCallInfo alloc] init];
  368. callInfo.id=[NSNumber numberWithInt: callId++];
  369. callInfo.args=args==nil?@[]:args;
  370. callInfo.method=methodName;
  371. if(completionHandler){
  372. [handerMap setObject:completionHandler forKey:callInfo.id];
  373. }
  374. if(callInfoList!=nil){
  375. [callInfoList addObject:callInfo];
  376. }else{
  377. [self dispatchJavascriptCall:callInfo];
  378. }
  379. }
  380. - (void)dispatchStartupQueue{
  381. if(callInfoList==nil) return;
  382. for (DSCallInfo * callInfo in callInfoList) {
  383. [self dispatchJavascriptCall:callInfo];
  384. }
  385. callInfoList=nil;
  386. }
  387. - (void) dispatchJavascriptCall:(DSCallInfo*) info{
  388. NSString * json=[JSBUtil objToJsonString:@{@"method":info.method,@"callbackId":info.id,
  389. @"data":[JSBUtil objToJsonString: info.args]}];
  390. [self evaluateJavaScript:[NSString stringWithFormat:@"window._handleMessageFromNative(%@)",json]
  391. completionHandler:nil];
  392. }
  393. - (void) addJavascriptObject:(id)object namespace:(NSString *)namespace{
  394. if(namespace==nil){
  395. namespace=@"";
  396. }
  397. if(object!=NULL){
  398. [javaScriptNamespaceInterfaces setObject:object forKey:namespace];
  399. }
  400. }
  401. - (void) removeJavascriptObject:(NSString *)namespace {
  402. if(namespace==nil){
  403. namespace=@"";
  404. }
  405. [javaScriptNamespaceInterfaces removeObjectForKey:namespace];
  406. }
  407. - (void)customJavascriptDialogLabelTitles:(NSDictionary *)dic{
  408. if(dic){
  409. dialogTextDic=dic;
  410. }
  411. }
  412. - (id)onMessage:(NSDictionary *)msg type:(int)type{
  413. id ret=nil;
  414. switch (type) {
  415. case DSB_API_HASNATIVEMETHOD:
  416. ret= [self hasNativeMethod:msg]?@1:@0;
  417. break;
  418. case DSB_API_CLOSEPAGE:
  419. [self closePage:msg];
  420. break;
  421. case DSB_API_RETURNVALUE:
  422. ret=[self returnValue:msg];
  423. break;
  424. case DSB_API_DSINIT:
  425. ret=[self dsinit:msg];
  426. break;
  427. case DSB_API_DISABLESAFETYALERTBOX:
  428. [self disableJavascriptDialogBlock:[msg[@"disable"] boolValue]];
  429. break;
  430. default:
  431. break;
  432. }
  433. return ret;
  434. }
  435. - (bool) hasNativeMethod:(NSDictionary *) args
  436. {
  437. NSArray *nameStr=[JSBUtil parseNamespace:[args[@"name"]stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]];
  438. NSString * type= [args[@"type"] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
  439. id JavascriptInterfaceObject= [javaScriptNamespaceInterfaces objectForKey:nameStr[0]];
  440. if(JavascriptInterfaceObject){
  441. bool syn=[JSBUtil methodByNameArg:1 selName:nameStr[1] class:[JavascriptInterfaceObject class]]!=nil;
  442. bool asyn=[JSBUtil methodByNameArg:2 selName:nameStr[1] class:[JavascriptInterfaceObject class]]!=nil;
  443. if(([@"all" isEqualToString:type]&&(syn||asyn))
  444. ||([@"asyn" isEqualToString:type]&&asyn)
  445. ||([@"syn" isEqualToString:type]&&syn)
  446. ){
  447. return true;
  448. }
  449. }
  450. return false;
  451. }
  452. - (id) closePage:(NSDictionary *) args{
  453. if(javascriptCloseWindowListener){
  454. javascriptCloseWindowListener();
  455. }
  456. return nil;
  457. }
  458. - (id) returnValue:(NSDictionary *) args{
  459. void (^ completionHandler)(NSString * _Nullable)= handerMap[args[@"id"]];
  460. if(completionHandler){
  461. if(isDebug){
  462. completionHandler(args[@"data"]);
  463. }else{
  464. @try{
  465. completionHandler(args[@"data"]);
  466. }@catch (NSException *e){
  467. NSLog(@"%@",e);
  468. }
  469. }
  470. id complete = args[@"complete"];
  471. if (complete && ![complete boolValue]) {
  472. } else {
  473. [handerMap removeObjectForKey:args[@"id"]];
  474. }
  475. }
  476. return nil;
  477. }
  478. - (id) dsinit:(NSDictionary *) args{
  479. [self dispatchStartupQueue];
  480. return nil;
  481. }
  482. - (void) disableJavascriptDialogBlock:(bool) disable{
  483. jsDialogBlock=!disable;
  484. }
  485. - (void)hasJavascriptMethod:(NSString *)handlerName methodExistCallback:(void (^)(bool exist))callback{
  486. [self callHandler:@"_hasJavascriptMethod" arguments:@[handlerName] completionHandler:^(NSNumber* _Nullable value) {
  487. callback([value boolValue]);
  488. }];
  489. }
  490. @end