UIWebView與JS的交互,說白了就是Objective-C和JavaScript的相互調用。Objective-C調用JavaScript代碼的方法,是通過UIWebView的 - (NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;
的方法來實現的。該方法向UIWebView傳遞一段需要執行的JavaScript代碼最后獲取執行結果。
JavaScript調用Objective-C的方法,并沒有現成的API,但是有些方法可以達到相應的效果。具體是利用UIWebView的特性:在UIWebView的內發起的所有網絡請求,都可以通過delegate函數得到通知。
說明:
本文是一個小白記錄OC與JS交互的學習歷程,適合跟我一樣的小白,大神若要噴,請輕噴_
學習UIWebView與JS的交互之前,建議熟悉下HTML和Javascript的基本語法,不用看太多,在w3school看一到兩天HTML,再看一到兩天JS就行。
OC調用JS方法、JS調用OC方法(不使用第三方開源庫的情況下)
準備工作:
1.新建一個Single View Application,
再新建一個ViewController(eg:BasicUsageViewController),然后在StoryBoard新建一個ViewController,拖一個UIWebView和UILabel以備用,關聯webView及代理
@property (weak, nonatomic) IBOutlet UIWebView *webView;
@property (weak, nonatomic) IBOutlet UILabel *testLabel;
2.在工程中新建一個web1.html
文件(Commend+N、Other
、Empty
、Next
、輸入、create
),代碼如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>我是HTML標題</title>
</head>
<body bgcolor="#9aff9a">
<div id="addNewNodeTest">
<p id="p1"> 這是段落A。</p>
<p id="p2"> 這是段落B。</p>
</div>
<div class="page">
<button onclick="changeUILabelText()"> 改變UILabel文字 </button>
<button onclick="logText()"> NSLog打印文字 </button>
</div>
</body>
</html>
3.再在工程中新建一個test.js
文件,代碼如下:
//添加子節點
function addNewNodeTest () {
var para = document.createElement("p");
var node = document.createTextNode("這是新段落。");
para.appendChild(node);
var element = document.getElementById("addNewNodeTest");
element.appendChild(para);
console.log("添加子節點成功");
}
//改變UILabel的文本
function changeUILabelText() {
//"changelabeltext"是你自己定的一個協議。
//注url不要含大寫字母,就算是大寫字母,在`webView:shouldStartLoadWithRequest:navigationType:`代理方法里也會被替換成小寫字母
var url = "changelabeltext:" + "我是改變后的文字";
//給document.location重新賦值就相當于webView加載一個新的URL,所以又會調用`webView:shouldStartLoadWithRequest:navigationType:`方法,然后就可以在這個代理方法里截獲這個重定向請求
document.location = url;
}
//也可以自己封裝個傳參數的方法
function sendCommand(cmd,param){
var url = "yourprotocol:" + cmd + ":" + param;
document.location = url;
}
//打印測試
function logText(){
sendCommand("log","Hi,I'm In logText Function");
}
好了,現在可以開擼了
4.加載webView并插入測試js
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
NSString* path = [[NSBundle mainBundle] pathForResource:@"web1" ofType:@"html"];
[self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL fileURLWithPath:path]]];
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"js"];
NSString *jsString = [[NSString alloc] initWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
[self.webView stringByEvaluatingJavaScriptFromString:jsString];
}
5、加載結束,獲取HTML頁面title元素,賦值給self.title
- (void)webViewDidFinishLoad:(UIWebView *)webView {
// 獲取HTML頁面title元素,賦值給self.title
self.title = [webView stringByEvaluatingJavaScriptFromString:@"document.title"];
}
6、建幾個按鈕,體驗插入js的幾種方式
//分別對應上圖3個按鈕
- (IBAction)insertJavaScript1:(UIButton *)sender {
//方法1:預加載的test.js內部已經寫了addNewNodeTest()方法,這里只需注入"addNewNodeTest()"字符串即可
[self.webView stringByEvaluatingJavaScriptFromString:@"addNewNodeTest()"];
}
- (IBAction)insertJavaScript2:(UIButton *)sender {
//方法2:把test.js內部的addNewNodeTest()方法復制過來,去掉行與行之間的空格
//字符串雙引號要么前面加轉義符"\",要么變成單引號,例如:
NSString *addNewNode = @"var para = document.createElement(\"p\");var node=document.createTextNode('這是新段落。');para.appendChild(node);var element=document.getElementById('addNewNodeTest');element.appendChild(para);";
[self.webView stringByEvaluatingJavaScriptFromString:addNewNode];
}
- (IBAction)insertJavaScript3:(UIButton *)sender {
//方法3:把test.js內部的addNewNodeTest()方法復制過來,并在每一行首尾加上雙引號(跟方法2差不多)
NSString *addNewNode =
@"var para = document.createElement('p');"
"var node = document.createTextNode('這是新段落。');"
"para.appendChild(node);"
"var element = document.getElementById('addNewNodeTest');"
"element.appendChild(para);";
[self.webView stringByEvaluatingJavaScriptFromString:addNewNode];
}
說明:addNewNodeTest()方法執行的操作是創建了一個節點<p> 這是新段落。</p>
,添加到了位置1,然后webView上就會新增一行,不懂的同學請自行腦補(看不懂也沒關系,這里只是演示怎么用OC調js代碼)
<div id="addNewNodeTest">
<p id="p1"> 這是段落A。</p>
<p id="p2"> 這是段落B。</p>
//位置1
</div>
7.好了,現在讓js調OC的方法:在ViewController里添加如下代碼:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
NSLog(@"開始加載請求");
//當點擊按鈕時,navigationType = UIWebViewNavigationTypeOther
NSString *requestString = [[request URL] absoluteString];
NSArray *components = [requestString componentsSeparatedByString:@":"];
if ([components[0] isEqualToString:@"changelabeltext"] && components.count > 1) {
//這種通過URL傳參數的方式貌似不是太好,因為參數如果含中文還得URL解碼,eg:
self.testLabel.text = [components[1] stringByRemovingPercentEncoding];
return NO;
}
//也可以這樣判斷
else if([request.URL.scheme isEqualToString:@"yourprotocol"]) {
NSLog(@"%@",[components[2] stringByRemovingPercentEncoding]);
return NO;
}
return YES;
}
點擊webView里的改變UILabel文字
按鈕會發現testLabel
的文字變了,這里解釋下原因:web1.html
代碼中
<button onclick="changeUILabelText()"> 改變UILabel文字 </button>
這個按鈕綁定了一個方法,名字叫changeUILabelText()
,點擊就會調用changeUILabelText()方法(當然包含這個方法的test.js已經加載了),然后webView的URL變了就會重新加載,這樣在回調方法webView:shouldStartLoadWithRequest:navigationType:
會再次調用,然后就可以在這個代理方法里截獲這個重定向請求的request.URL.absoluteString
來處理OC代碼了
說明:
(1)Objective-C調用JavaScript代碼的時候是同步的
- (NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;
(2)JavaScript調用Objective-C代碼的時候是異步的
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
調試模擬器或真機里的WebView的技巧
- 模擬器
模擬器加載網頁后,打開電腦端Safari(確保偏好設置里的高級
- 在菜單中顯示“開發”菜單
選項已打開),然后選擇開發
、Simulator
,就會看見模擬器正在運行的web1.html
,點擊web1.html
就來到了控制臺。
點擊方式一
按鈕,就會發現控制臺標簽頁最先面有輸出,這是因為在test.js
的addNewNodeTest ()
的最后一行有這么一句話:console.log("添加子節點成功");
,在這里,console.log()
相當于NSLog,括號內可以直接加變量。
當然,你也可以在控制臺插入js代碼,如下圖:在左下角輸入一句js代碼alert('666');
就能在模擬器上得到反饋,當然,此時你輸入addNewNodeTest();
效果跟點擊方式一
按鈕是一樣的
你也可以切換到調試器
標簽,然后打個斷點,點擊方式一
按鈕,就可以單步調試了。有興趣的同學可以切換到元素
標簽頁看看
- 真機
首先在手機的設置
- Safari
- 高級
- 啟用Web檢查器
,然后用數據線連接電腦,Xcode運行你的項目,打開一個含webView的頁面,就可以在電腦端Safari的開發
菜單下看到你的設備了,調試方法同上
高級用法(WebViewJavascriptBridge)
WebViewJavascriptBridge 是一個用于UIWebView / WKWebViews和JS交互的封裝庫,連Facebook Messenger都在使用。
這里我就引用一下楊騎滔的這篇博客的內容,也就是通過實現以下功能來學習WebViewJavascriptBridge的使用(侵刪)。
原文已經比較詳盡了,但是有一些地方對于我等小白來說可能不夠詳細,所以折騰了不少時間,所以在這里對原文做了一點修改,更加清晰易懂。
要實現的功能
WebView展示一段HTML,禁止HTML文本中自帶的
<img>
標簽自動加載,也就是說下載圖片的操作放在native端來處理,并通過JS將圖片在Cache中的地址返回給UIWebview。實現點擊WebView圖片放大、保存圖片到相冊等操作。
之所以要把圖片操作放在native端做的好處在于:1、可以進行本地緩存,下次進入這篇文章可以直接從緩存讀取,提高響應速度并且節省用戶流量。2、可以實現點擊圖片放大、保存圖片到相冊等操作。
技術難點也有兩個:
- 如何讓HTML文本onLoad的時候,禁用自身的圖片加載而是從本地獲取圖片?
- 如何把native端下載好的圖片返回給網頁?
先來看看基本用法
在WebViewJavascriptBridge中,交互的方式只有兩種:send 和 callHandler,JS和OC都有這兩個方法,所以對應的四種關系是(很重要):
以上表中的對應關系的解讀是,例如第一條:在JS中如果調用了bridge.send(),那么將觸發OC端_bridge初始化方法中的回調。
同理,第二條,在JS中調用了bridge.callHandler('testJavascriptHandler'),它將觸發OC端注冊的同名方法:
也就是說,一種語言register了Handler(回調或者block),另一種語言callHandler就會執行回調或者block,還能傳遞數據;不理解不要緊,下面的Demo這四種方式全都有例子。
了解了使用規則,下面來看看在我們這個實際需求中應用的整體思路:
廢話不說,直接開擼:
1、導入WebViewJavascriptBridge
,新建一個ViewController,聲明一個WebViewJavascriptBridge實例:
@property WebViewJavascriptBridge* bridge;
2、找一個含圖片的html,比如這一篇(源碼已做刪減),導入到項目中
3、在項目中新建一個js文件,比如imageCache.js
,貼上如下代碼:
//一加載這個js就會調用下面自己寫的onLoaded() 方法
window.onload = function() {
onLoaded();
}
//使用WebViewJavascriptBridge的話,這一段是必須的(固定寫法)
function connectWebViewJavascriptBridge(callback) {
if (window.WebViewJavascriptBridge) {
callback(WebViewJavascriptBridge)
} else {
document.addEventListener('WebViewJavascriptBridgeReady', function() {
callback(WebViewJavascriptBridge)
}, false)
}
}
//上面已經說了,一插入js,這個方法就開始執行
function onLoaded() {
connectWebViewJavascriptBridge(function(bridge) {
//document.querySelectorAll:按文檔順序返回指定元素節點的子樹中匹配選擇器的元素集合,如果沒有匹配返回空集合
//下面這幾句是提取所有img標簽的esrc屬性值(圖片的URL),并存到imageUrlsArray這個數組中
var allImage = document.querySelectorAll("img");
allImage = Array.prototype.slice.call(allImage, 0);
var imageUrlsArray = new Array();
allImage.forEach(function(image) {
var esrc = image.getAttribute("esrc");
var newLength = imageUrlsArray.push(esrc);
});
//將imageUrlsArray這個數組發送到OC的block
bridge.send(imageUrlsArray);////四種關系圖表之第1種
bridge.init(function(message, responseCallback) {
alert(message);
if (responseCallback) {
responseCallback("Message1已收到,送你個Message2")
}
})
//這里先注冊下,等待OC代碼的_bridge調用([_bridge callHandler:....])
bridge.registerHandler('imagesDownloadComplete', function(data, responseCallback) {
var allImage = document.querySelectorAll("img");
allImage = Array.prototype.slice.call(allImage, 0);
allImage.forEach(function(image) {
if (image.getAttribute("esrc") == data[0] || image.getAttribute("esrc") == decodeURIComponent(data[0])) {
image.src = data[1];
}
});
responseCallback("圖片"+data[0]+"已加載")
})
//使用WebViewJavascriptBridge的話,這一段是必須的,不然上面的imageUrlsArray傳不過去
bridge.send('Please respond to this', function responseCallback(responseData) {
console.log("Javascript got its response", responseData)
})
});
}
4、viewDidLoad里加載 webView
NSString *path = [[NSBundle mainBundle] pathForResource:@"article1" ofType:@"html"];
//原網頁html代碼
NSString *_content = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
//我們要做的第一步是替換獲取的HTML文本中默認的src,避免webView自動加載圖片
_content = [_content stringByReplacingOccurrencesOfString:@"src" withString:@"esrc"];
//正則替換,給每個圖片添加一個onImageClick點擊方法
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"(<img[^>]+esrc=\")(\\S+)\"" options:0 error:nil];
//終于得到我想要的html了!!!
_content = [regex stringByReplacingMatchesInString:_content options:0 range:NSMakeRange(0, _content.length) withTemplate:@"<img esrc=\"$2\" onClick=\"javascript:onImageClick('$2')\""];
[self.webView loadHTMLString:_content baseURL:nil];
//插入js
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"imageCache" ofType:@"js"];
NSString *jsString = [[NSString alloc] initWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
[self.webView stringByEvaluatingJavaScriptFromString:jsString];
//初始化一個WebViewJavascript橋梁,方便imageCache.js把數據傳過來
self.bridge = [WebViewJavascriptBridge bridgeForWebView:self.webView webViewDelegate:self handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@"###來自imageCache.js的圖片URL數組: %@", data);
//利用SDWebImageManager下載圖片到本地
[self downloadAllImagesInNative:data];
_imageURLs = data;
responseCallback(@"###Right back atcha");
}];
#pragma mark -- 下載全部圖片
-(void)downloadAllImagesInNative:(NSArray *)imageUrls{
SDWebImageManager *manager = [SDWebImageManager sharedManager];
//初始化一個數組用于存image
_allImagesOfThisArticle = [NSMutableArray arrayWithCapacity:imageUrls.count];
for (NSUInteger i = 0; i < imageUrls.count; i++) {
NSString *_url = imageUrls[i];
[manager downloadImageWithURL:[NSURL URLWithString:_url] options:SDWebImageHighPriority progress:nil completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
if (image) {
[_allImagesOfThisArticle addObject:image];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSString *imgB64 = [UIImageJPEGRepresentation(image, 1.0) base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength];
//把圖片在磁盤中的地址傳回給JS
NSString *key = [manager cacheKeyForURL:imageURL];
NSString *source = [NSString stringWithFormat:@"data:image/png;base64,%@", imgB64];
//四種關系圖表之第4種
[_bridge callHandler:@"imagesDownloadComplete" data:@[key,source] responseCallback:^(id responseData) {
NSLog(@"js把img標簽的esrc屬性替換后-->%@<",responseData);
}];
});
}
}];
}
}
好了,到這里為止就webView就可以加載本地緩存的圖片了,如果要實現圖片的點擊放大,請接著往下看
5、imageCache.js里添加圖片點擊事件:
//圖片點擊會觸發
function onImageClick(picUrl){
connectWebViewJavascriptBridge(function(bridge) {
//作者用的是"p img[esrc]",意思是獲取p標簽里的img的src值
//我這里的圖片是div,所以要改成"div img[esrc]"
//var allImage = document.getElementsByTagName('img');//這樣比較通用
var allImage = document.querySelectorAll("div img[esrc]");
allImage = Array.prototype.slice.call(allImage, 0);
var urls = new Array();
var index = -1;
var x = 0;
var y = 0;
var width = 0;
var height = 0;
//獲取點擊圖片在所有圖片中的編號以及在圖片相對于webView左上角的位置、寬高,并把這些信息返回給OC
allImage.forEach(function(image) {
var imgUrl = image.getAttribute("esrc");
var newLength = urls.push(imgUrl);
if(imgUrl == picUrl || imgUrl == decodeURIComponent(picUrl)){
index = newLength-1;
x = image.getBoundingClientRect().left;
y = image.getBoundingClientRect().top;
x = x + document.documentElement.scrollLeft;
y = y + document.documentElement.scrollTop;
width = image.width;
height = image.height;
console.log("x:"+x +";y:" + y+";width:"+image.width +";height:"+image.height);
}
});
console.log("檢測到點擊"+"x="+x+"y="+y+"width="+width+"height="+height);
//四種關系圖表之第2種
bridge.callHandler('imageDidClicked', {'index':index,'x':x,'y':y,'width':width,'height':height}, function(response) {
console.log("JS已經發出imgurl和index,同時收到回調,說明OC已經收到數據");
});
});
}
6、viewDidLoad里注冊js圖片點擊事件回調,這里我用了一個簡單的圖片瀏覽器HZPhotoBrowser,修改了部分代碼使能夠適用于webView
//這里注冊一下,imageCache.js里的`bridge.callHandler('imageDidClicked', {'index':index,'x':x,'y':y,'width':width,'height':height}, function(response)`就會傳數據過來
[_bridge registerHandler:@"imageDidClicked" handler:^(id data, WVJBResponseCallback responseCallback) {
NSInteger index = [[data objectForKey:@"index"] integerValue];
CGFloat originX = [[data objectForKey:@"x"] floatValue];
CGFloat originY = [[data objectForKey:@"y"] floatValue];
CGFloat width = [[data objectForKey:@"width"] floatValue];
CGFloat height = [[data objectForKey:@"height"] floatValue];
//啟動圖片瀏覽器
HZPhotoBrowser *browserVc = [[HZPhotoBrowser alloc] init];
// browserVc.sourceImagesContainerView = cell.webView; // 原圖的父控件
browserVc.imageCount = _allImagesOfThisArticle.count; // 圖片總數
browserVc.currentImageIndex = index;
browserVc.delegate = self;
browserVc.imageFrameinWebView = CGRectMake(originX, originY+64, width, height);
[browserVc show];
NSLog(@"OC已經收到JS的imageDidClicked了: %@", data);
responseCallback(@"OC已經收到JS的imageDidClicked了");
}];
//四種關系圖表之第3種(測試)
// [_bridge send:@"###Message1:我將會被發送到imageCache.js里bridge.init()的回調里"];
//四種關系圖表之第3種(測試)
// [_bridge send:@"###Message1:我將會被發送到imageCache.js里bridge.init()的回調里,imageCache.js還會給我回調,不信你可能下面的Log" responseCallback:^(id responseData) {
// NSLog(@"###%@", responseData);
// }];
#pragma mark - HZPhotoBrowser的代理方法
//這里沒有占位小圖,所以就讓大圖代替
- (UIImage *)photoBrowser:(HZPhotoBrowser *)browser placeholderImageForIndex:(NSInteger)index {
return _allImagesOfThisArticle[index];
}
- (NSURL *)photoBrowser:(HZPhotoBrowser *)browser highQualityImageURLForIndex:(NSInteger)index {
return [NSURL URLWithString:_imageURLs[index]];
}
最終效果
好了,到此為止WebViewJavascriptBridge的基本用法已基本說完了,雖然很簡單,但是也花了我一天的時間,寫的同時又發現了不少新東西,還是很值的。這里是這個小Demo的源碼。渣渣代碼,就不上傳github了。
- 參考及推薦文章: