项目中有个新闻资讯模块展现公司和相关行业的最新动态。
这个部分基本是以展现网页为主,内部可能会有一些 native 和 JS 代码的交互。
由于是新项目,因此决定采用 iOS 8 中新出的 WebKit。
本文是对 WebKit 框架中 WKWebView 的一些学习和封装css
WKWebViewDemo 地址html
这二者都是 iOS 中展现 web 相关的组件。前者 iOS 2.0 就有了,后者是 iOS 8.0 时候新加的。java
网络中关于二者的差别和性能对比分析不少,这里再也不赘述。只是说明一下苹果文档中的重要提示以及本身须要功能的实现:ios
官方文档中重要提示
nginx
文档中主要说了如下几点:git
WKPreferences
的 javaScriptEnabled
属性决定是否支持 web 内容执行 JavaScript 代码NSPhotoLibraryUsageDescription
和 NSCameraUsageDescription
不然会直接 crashloadHTMLString:baseURL:
方法loadRequest:
方法stopLoading
方法用来结束加载。loading
属性查看WK进程中是否加载中goBack
和 goForward
方法可用于 WKWebView 的前进后退. canGoBack
和 canGoForward
属性来查看是否能前进后退dataDetectorTypes
属性中 UIDataDetectorTypes
的位字段不包含 UIDataDetectorTypePhoneNumber
.scalesPageToFit
属性用于第一次加载网页内容时候设置是否可使用手势改变网页缩放。 设置后用户就能够手势缩放网页大小delegate
并遵照 UIWebViewDelegate
协议有了基本的概念,就能够去看一下 WKWebView 的具体文档了。若是怕官方文档麻烦也能够直接看网络上别人整理好的网络整理。github
下面是我整理的 WebView 和 H5 调用逻辑图:web
特别说明一点:json
0. OC 执行 JS 方法 - (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^)(id, NSError *error))completionHandler; 这个方法中webView调用JS,block只是成功或失败的回调 1. JS方法中 window.webkit.messageHandlers.webViewApp.postMessage(message); 做用是JS 向以前注册的 webViewApp 发送消息。 OC 端接到消息会调用 <WKScriptMessageHandler> 中下面代理方法 - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
对于项目而言,网页功能无需太多,主要知足如下几点canvas
基本展现功能
JS和OC交互功能:交互的数据格式和方法名等须要和H5端具体协调
以上这些基本功能中基本展现功能都比较简单,和JS交互的部分须要和 H5 端小伙伴共同定义数据结构和互调的方法名、参数等。因此须要具体问题具体分析。项目以我本身 Demo 为例说一下。
为实现功能主要封装了三个类
XYWKWebViewController ---> 管理 webView 加载相关的代理方法 XYWKWebView ---> 封装 webView 请求相关方法 XYScriptMessage ---> 封装JS回调信息
XYWKWebView 核心功能
加载本地HTML文件
/** * 加载本地HTML页面 * * @param htmlName html页面文件名称 */ - (void)loadLocalHTMLWithFileName:(nonnull NSString *)htmlName
实现代码
- (void)loadLocalHTMLWithFileName:(nonnull NSString *)htmlName { NSString *path = [[NSBundle mainBundle] bundlePath]; NSURL *baseURL = [NSURL fileURLWithPath:path]; NSString * htmlPath = [[NSBundle mainBundle] pathForResource:htmlName ofType:@"html"]; NSString * htmlCont = [NSString stringWithContentsOfFile:htmlPath encoding:NSUTF8StringEncoding error:nil]; // WKWebView 的 loadHTMLString: 方法 [self loadHTMLString:htmlCont baseURL:baseURL]; }
加载网络内容
// 加载网络内容,根据是否有参数选不一样方法 - (void)loadRequestWithRelativeUrl:(nonnull NSString *)relativeUrl; - (void)loadRequestWithRelativeUrl:(nonnull NSString *)relativeUrl params:(nullable NSDictionary *)params;
实现代码
- (void)loadRequestWithRelativeUrl:(NSString *)relativeUrl params:(NSDictionary *)params { NSURL *url = [self generateURL:relativeUrl params:params]; [self loadRequest:[NSURLRequest requestWithURL:url]]; } - (NSURL *)generateURL:(NSString*)baseURL params:(NSDictionary*)params { self.webViewRequestUrl = baseURL; self.webViewRequestParams = params; NSMutableDictionary *param = [NSMutableDictionary dictionaryWithDictionary:params]; NSMutableArray* pairs = [NSMutableArray array]; //能够在这里将token参数添加进去,这样就能够实现补全token功能 for (NSString* key in param.keyEnumerator) { NSString *value = [NSString stringWithFormat:@"%@",[param objectForKey:key]]; NSString* escaped_value = (__bridge_transfer NSString *)CFURLCreateStringByAddingPercentEscapes(NULL, (__bridge CFStringRef)value, NULL, (CFStringRef)@"!*'\"();:@&=+$,/?%#[]% ", kCFStringEncodingUTF8); [pairs addObject:[NSString stringWithFormat:@"%@=%@", key, escaped_value]]; } NSString *query = [pairs componentsJoinedByString:@"&"]; baseURL = [baseURL stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; NSString* url = @""; if ([baseURL containsString:@"?"]) { url = [NSString stringWithFormat:@"%@&%@",baseURL, query]; } else { url = [NSString stringWithFormat:@"%@?%@",baseURL, query]; } //绝对地址 if ([url.lowercaseString hasPrefix:@"http"]) { return [NSURL URLWithString:url]; } else { return [NSURL URLWithString:url relativeToURL:self.baseUrl]; } }
XYWKWebViewController 核心功能
这是一个 Controller,建议建立新的Controller继承XYWKWebViewController 来使用,这样能够把不一样的页面区分开,每一个页面加载的url和相关的业务逻辑均可以单独处理,代码易读,也容易维护。若是项目后期添加功能也好处理
XYWKWebViewController主要完成了对一些功能的封装,好比进度条、页面title以及 webView 的生命周期。
进度条和title都是经过KVO实现
if (self.shouldShowProgress) { [self.webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:NULL]; } if (self.isUseWebPageTitle) { [self.webView addObserver:self forKeyPath:@"title" options:NSKeyValueObservingOptionNew context:NULL]; }
设置title 和 progressView 直接是本身简单写了一个 View
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context { if ([keyPath isEqualToString:@"estimatedProgress"]) { if (object == self.webView) { [self showLoadingProgress:self.webView.estimatedProgress andTintColor:[UIColor colorWithRed:24/255.0 green:124/255.0 blue:244/255.0f alpha:1.0]]; } else{ [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } } else if ([keyPath isEqualToString:@"title"]){ if (object == self.webView) { if ([self isUseWebPageTitle]) { self.title = self.webView.title; } } else{ [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } }
OC 与 JS 之间交互的处理
这部分是可定制化功能最多的,遇到的问题也是最多的。WKWebView 和 JS 之间的交互须要设置 ScriptMessageHandler 以下。
- (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration { self = [super initWithFrame:frame configuration:configuration]; if (self) { if (configuration) { //文档中说 //Adds a script message handler. //Adding a script message handler with name name causes the JavaScript function window.webkit.messageHandlers.name.postMessage(messageBody) to be defined in all frames in all web views that use the user content controller. // 这里就是设置 网页中 JS Message handler // 经过 “name” 注册以后,JS 内部函数 window.webkit.messageHandlers.“name”.postMessage(messageBody) 就被定义到整个用户的Web内容的控制器中。 //后面的JS调用OC也是经过 “name” 联系的 [configuration.userContentController addScriptMessageHandler:self name:@"webViewApp"]; } //默认容许系统自带的侧滑后退 self.allowsBackForwardNavigationGestures = YES; } return self; }
而后实现 WKScriptMessageHandler 代理
// JS 调用 OC 的回调。JS 向OC 发送消息会调用这个方法 - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { NSLog(@"获得的 JS message 是 :%@",message.body); if ([message.body isKindOfClass:[NSDictionary class]]) { NSDictionary *body = (NSDictionary *)message.body; // 这里是对 JS 消息的一个处理,用本身定义的消息类型,封装并发送给代理去外部处理,具体格式须要工做中和H5共同制定 XYScriptMessage *msg = [XYScriptMessage new]; [msg setValuesForKeysWithDictionary:body]; if (self.xy_messageHandlerDelegate && [self.xy_messageHandlerDelegate respondsToSelector:@selector(xy_webView:didReceiveScriptMessage:)]) { [self.xy_messageHandlerDelegate xy_webView:self didReceiveScriptMessage:msg]; } } }
其中自定义的 XYScriptMessage 以下
/** * WKWebView与JS调用时参数规范实体 */ @interface XYScriptMessage : NSObject /** * 方法名 * 用来肯定Native App的执行逻辑 */ @property (nonatomic, copy) NSString *method; /** * 方法参数 * json字符串 */ @property (nonatomic, copy) NSDictionary *params; /** * 回调函数名 * Native App执行完后回调的JS方法名 */ @property (nonatomic, copy) NSString *callback; @end
同时提供delegate方法供XYWKWebViewController实现
/** * JS调用原生方法处理,其中方法名都须要和 H5 端相互协调 */ - (void)xy_webView:(XYWKWebView *)webView didReceiveScriptMessage:(XYScriptMessage *)message { NSLog(@"webView method:%@",message.method); //返回上一页 if ([message.method isEqualToString:@"tobackpage"]) { [self.navigationController popViewControllerAnimated:YES]; } //打开新页面 else if ([message.method isEqualToString:@"openappurl"]) { NSString *url = [message.params objectForKey:@"url"]; if (url.length) { XYWKWebViewController *webViewController = [[XYWKWebViewController alloc] init]; webViewController.url = url; [self.navigationController pushViewController:webViewController animated:YES]; } } }
一个提供四类使用功能,使用方法建议直接继承 XYWKWebViewController。
class WebViewController: XYWKWebViewController { override func viewDidLoad() { super.viewDidLoad() /// #用法0: 直接加载对应的地址 <没有参数> //self.webView.loadRequest(withRelativeUrl: "https://www.httpbin.org/") /// #用法1: 直接加载对应的地址 <有参数> //let params = ["name":"xiaoyou", // "password" : "123456#/HTTP_Methods/get_get"] //self.webView.loadRequest(withRelativeUrl: "https://www.httpbin.org/", params: params) /// #用法2: 直接加载本地HTML文件 <没有参数> self.webView.loadLocalHTML(withFileName: "main") /// #用法3: JS 注入,添加一些方法 <这里的原生坐标和JS之间没法直接相对应> let margin : CGFloat = 6.0 let padding : CGFloat = 10.0 let width = UIScreen.main.bounds.size.width - (margin * 2.0) - (margin * 7.0 + padding) let btnWidth = (width - padding - 5) / 2.0 let styleJS = """ <style type="text/css"> #foot { border:solid 10px #600; padding:\(padding)px; margin:\(margin)px; clear:both; width:\(width)px } #share { border:solid 1px #600; padding:2px; margin:2px; clear:both; width:\(btnWidth)px; heiht:150px } #like { border:solid 1px #600; padding:2px; margin:2px; clear:both; width:\(btnWidth)px; heiht:50px } </style> """ let funcJS = """ \t\t\tfunction testFunc(text){\n \t\t\t\tvar message = \"点我干什么\";\n \t\t\t\twindow.webkit.messageHandlers.webViewApp.postMessage(message);\n \t\t\t\talert(text);\n \t\t\t}\n """ let footerJS = """ \t<button onclick=\"testFunc('http://www.baidu.com/')\">本身添加的Footer的Button一个</button><br /><br /><br />\n \t <div id=\"foot\">底部说明 <br /> <button id=\"share\" onclick=\"testFunc('分享')\">分享</button> <button id=\"like\" onclick=\"testFunc('点赞')\">点赞</button><br /> </div> """ self.webView.loadLocalHTML("main", withAddingStyleJS: styleJS, funcJS: funcJS, footerJS: footerJS) /// 设置导航 self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "返回", style: .plain, target: self, action: #selector(backAction)); self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "调用JS", style: .plain, target: self, action: #selector(callJS)); } } /// #用法4: OC 调用JS方法。这里能够调用JS,把H5须要的参数传给他们 /// 这里是JS 回调方法 extension WebViewController{ @objc func backAction() { self.dismiss(animated: true, completion: nil) } @objc func callJS() { self.webView.callJS("call('Hello World!')") { (response) in print("\(String(describing: response))") } } /// 这里是重写了WebView接受到JS消息的回调,须要调用super方法才能执行内部方法,不然这里只是打印 override func xy_webView(_ webView: XYWKWebView, didReceive message: XYScriptMessage) { // 若是彻底自定义的js方法处理,无需重写父类,自行实现便可 super.xy_webView(webView, didReceive: message) print(message) } }
具体见Demo
HTML 中超连接,须要打开新页面的"_blank"处理
WebKit 的小封装能实现目前所需功能,但不少内容还须要在须要的时候去探究,但愿能帮到一样学习的小伙伴。
若是看完有收获,不妨点个赞,让我也更有分享的动力!