网络开发中,当公司已经使用 HTML5 技术实现同时适应 Android 和 iOS 等多个平台的网页时,这时往往需要我们 iOS 平台能够嵌入网页并进行各种交互,那我们应该怎么做来实现这种需求呢?
这里我们考虑的方案就是:使用 UIWebView 网页控件
然而考虑使用 UIWebView 进行混合编程的场景特点有:
(1)排版复杂:通常包括图片和文字的混排,还有可能包括链接需要支持点击操作。如果自己用原生控件拼装实现,由于界面元素过多,做起来会很困难。就算是使用 CoreText 来实现,也需要自己实现相当多的复杂排版逻辑。
(2)界面的变动需求频繁:例如淘宝 App 的彩票页面,可能常常需要更新界面以推出不同的活动。采用 UIWebView 嵌套页面实现后,这类页面不需要向 App Store 提交新的版本就可以动态地更新,而原生控件实现的界面很难达到如此高的灵活性。
(3)界面对用户的交互需求不复杂:因为 UIWebView 实现的交互效果与原生效果相比还是会大打折扣,所以这类界面通常都没有复杂的交互效果。这也是主流 App 大多采用混合 UIWebView 来实现界面而不是使用纯 UIWebView 来实现界面的原因之一,另外的原因是纯 UIWebView 实现的性能也没原生的好,而且纯 UIWebView 实现的话根本无法通过 App Store 的审核,他会建议您搞 Web App。
本随笔简单介绍如何使用 UIWebView 网页控件:
(1)简单浏览器
- file://WhatsNewIniPhoneOS.pdf 读取本地资源文件
- http://www.apple.com 读取网站内容
- KenmuHuang 读取百度搜索网站相关查询内容
(2)页面交互;可以考虑使用模版引擎库 GRMustache:https://github.com/groue/GRMustache,方便阅读和更改渲染内容
(3)页面调用 OC 方法;可以考虑使用 JS 与 OC 交互库「WebViewJavascriptBridge」:https://github.com/marcuswestin/WebViewJavascriptBridge,方便 JS 与 OC 之间发送消息进行交互
效果如下:
ViewController.h
1 #import <UIKit/UIKit.h>
2
3 @interface ViewController : UITableViewController
4 @property (copy, nonatomic) NSArray *arrSampleName;
5
6 - (instancetype)initWithSampleNameArray:(NSArray *)arrSampleName;
7
8 @end
ViewController.m
1 #import "ViewController.h"
2 #import "SimpleBrowserViewController.h"
3 #import "PageInteractionViewController.h"
4 #import "PageCallOCFunctionViewController.h"
5
6 @interface ViewController ()
7 - (void)layoutUI;
8 @end
9
10 @implementation ViewController
11 - (void)viewDidLoad {
12 [super viewDidLoad];
13
14 [self layoutUI];
15 }
16
17 - (void)didReceiveMemoryWarning {
18 [super didReceiveMemoryWarning];
19 // Dispose of any resources that can be recreated.
20 }
21
22 - (instancetype)initWithSampleNameArray:(NSArray *)arrSampleName {
23 if (self = [super initWithStyle:UITableViewStyleGrouped]) {
24 self.navigationItem.title = @"UIWebView 操作";
25 self.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"返回" style:UIBarButtonItemStylePlain target:nil action:nil];
26
27 _arrSampleName = arrSampleName;
28 }
29 return self;
30 }
31
32 - (void)layoutUI {
33
34 }
35
36 #pragma mark - UITableViewController相关方法重写
37 - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
38 return 0.1;
39 }
40
41 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
42 return 1;
43 }
44
45 - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
46 return [_arrSampleName count];
47 }
48
49 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
50 static NSString *cellIdentifier = @"cell";
51 UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
52 if (!cell) {
53 cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
54 }
55 cell.textLabel.text = _arrSampleName[indexPath.row];
56 return cell;
57 }
58
59 - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
60 switch (indexPath.row) {
61 case 0: {
62 SimpleBrowserViewController *simpleBrowserVC = [SimpleBrowserViewController new];
63 [self.navigationController pushViewController:simpleBrowserVC animated:YES];
64 break;
65 }
66 case 1: {
67 PageInteractionViewController *pageInteractionVC = [PageInteractionViewController new];
68 [self.navigationController pushViewController:pageInteractionVC animated:YES];
69 break;
70 }
71 case 2: {
72 PageCallOCFunctionViewController *pageCallOCFunctionVC = [PageCallOCFunctionViewController new];
73 [self.navigationController pushViewController:pageCallOCFunctionVC animated:YES];
74 break;
75 }
76 default:
77 break;
78 }
79 }
80
81 @end
PrefixHeader.pch
1 #define kTitleOfSimpleBrowser @"简单浏览器"
2 #define kTitleOfPageInteraction @"页面交互"
3 #define kTitleOfPageCallOCFunction @"页面调用 OC 方法"
4
5 #define kApplication [UIApplication sharedApplication]
SimpleBrowserViewController.h
1 #import <UIKit/UIKit.h>
2
3 @interface SimpleBrowserViewController : UIViewController <UISearchBarDelegate, UIWebViewDelegate>
4 @property (strong, nonatomic) UISearchBar *searchBar;
5 @property (strong, nonatomic) UIWebView *webView;
6 @property (strong, nonatomic) UIToolbar *toolbar;
7 @property (strong, nonatomic) UIBarButtonItem *barBtnBack;
8 @property (strong, nonatomic) UIBarButtonItem *barBtnForward;
9
10 @end
SimpleBrowserViewController.m
1 #import "SimpleBrowserViewController.h"
2
3 @interface SimpleBrowserViewController ()
4 - (void)webViewBack;
5 - (void)webViewForward;
6 - (void)layoutUI;
7 - (void)sendRequest:(NSString *)requestURLStr;
8 - (void)changeBarButtonStatus;
9 @end
10
11 @implementation SimpleBrowserViewController
12
13 - (void)viewDidLoad {
14 [super viewDidLoad];
15
16 [self layoutUI];
17 }
18
19 - (void)didReceiveMemoryWarning {
20 [super didReceiveMemoryWarning];
21 // Dispose of any resources that can be recreated.
22 }
23
24 - (void)webViewBack {
25 [_webView goBack];
26 }
27
28 - (void)webViewForward {
29 [_webView goForward];
30 }
31
32 - (void)layoutUI {
33 self.navigationItem.title = kTitleOfSimpleBrowser;
34 self.automaticallyAdjustsScrollViewInsets = NO; //是否自动适应滚动视图的内嵌入;默认为YES,这里设置为NO,避免网页控件中_UIWebViewScrollView的UIWebBrowserView位置偏移
35
36 CGRect rect = [[UIScreen mainScreen] bounds];
37 static const CGFloat heightOfStatusBarAndNavigationBar = 64.0;
38 CGFloat widthOfScreen = rect.size.width;
39 CGFloat heightOfScreen = rect.size.height;
40
41 //添加搜索栏
42 _searchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(0.0, heightOfStatusBarAndNavigationBar, widthOfScreen, 44.0)];
43 _searchBar.delegate = self;
44 _searchBar.placeholder = @"请输入以file://或http开头的地址";
45 [self.view addSubview:_searchBar];
46
47 //添加工具栏和左右按钮(回退和前进)
48 _toolbar = [[UIToolbar alloc] initWithFrame:CGRectMake(0.0, heightOfScreen - 44.0,
49 widthOfScreen, 44.0)];
50 rect = CGRectMake(0.0, 0.0, 32.0, 32.0);
51 UIButton *btnBack = [UIButton buttonWithType:UIButtonTypeCustom];
52 btnBack.frame = rect;
53 [btnBack setImage:[UIImage imageNamed:@"LastPageNormal"] forState:UIControlStateNormal];
54 [btnBack setImage:[UIImage imageNamed:@"LastPageDisabled"] forState:UIControlStateDisabled];
55 [btnBack addTarget:self
56 action:@selector(webViewBack)
57 forControlEvents:UIControlEventTouchUpInside];
58 _barBtnBack = [[UIBarButtonItem alloc] initWithCustomView:btnBack];
59 _barBtnBack.enabled = NO;
60
61 UIBarButtonItem *barBtnSpacing=[[UIBarButtonItem alloc]initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
62
63 UIButton *btnForward = [UIButton buttonWithType:UIButtonTypeCustom];
64 btnForward.frame = rect;
65 [btnForward setImage:[UIImage imageNamed:@"NextPageNormal"] forState:UIControlStateNormal];
66 [btnForward setImage:[UIImage imageNamed:@"NextPageDisabled"] forState:UIControlStateDisabled];
67 [btnForward addTarget:self
68 action:@selector(webViewForward)
69 forControlEvents:UIControlEventTouchUpInside];
70 _barBtnForward = [[UIBarButtonItem alloc] initWithCustomView:btnForward];
71 _barBtnForward.enabled = NO;
72
73 _toolbar.items = @[ _barBtnBack, barBtnSpacing, _barBtnForward ];
74 [self.view addSubview:_toolbar];
75
76 //添加网页控件
77 CGFloat heightOfWebView = heightOfScreen - _searchBar.frame.origin.y - _searchBar.frame.size.height - _toolbar.frame.size.height;
78 _webView = [[UIWebView alloc] initWithFrame:CGRectMake(0.0, heightOfStatusBarAndNavigationBar + _searchBar.frame.size.height,
79 widthOfScreen, heightOfWebView)];
80 _webView.dataDetectorTypes = UIDataDetectorTypeAll;
81 _webView.delegate = self;
82 [self.view addSubview:_webView];
83 }
84
85 - (void)sendRequest:(NSString *)requestURLStr {
86 if (requestURLStr.length > 0) {
87 NSURL *requestURL;
88
89 //加载bundle中的文件;网页控件打开本地pdf、word文件依靠的并不是他自身解析,而是依靠MIME Type识别文件类型并调用对应应用打开
90 if ([requestURLStr hasPrefix:@"file://"]) {
91 NSRange range = [requestURLStr rangeOfString:@"file://"];
92 NSString *fileName = [requestURLStr substringFromIndex:range.length];
93 requestURL = [[NSBundle mainBundle] URLForResource:fileName
94 withExtension:nil];
95 } else {
96 //加载百度搜索网站的内容
97 if (![requestURLStr hasPrefix:@"http"]) {
98 requestURLStr = [NSString stringWithFormat:@"https://www.baidu.com/s?wd=%@", requestURLStr];
99 }
100 //最终加载的还是HTTP或者HTTPS协议的网站内容,进行编码操作
101 requestURLStr = [requestURLStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
102 requestURL = [NSURL URLWithString:requestURLStr];
103 }
104
105 //加载请求地址内容
106 [_webView loadRequest:[NSURLRequest requestWithURL:requestURL]];
107 }
108 }
109
110 - (void)changeBarButtonStatus {
111 _barBtnBack.enabled = _webView.canGoBack;
112 _barBtnForward.enabled = _webView.canGoForward;
113 }
114
115 #pragma mark - UIWebViewDelegate
116 - (void)webViewDidStartLoad:(UIWebView *)webView {
117 kApplication.networkActivityIndicatorVisible = YES;
118 }
119
120 - (void)webViewDidFinishLoad:(UIWebView *)webView {
121 kApplication.networkActivityIndicatorVisible = NO;
122 [self changeBarButtonStatus];
123 }
124
125 - (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error {
126 NSLog(@"Error: %@", error);
127 kApplication.networkActivityIndicatorVisible = NO;
128 UIAlertView *alertVCustom = [[UIAlertView alloc] initWithTitle:@"提示信息"
129 message:@"网络连接错误"
130 delegate:nil
131 cancelButtonTitle:@"确定"
132 otherButtonTitles:nil, nil];
133 [alertVCustom show];
134 }
135
136 #pragma mark - UISearchBarDelegate
137 - (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar {
138 [self sendRequest:searchBar.text];
139 [_searchBar resignFirstResponder];
140 }
141
142 @end
PageInteractionViewController.h
1 #import <UIKit/UIKit.h>
2
3 @interface PageInteractionViewController : UIViewController<UIWebViewDelegate>
4 @property (strong, nonatomic) UIWebView *webView;
5
6 @end
PageInteractionViewController.m
1 #import "PageInteractionViewController.h"
2
3 @interface PageInteractionViewController ()
4 - (void)layoutUI;
5 - (void)sendRequest;
6 @end
7
8 @implementation PageInteractionViewController
9
10 - (void)viewDidLoad {
11 [super viewDidLoad];
12
13 [self layoutUI];
14
15 [self sendRequest];
16 }
17
18 - (void)didReceiveMemoryWarning {
19 [super didReceiveMemoryWarning];
20 // Dispose of any resources that can be recreated.
21 }
22
23 - (void)layoutUI {
24 self.navigationItem.title = kTitleOfPageInteraction;
25 self.automaticallyAdjustsScrollViewInsets = NO; //是否自动适应滚动视图的内嵌入;默认为YES,这里设置为NO,避免网页控件中_UIWebViewScrollView的UIWebBrowserView位置偏移
26
27 CGRect rect = [[UIScreen mainScreen] bounds];
28 static const CGFloat heightOfStatusBarAndNavigationBar = 64.0;
29 //添加网页控件
30 _webView = [[UIWebView alloc] initWithFrame:CGRectMake(0.0, heightOfStatusBarAndNavigationBar,
31 rect.size.width, rect.size.height - heightOfStatusBarAndNavigationBar)];
32 _webView.dataDetectorTypes = UIDataDetectorTypeAll;
33 _webView.delegate = self;
34 self.automaticallyAdjustsScrollViewInsets = NO;
35 [self.view addSubview:_webView];
36 }
37
38 - (void)sendRequest {
39 NSString *htmlStr=@"<html>\
40 <head><title>KenmuHuang's Blog</title></head>\
41 <body style=\"color:#0044AA;\">\
42 <h3 id=\"header\">I'm KenmuHuang</h3>\
43 <p>More coding, more thinking. Stay hungry, stay foolish.</p>\
44 <span>http://www.cnblogs.com/huangjianwu/</span>\
45 </body>\
46 </html>";
47 [_webView loadHTMLString:htmlStr baseURL:nil];
48 }
49
50 #pragma mark - UIWebViewDelegate
51 - (void)webViewDidStartLoad:(UIWebView *)webView {
52 kApplication.networkActivityIndicatorVisible = YES;
53 }
54
55 - (void)webViewDidFinishLoad:(UIWebView *)webView {
56 kApplication.networkActivityIndicatorVisible = NO;
57 NSLog(@"%@",[_webView stringByEvaluatingJavaScriptFromString:@"document.title"]); //打印内容为html的title标签内容:KenmuHuang's Blog
58 NSLog(@"%@",[_webView stringByEvaluatingJavaScriptFromString:@"document.getElementById('header').innerHTML='KenmuHuang\\'s Blog';"]); //必须使用双反斜杆来转译单引号
59 }
60
61 - (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error {
62 NSLog(@"Error: %@", error);
63 kApplication.networkActivityIndicatorVisible = NO;
64 UIAlertView *alertVCustom = [[UIAlertView alloc] initWithTitle:@"提示信息"
65 message:@"网络连接错误"
66 delegate:nil
67 cancelButtonTitle:@"确定"
68 otherButtonTitles:nil, nil];
69 [alertVCustom show];
70 }
71
72 @end
RedirectURL.js
1 function showSheet(title, cancelButtonTitle, destructiveButtonTitle, otherButtonTitle) {
2 var url = 'KMActionSheet://?';
3 var paramas = title + '&' + cancelButtonTitle + '&' + destructiveButtonTitle;
4 if(otherButtonTitle) {
5 paramas += '&' + otherButtonTitle;
6 }
7 window.location.href = url + encodeURIComponent(paramas);
8 }
9
10 var header = document.getElementById('header');
11 if(header) {
12 header.onclick = function(){
13 showSheet('系统提示', '取消', '确定', null);
14 };
15 }
PageCallOCFunctionViewController.h
1 #import <UIKit/UIKit.h>
2
3 @interface PageCallOCFunctionViewController : UIViewController<UIWebViewDelegate>
4 @property (strong, nonatomic) UIWebView *webView;
5
6 @end
PageCallOCFunctionViewController.m
1 #import "PageCallOCFunctionViewController.h"
2
3 @interface PageCallOCFunctionViewController ()
4 - (void)layoutUI;
5 - (void)sendRequest;
6 - (void)showActionSheetWithTitle:(NSString *)title cancelButtonTitle:(NSString *)cancelButtonTitle destructiveButtonTitle:(NSString *)destructiveButtonTitle otherButtonTitle:(NSString *)otherButtonTitle;
7 @end
8
9 @implementation PageCallOCFunctionViewController
10
11 - (void)viewDidLoad {
12 [super viewDidLoad];
13
14 [self layoutUI];
15
16 [self sendRequest];
17 }
18
19 - (void)didReceiveMemoryWarning {
20 [super didReceiveMemoryWarning];
21 // Dispose of any resources that can be recreated.
22 }
23
24 - (void)layoutUI {
25 self.navigationItem.title = kTitleOfPageCallOCFunction;
26 self.automaticallyAdjustsScrollViewInsets = NO; //是否自动适应滚动视图的内嵌入;默认为YES,这里设置为NO,避免网页控件中_UIWebViewScrollView的UIWebBrowserView位置偏移
27
28 CGRect rect = [[UIScreen mainScreen] bounds];
29 static const CGFloat heightOfStatusBarAndNavigationBar = 64.0;
30 //添加网页控件
31 _webView = [[UIWebView alloc] initWithFrame:CGRectMake(0.0, heightOfStatusBarAndNavigationBar,
32 rect.size.width, rect.size.height - heightOfStatusBarAndNavigationBar)];
33 _webView.dataDetectorTypes = UIDataDetectorTypeAll;
34 _webView.delegate = self;
35 self.automaticallyAdjustsScrollViewInsets = NO;
36 [self.view addSubview:_webView];
37 }
38
39 - (void)sendRequest {
40 NSString *htmlStr=@"<html>\
41 <head><title>KenmuHuang's Blog</title></head>\
42 <body style=\"color:#0044AA;\">\
43 <h3 id=\"header\">I'm KenmuHuang, click me to show action sheet.</h3>\
44 <p>More coding, more thinking. Stay hungry, stay foolish.</p>\
45 <span>http://www.cnblogs.com/huangjianwu/</span>\
46 </body>\
47 </html>";
48 [_webView loadHTMLString:htmlStr baseURL:nil];
49 }
50
51 - (void)showActionSheetWithTitle:(NSString *)title cancelButtonTitle:(NSString *)cancelButtonTitle destructiveButtonTitle:(NSString *)destructiveButtonTitle otherButtonTitle:(NSString *)otherButtonTitle{
52 UIActionSheet *actionSheet=[[UIActionSheet alloc] initWithTitle:title
53 delegate:nil
54 cancelButtonTitle:cancelButtonTitle
55 destructiveButtonTitle:destructiveButtonTitle
56 otherButtonTitles:otherButtonTitle, nil];
57 [actionSheet showInView:self.view];
58 }
59
60 #pragma mark - UIWebViewDelegate
61 - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
62 BOOL isStartLoad = YES;
63 if ([request.URL.scheme isEqualToString:@"kmactionsheet"]) { //request.URL.scheme 返回全小写的内容
64 NSString *paramContent = request.URL.query;
65 NSArray *arrParam = [[paramContent stringByRemovingPercentEncoding] componentsSeparatedByString:@"&"];
66 NSString *otherButtonTitle = nil;
67 if (arrParam.count > 3) {
68 otherButtonTitle = arrParam[3];
69 }
70
71 [self showActionSheetWithTitle:arrParam[0]
72 cancelButtonTitle:arrParam[1]
73 destructiveButtonTitle:arrParam[2]
74 otherButtonTitle:otherButtonTitle];
75 isStartLoad = NO;
76 }
77 return isStartLoad;
78 }
79
80 - (void)webViewDidStartLoad:(UIWebView *)webView {
81 kApplication.networkActivityIndicatorVisible = YES;
82 }
83
84 - (void)webViewDidFinishLoad:(UIWebView *)webView {
85 kApplication.networkActivityIndicatorVisible = NO;
86
87 //加载用于重定向地址的JavaScript内容
88 NSString *jsPath = [[NSBundle mainBundle] pathForResource:@"RedirectURL.js" ofType:nil];
89 NSString *jsContent = [NSString stringWithContentsOfFile:jsPath
90 encoding:NSUTF8StringEncoding
91 error:nil];
92 [_webView stringByEvaluatingJavaScriptFromString:jsContent];
93 }
94
95 - (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error {
96 NSLog(@"Error: %@", error);
97 kApplication.networkActivityIndicatorVisible = NO;
98 UIAlertView *alertVCustom = [[UIAlertView alloc] initWithTitle:@"提示信息"
99 message:@"网络连接错误"
100 delegate:nil
101 cancelButtonTitle:@"确定"
102 otherButtonTitles:nil, nil];
103 [alertVCustom show];
104 }
105
106 @end
AppDelegate.h
1 #import <UIKit/UIKit.h>
2
3 @interface AppDelegate : UIResponder <UIApplicationDelegate>
4
5 @property (strong, nonatomic) UIWindow *window;
6 @property (strong, nonatomic) UINavigationController *navigationController;
7
8 @end
AppDelegate.m
1 #import "AppDelegate.h"
2 #import "ViewController.h"
3
4 @interface AppDelegate ()
5
6 @end
7
8 @implementation AppDelegate
9
10
11 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
12 _window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
13 ViewController *viewController = [[ViewController alloc]
14 initWithSampleNameArray:@[ kTitleOfSimpleBrowser,
15 kTitleOfPageInteraction,
16 kTitleOfPageCallOCFunction ]];
17 _navigationController = [[UINavigationController alloc] initWithRootViewController:viewController];
18 _window.rootViewController = _navigationController;
19 //[_window addSubview:_navigationController.view]; //当_window.rootViewController关联时,这一句可有可无
20 [_window makeKeyAndVisible];
21 return YES;
22 }
23
24 - (void)applicationWillResignActive:(UIApplication *)application {
25 }
26
27 - (void)applicationDidEnterBackground:(UIApplication *)application {
28 }
29
30 - (void)applicationWillEnterForeground:(UIApplication *)application {
31 }
32
33 - (void)applicationDidBecomeActive:(UIApplication *)application {
34 }
35
36 - (void)applicationWillTerminate:(UIApplication *)application {
37 }
38
39 @end