这是我参与更文挑战的第10天,活动详情查看: 更文挑战
前言
之前上天,我分别讲解了iOS原生、Flutter和H5的交互,今天来介绍的是通过第三方框架来进行iOS与H5的互调。
这个第三方框架来头不小,资历老,在UIWebView时代,我曾经就用过它进行OC与JS的互调工作。它就是WebViewJavascriptBridge
来,我上图让大家感受一下:
gitignore创建于8年前。
最后一次更新与2017年11月,离现在也有4、5年的时间了。
为何这个框架如此之久没有更新了仍然可用,我个人认为有以下两点原因:
-
1.WKWebView推出之后,苹果这边再也没有针对Web这块推出新的模块与组件,虽然随着iOS的版本迭代,WKWebView会增加一些新的Api,但是核心的iOS与H5的交互核心与业务没有改变。
-
2.WebViewJavascriptBridge是由Objective-C语言编写,这么语言最大的特色就是基本不会有什么语言更新,非常稳定。
基于以上两个原因,这就是WebViewJavascriptBridge依旧可用的原因。
WebViewJavascriptBridge的使用
本篇会附上通过修改WebViewJavascriptBridge官方提供的example.html,使得JS调起原生摄像头,原生获取图像数据后,主动调用JS方法,将图片数据传递给JS端的代码与例子。
先上JS这边的代码,重要的地方我会写注释,请仔细看,另外我询问过我的前端小伙伴,说这代码比较上古,大家主要看<scrpit>
里面的逻辑实现。
Html侧代码:
<!doctype html>
<html>
<head>
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1.0, maximum-scale=1.0">
<style type='text/css'>
html { font-family:Helvetica; color:#222; }
h1 { color:steelblue; font-size:24px; margin-top:24px; }
button { margin:0 3px 10px; font-size:12px; }
.logLine { border-bottom:1px solid #ccc; padding:4px 2px; font-family:courier; font-size:11px; }
</style>
</head>
<body>
<h1>WebViewJavascriptBridge Demo</h1>
<div id="images"></div>
<script>
window.onerror = function(err) {
log('window.onerror: ' + err)
}
function setupWebViewJavascriptBridge(callback) {
if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
window.WVJBCallbacks = [callback];
var WVJBIframe = document.createElement('iframe');
WVJBIframe.style.display = 'none';
WVJBIframe.src = 'https://__bridge_loaded__';
document.documentElement.appendChild(WVJBIframe);
setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}
setupWebViewJavascriptBridge(function(bridge) {
var uniqueId = 1
function log(message, data) {
var log = document.getElementById('log')
var el = document.createElement('div')
el.className = 'logLine'
el.innerHTML = uniqueId++ + '. ' + message + ':<br/>' + JSON.stringify(data)
if (log.children.length) { log.insertBefore(el, log.children[0]) }
else { log.appendChild(el) }
}
/// 这里是JS这边注册一个nativeImageData句柄
/// 用于原生调用该方法,主动将原生获取相机数据传递过来
bridge.registerHandler('nativeImageData', function(data, responseCallback) {
log('Swift 调用 JS nativeImageData with', data)
let imageString = data.imgData
var d1=document.getElementById("images")
var img=document.createElement("img")
img.src= "data:image/png;base64,"+imageString
d1.appendChild(img)
var responseData = { 'JS':'Get the Image' }
log('JS responding with', responseData)
responseCallback(responseData)
})
document.body.appendChild(document.createElement('br'))
var callbackButton = document.getElementById('buttons').appendChild(document.createElement('button'))
callbackButton.innerHTML = '调用原生相机'
callbackButton.onclick = function(e) {
e.preventDefault()
log('JS 调用 Swift')
/// 这里是JS调用一个叫jsCallNativeTakePhoto的句柄
/// 其目的是让在原生注册过jsCallNativeTakePhoto的位置,进行JS调用原生。
bridge.callHandler('jsCallNativeTakePhoto', {'message': 'take a photo'}, function(response) {
log('JS got response', response)
})
}
})
</script>
<div id='buttons'></div> <div id='log'></div>
</body>
</html>
iOS侧代码
import UIKit
import WebKit
import AVFoundation
import WebViewJavascriptBridge
/// 定义一个bridge,用于原生与H5侧互调
private var bridge: WebViewJavascriptBridge!
class BridgeWebViewController: UIViewController {
/// 懒加载webView
private lazy var webView: WKWebView = {
let config = WKWebViewConfiguration()
let preferences = WKPreferences()
preferences.javaScriptCanOpenWindowsAutomatically = true
config.preferences = preferences
let webView = WKWebView(frame: view.frame, configuration: config)
webView.allowsBackForwardNavigationGestures = true
webView.navigationDelegate = self
return webView
}()
override func viewDidLoad() {
super.viewDidLoad()
bridgeSetting()
setupUI()
}
}
extension BridgeWebViewController {
func setupUI() {
view.backgroundColor = .white
view.addSubview(webView)
/// 调用本地的html
let htmlPath = Bundle.main.path(forResource: "ExampleApp", ofType: "html")
let appHtml = try! String(contentsOfFile: htmlPath!, encoding: .utf8)
let baseURL = URL(fileURLWithPath: htmlPath!)
webView.loadHTMLString(appHtml, baseURL: baseURL)
}
/// bridge的配置
func bridgeSetting() {
if let _ = bridge {
return
}
WebViewJavascriptBridge.enableLogging()
/// 初始化bridge
bridge = WebViewJavascriptBridge(forWebView: webView)
/// 设置代理
bridge.setWebViewDelegate(self)
/// 通过bridge注册监听JS的句柄jsCallNativeTakePhoto
bridge.registerHandler("jsCallNativeTakePhoto") { [weak self] any, responseCallback in
/// 这里面写的就是监听到JS的方法后,在原生侧的方法实现
/// 这里我干了两件事:1.回调给JS侧一个字符串, 2.打开相机
print("jsCallNativeTakePhoto:\(any)")
responseCallback?("这里是从Swift回调给JS的字符串")
guard let self = self else { return }
/// 打开相机弹窗,这里是通过一个分类单独写的,后面放上。
self.showSystemImageAlertControll(delegate: self)
}
}
/// 通过bridge调用JS端的函数,将原生数据传递给
private func takeImageToJS(imageBase64String: String, type : Int) {
bridge.callHandler("nativeImageData", data: ["imgData": imageBase64String, "type": type])
}
}
/// UIImagePickerControllerDelegate回调图片数据
extension BridgeWebViewController: UIImagePickerControllerDelegate {
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
picker.dismiss(animated: true)
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
/// 图片转二进制
guard let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage,
let data = image.pngData() else {
return
}
/// 二进制转为base64字符串
let string = data.base64EncodedString()
/// 在picker消失的时候,将原生侧的数据传递给JS侧
picker.dismiss(animated: true) {
self.takeImageToJS(imageBase64String: string, type: 0)
}
}
}
extension BridgeWebViewController: UINavigationControllerDelegate {}
/// 监听WebView加载的生命周期,按需使用,这里只是写了这两个。
extension BridgeWebViewController: WKNavigationDelegate {
/// 页面开始加载时调用
///
/// - Parameters:
/// - webView: 实现该代理的webview
/// - navigation: 当前navigation
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
}
/// 页面加载完成之后调用
///
/// - Parameters:
/// - webView: 实现该代理的webview
/// - navigation: 当前navigation
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
}
}
详细的注释我都写在代码块里面了,使用这个框架,其实针对原生与JS都做了约束,原生和JS都有相同的两个方法registerHandler
和callHandler
,分别表示注册监听
与调用
,相当于在个各端保证Api的一致性,并行通过回调,可以将有没有收到另一侧的消息通过回调的形式返回过去。
WebViewJavascriptBridge没有什么在用法上没有什么太多需要说明的地方,原生侧与JS侧约定好方法名,入参形式,就可以进行调试了。
Android侧可以通过JsBridge与H5通信,用于iOS、Android、H5在方法层面上的大统一。
WebViewJavascriptBridge使用的注意事项:
上面已经说过,WebViewJavascriptBridge最后一次更新是2017年,而其中的代码还兼容了UIWebView,而目前UIWebView在iOS已经过期,如果使用,在上架时会被拒。
建议将WebViewJavascriptBridge中涉及UIWebView的部分删除后再进行集成。
这个框架也算是在Swift中调用OC代码的典型代表。
最后附上调用系统相机与相册的UIViewController分类,CV即用
import UIKit
import AVFoundation
/// 系统相册调用的协议
public typealias SystemImagePickerInterface = UIImagePickerControllerDelegate & UINavigationControllerDelegate
// MARK: - 系统相册的调用
extension UIViewController {
//MARK:- 显示alertController,相册选择,拍照,取消的方法
/// 显示alertController
///
/// - Parameter delegate: 需要设置代理的类
public func showSystemImageAlertControll(delegate: SystemImagePickerInterface) {
let imagePicker = UIImagePickerController()
imagePicker.navigationBar.barTintColor = navigationController?.navigationBar.barTintColor
imagePicker.navigationBar.tintColor = navigationController?.navigationBar.tintColor
imagePicker.navigationBar.titleTextAttributes = navigationController?.navigationBar.titleTextAttributes
imagePicker.delegate = delegate
imagePicker.allowsEditing = true
let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
// 拍照
let takePhotonAction = UIAlertAction(title: "拍照", style: .default) {
(action: UIAlertAction) in
self.takePhoto(imagePicker: imagePicker)
}
// 从相册中选择
let selectFromPhotoAlbumAction = UIAlertAction(title: "从相册中选择", style: .default) {
(action: UIAlertAction) in
self.selectFromPhotoAlbum(imagePiker: imagePicker)
}
// 取消
let cancelAction: UIAlertAction = UIAlertAction(title: "取消", style: .cancel, handler: nil)
alertController.addAction(takePhotonAction)
alertController.addAction(selectFromPhotoAlbumAction)
alertController.addAction(cancelAction)
present(alertController, animated: true, completion: nil)
}
//MARK:- 选取图片的具体方法
/// 拍照获取图片的方法
///
/// - Parameter imagePicker: UIImagePickerController
private func takePhoto(imagePicker: UIImagePickerController) {
imagePicker.sourceType = .camera
useFrontOrRearCamera(imagePiker: imagePicker)
let status: AVAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: AVMediaType.video)
switch status {
case .authorized:
present(imagePicker, animated: true, completion: nil)
case .denied:
break
case .restricted:
break
case .notDetermined:
present(imagePicker, animated: true, completion: nil)
@unknown default:
break
}
}
/// 从相册获取图片的方法
///
/// - Parameter imagePiker: UIImagePickerController
private func selectFromPhotoAlbum(imagePiker: UIImagePickerController) {
imagePiker.sourceType = .photoLibrary
present(imagePiker, animated: true, completion: nil)
}
/// 使用前置或者后置摄像头的方法,通过调整判断前置与后置的顺序可以改变调用的优先级
///
/// - Parameter imagePiker: UIImagePickerController
private func useFrontOrRearCamera(imagePiker: UIImagePickerController) {
if UIImagePickerController.isCameraDeviceAvailable(.rear) {
imagePiker.cameraDevice = .rear
}else if UIImagePickerController.isCameraDeviceAvailable(.front) {
imagePiker.cameraDevice = .front
}else {
return
}
}
}
明日继续
转眼明天就是周五,马上就要端午节了,本周因为工作原因,主要介绍了Swift与H5交互,下一步主要介绍网络请求模块,也就是Alamofire、Moya、Codable。
在介绍这一块之前,可能会先讲解一下枚举和其他小功能。
大家加油!