iOS WKWebView交互实现及iOS13以上, iOS11以下适配

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

笔者性懒,腹中无墨.
以下简单列出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的uiDelegatenavigationDelegate最好也在这里指定, 防止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
        })
    }
本demo是WKWebView的基本使用和交互实现了原生调用js的方法、js调用原生的方法、通过拦截进行交互的方法;修改内容 加入沙盒 / /加载沙盒 不带参数 // NSArray * paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); // NSString * path = [paths objectAtIndex:0]; // path = [path stringByAppendingString:[NSString stringWithFormat:@"/app/html/index.html"]]; // NSURL *url = [NSURL URLWithString:[[NSString stringWithFormat:@"file://%@",path] stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLFragmentAllowedCharacterSet]] relativeToURL:[NSURL fileURLWithPath:NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject]]; // [self.wkView loadFileURL:url allowingReadAccessToURL:[NSURL fileURLWithPath: [paths objectAtIndex:0]]]; // 带参数 /* NSArray * paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString * path = [paths objectAtIndex:0]; path = [path stringByAppendingString:[NSString stringWithFormat:@"/app/html/index.html"]]; NSURL * url = [NSURL fileURLWithPath:path isDirectory:NO]; NSURLComponents *urlComponents = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO]; [queryItemArray addObject:[NSURLQueryItem queryItemWithName:@"version" value:[[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"]]]; [urlComponents setQueryItems:queryItemArray]; [self.wkView loadFileURL:urlComponents.URL allowingReadAccessToURL:[NSURL fileURLWithPath: [paths objectAtIndex:0]]]; */
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值