笔者性懒,腹中无墨.
以下简单列出WKWebView的使用及注意事项.
1. 初始化webview, 一般设置如下, 支持javaScript交互.
fileprivate lazy var webView: WKWebView = {
let webConfiguration = WKWebViewConfiguration()
webConfiguration.preferences = WKPreferences()
webConfiguration.preferences.minimumFontSize = 0
webConfiguration.preferences.javaScriptEnabled = true
webConfiguration.processPool = WKProcessPool()
webConfiguration.preferences.javaScriptCanOpenWindowsAutomatically = true
let webView = WKWebView(frame: .zero, configuration: webConfiguration)
webView.backgroundColor = .white
webView.isOpaque = false
webView.uiDelegate = self
webView.navigationDelegate = self
webView.allowsBackForwardNavigationGestures = true
return webView
}()
2. 添加视图
添加视图在viewDidLoad()里, webview的uiDelegate和navigationDelegate最好也在这里指定, 防止App还未加载完该页面, 因某种原因或需求调用deinit()方法时, 找不到代理而出错.
//防止App还未加载完该页面, 因某种原因或需求调用deinit()方法时出错.
webView.uiDelegate = self
webView.navigationDelegate = self
view.addSubview(webView)
webView.snp.makeConstraints { (make) in
make.edges.equalTo(UIEdgeInsets.zero)
}
webView.addSubview(topBlueView)
3. 新建WKScriptMessageHandler协议类
支持js交互的话, webview需要实现WKScriptMessageHandler协议, 它提供了从网页中运行的JavaScript接收消息的方法.
这里我们把这个协议的实现独立出来, 新建一个WKScriptMessageDelegate类, 交由代理实现上述协议, 注意代理的弱引用.
import UIKit
import WebKit
class WKScriptMessageDelegate: NSObject {
//这里建议使用弱引用, 以免造成循环引用问题
weak var scriptDelegate: WKScriptMessageHandler?
init(delegate: WKScriptMessageHandler?) {
scriptDelegate = delegate
}
deinit {
print("WeakScriptMessageDelegate is deinit")
}
}
extension WKScriptMessageDelegate: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
scriptDelegate?.userContentController(userContentController, didReceive: message)
}
}
4. 控制器懒加载WKScriptMessageDelegate
lazy var wkDelegate: WKScriptMessageDelegate = {
let d = WKScriptMessageDelegate(delegate: self)
return d
}()
5. 加载URL, 注意缓存问题, 根据项目需求修改
///加载url
private func loadUrl() {
guard let webUrl = webUrl, let url = URL(string: webUrl) else {
return
}
// MARK: - 未设置则使用默认缓存规则, 即: 若请求协议头保持为no-cache, 表现为直接从后台请求数据, 则不需修改.
webView.load(URLRequest(url: url))
/**
case useProtocolCachePolicy
case reloadIgnoringLocalCacheData
case reloadIgnoringLocalAndRemoteCacheData
case returnCacheDataElseLoad
case returnCacheDataDontLoad
case reloadRevalidatingCacheData
*/
}
6. 对于 js调用native :
由js触发, 先约定好方法名, 原生需要先注册该方法.
第一步: 原生注册及移除方法
可直接添加添加交互事件, 也可根据当前webview的类型添加.
///web类型与事件监听
private func configWeb() {
//调用此webview都会添加unLogin方法.
webView.configuration.userContentController.add(wkDelegate, name: FunctionName.unLogin.rawValue)
switch webType {
//仅当webview类型为contract时, 会添加的方法
case .contract:
webView.configuration.userContentController.add(wkDelegate, name: FunctionName.uploadClientImage.rawValue)
default:
break
}
}
//移除所有事件
deinit {
webView.configuration.userContentController.removeScriptMessageHandler(forName: FunctionName.unLogin.rawValue)
webView.configuration.userContentController.removeScriptMessageHandler(forName: FunctionName.uploadClientImage.rawValue)
}
第二步: 实现协议方法
在WKScriptMessageHandler协议的代理方法userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage)中实现对应的原生方法
extension H5VC: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
switch message.name {
case FunctionName.unLogin.rawValue:
///调用原生被登出方法
callNativeLogin()
case FunctionName.uploadClientImage.rawValue:
guard let msg: String = message.body as? String else {
return
}
///调用原生上传图片方法
callNativeCamera(msg)
default: break
}
}
}
7. 对于native调用js :
由原生触发, 在触发方法里调用js的方法
/**
调用js方法传图片
*/
func callJSUploadImage(_ image: UIImage) {
let imageData = image.jpegData(compressionQuality: 0.3)
guard let imageBase64String = imageData?.base64EncodedString().replacingOccurrences(of: "\n", with: "") else {
return
}
//必须要有双引号
let method = "\(FunctionName.getClientImage.rawValue)('\(imageBase64String)')"
webView.evaluateJavaScript(method) { (obj, error) in
if let myError = error {
print("传图片-JS方法调用失败: \(myError)")
} else {
print("传图片-JS方法调用成功")
}
}
}
这里用到的FuntionName其实是一个方法名的枚举
/**
交互方法
*/
enum FunctionName: String {
/**
js调native
*/
case unLogin //被登出
case uploadClientImage //获取本地图片
/**
native调js
*/
case getClientImage //上传图片
}
8. 几个常用的代理方法的实现 :
这里对于WKUIDelegate及WKNavigationDelegate的实现不做赘述, 根据需求时间代理方法即可.
extension H5VC: WKUIDelegate {
///处理部分网页上链接,点击无效问题
internal func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
if navigationAction.targetFrame?.isMainFrame ?? true {
webView.load(navigationAction.request)
}
return nil
}
}
extension H5VC: WKNavigationDelegate {
/// 在发送请求之前,决定是否跳转
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
if let url = navigationAction.request.url, let scheme = url.scheme {
/**
微信网页支付 取消支付(特例)
*/
if url.relativePath.contains("pay/return/wechat") { // "/apis/tiger/pay/return/wechat"
navigationController?.popViewController(animated: true)
decisionHandler(.cancel)
return
}
/**
网页下载支付宝App (特例)
*/
if scheme.contains("itms-appss"), UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
decisionHandler(.cancel)
return
}
/**
网页内拨打电话
*/
if scheme.contains("tel") {
//调用拨打电话方法
DRTool().callPhone(url)
decisionHandler(.cancel)
return
}
/**
网页拉起微信或支付宝支付
*/
if scheme.contains("alipay") || scheme.contains("weixin"), UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
decisionHandler(.cancel)
return
}
decisionHandler(.allow)
} else {
decisionHandler(.allow)
}
}
/// 需要认证时调用
func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, challenge.previousFailureCount == 0 {
guard let trust = challenge.protectionSpace.serverTrust else {
return
}
let card = URLCredential(trust: trust)
/**
case useCredential // 使用服务器发回的凭据,可能为空
case performDefaultHandling // 默认的处理方法,如果未执行此代理,则忽略凭据参数
case cancelAuthenticationChallenge //取消整个请求,忽略凭据参数
case rejectProtectionSpace // 这次质询被拒绝,尝试下一个身份验证保护控件 ,忽略凭据参数
*/
completionHandler(URLSession.AuthChallengeDisposition.useCredential, card)
} else {
completionHandler(URLSession.AuthChallengeDisposition.cancelAuthenticationChallenge, nil)
}
}
}
以上即为WKWebView与js交互的几个重要的点, 以下为需求补充
9. 设置useragent :
webview一般会添加分享功能, 有时, 为了区分是通过App内分享到微信打开的页面还是别的渠道打开的页面, App内我们会给webview的useragent添加后缀, 后缀一般用你的项目名.
///设置useragent
private func addUserAgent() {
webView.evaluateJavaScript("navigator.userAgent") { [weak self] (result, error) in
guard let userAgent: String = result as? String else {
return
}
if !userAgent.hasSuffix("你的项目名") {
let newAgent = userAgent.appending("你的项目名")
let dic = ["UserAgent": newAgent]
//"App内所有Web请求的User-Agent全部被修改"
UserDefaults.standard.register(defaults: dic)
UserDefaults.standard.synchronize()
/**
iOS9之后 设置customUserAgent则App内其他H5页面不会修改
*/
self?.webView.customUserAgent = newAgent
}
}
}
10. 适配问题
底部留白问题, webview不能撑满全屏, 底部一个白色的高度, 其中judgeiPhoneX()是判断是否是刘海屏手机的方法.
/**
刘海屏&iOS13, 底部留白问题处理
*/
if #available(iOS 13.0, *), judgeiPhoneX() {
webView.scrollView.contentInsetAdjustmentBehavior = .never
}
顶部留白问题, 有时, 嵌入的外部的URL, 顶部会出现一条窄的等屏宽的白条, H5无人处理时需要原生处理. 比如加一个颜色与其导航栏色值相同的view, 以下以topBlueView为例
topBlueView.frame.size.height = UIApplication.shared.statusBarFrame.height
view.addSubview(webView)
if #available(iOS 11.0, *) {
webView.snp.makeConstraints { (make) in
make.edges.equalTo(UIEdgeInsets.zero)
}
/**
iOS11以上 webview可直接加蓝色头
*/
webView.addSubview(topBlueView)
} else {
webView.frame = CGRect(x: 0, y: topBlueView.frame.size.height, width: kScreenWidth, height: kScreenHeight-topBlueView.frame.size.height-64)
/**
iOS11以下 需在view上加蓝色头
*/
view.addSubview(topBlueView)
}
横屏问题, 有视频的网页可在点击播放时, 直接横屏播放
///横屏
private func appObserve() {
NotificationCenter.default.addObserver(self, selector: #selector(deviceDidRotate), name: UIDevice.orientationDidChangeNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(toLandscapeRight), name: UIWindow.didBecomeVisibleNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(toPortrait), name: UIWindow.didBecomeHiddenNotification, object: nil)
}
@objc private func deviceDidRotate() {
if responds(to: #selector(setNeedsStatusBarAppearanceUpdate)) {
perform(#selector(setNeedsStatusBarAppearanceUpdate))
}
}
@objc private func toLandscapeRight() {
// Trick: ViewController 生命周期,第一次设置转横屏,强制设置两次(不然第一次设置不成功)
if allowTime > 0 {
allowTime -= 1
UIDevice.current.setValue(value, forKey: "orientation")
let value2 = UIInterfaceOrientation.portrait.rawValue
UIDevice.current.setValue(value2, forKey: "orientation")
}
UIDevice.current.setValue(value, forKey: "orientation")
}
@objc fileprivate func toPortrait() {
let value = UIInterfaceOrientation.portrait.rawValue
UIDevice.current.setValue(value, forKey: "orientation")
}
webview监听问题, 加载进度, 返回及title监听.
//加载进度监听
var progressOB: NSKeyValueObservation?
//标题监听
var titleOB: NSKeyValueObservation?
//返回监听
var backOB: NSKeyValueObservation?
private lazy var actView: UIActivityIndicatorView = {
let act = UIActivityIndicatorView()
act.hidesWhenStopped = true
act.isUserInteractionEnabled = false
act.backgroundColor = UIColor(white: 0.5, alpha: 0.1 )
act.color = .darkGray
return act
}()
private lazy var progressView: UIProgressView = {
let pro = UIProgressView()
return pro
}()
/// 监听
private func eventMonitor() {
//加载进度
progressOB = webView.observe(\.estimatedProgress, options: [.old, .new]) { [weak self] object, change in
guard let value1: NSNumber = change.newValue as NSNumber? else {
return
}
let value: Float = Float(truncating: value1)
if value == 1.0 {
//加载完全, UI处理
self?.progressView.isHidden = true
self?.actView.stopAnimating()
} else {
//加载不完全, UI处理
self?.progressView.isHidden = false
self?.progressView.progress = value
self?.actView.startAnimating()
}
}
// 关闭按钮监听
backOB = webView.observe(\.canGoBack, options: [.new, .old], changeHandler: {[weak self] (ob, change) in
guard let value: Bool = change.newValue as Bool? else {
return
}
guard let backBtn = self?.backBtn, let closeBtn = self?.closeBtn else {
return
}
if value {
//一个返回一个关闭按钮
self?.navigationItem.leftBarButtonItems = [UIBarButtonItem(customView: backBtn), UIBarButtonItem(customView: closeBtn)]
} else {
//仅返回按钮
self?.navigationItem.leftBarButtonItems = [UIBarButtonItem(customView: backBtn)]
}
})
// 标题监听
titleOB = webView.observe(\.title, options: [.old, .new], changeHandler: { [weak self] (ob, change) in
guard let value = change.newValue as? String else {
return
}
self?.title = value
})
}
iOS WKWebView交互实现与适配策略

本文介绍了iOS中WKWebView的初始化、添加视图、创建WKScriptMessageHandler协议类、处理JavaScript与原生交互、设置UserAgent以及适配问题的详细步骤。通过这些方法,可以实现WKWebView与JavaScript的有效通信,并解决不同iOS版本的适配问题。
440

被折叠的 条评论
为什么被折叠?



