// // H5BaseViewController.swift // E-Wow // // Created by dong on 2021/1/5. // // import Alamofire // import CryptoSwift import UIKit import URLNavigator import WebKit class H5BaseViewController: CLBaseViewController { public var navTitleShow: Bool = true var targetUrl: URL! = URL(string: "") var lastProgress: Double! = 0 private var observeRegister: Bool = false private let schemes = ["route", "closeWebview", "getUserInfo", "setLoading", "request", "modal", "share", "init", "openBrowser", "getAppVersion", "sendTrack"] lazy var webView: WKWebView! = { let config = WKWebViewConfiguration() let preferences = WKPreferences() preferences.javaScriptCanOpenWindowsAutomatically = true config.preferences = preferences config.allowsInlineMediaPlayback = true config.mediaTypesRequiringUserActionForPlayback = .video let webView = WKWebView(frame: CGRect(x: 0, y: 0, width: 320, height: 480), configuration: config) // 以下3行修改webView颜色是有效的✅ webView.backgroundColor = .c.cbd webView.scrollView.backgroundColor = .c.cbd webView.isOpaque = false let jsFilePath = Bundle.main.path(forResource: "webview", ofType: "js") if let jshtml = try? String(contentsOfFile: jsFilePath!, encoding: .utf8) { let script = WKUserScript(source: jshtml, injectionTime: .atDocumentStart, forMainFrameOnly: true) webView.configuration.userContentController.addUserScript(script) } return webView }() lazy var progressView: UIProgressView = { let progressView = UIProgressView(frame: .zero) progressView.progress = 0 progressView.setProgress(0, animated: false) navigationView.addSubview(progressView) progressView.snp.makeConstraints { make in make.left.right.bottom.equalToSuperview() make.height.equalTo(1) } progressView.progressTintColor = .c.cpn//.purple progressView.trackTintColor = .clear return progressView }() lazy var closeButton: UIButton = { let button = UIButton(type: .custom) button.setImage(R.image.icon_close_20(), for: .normal) button.addTarget(self, action: #selector(tapNaviCloseBtn), for: .touchUpInside) button.isHidden = true navigationView.leftStackH.addArrangedSubview(button) button.snp.makeConstraints { make in make.size.equalTo(CGSize(width: 44, height: 44)) } return button }() override func viewDidLoad() { super.viewDidLoad() // view.backgroundColor = .white // webView.backgroundColor = .white webView.backgroundColor = .c.cbd webView.navigationDelegate = self view.addSubview(webView) webView.snp.makeConstraints { make in make.bottom.right.left.equalTo(self.view) make.top.equalTo(navigationView.snp.bottom) } navigationView.backButton.removeTarget(navigationView, action: .none, for: .touchUpInside) navigationView.backButton.addTarget(self, action: #selector(tapNaviBackBtn), for: .touchUpInside) setupBaseEvents() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) for (_, per) in schemes.enumerated() { webView.configuration.userContentController.add(self, name: per) } } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) for (_, per) in schemes.enumerated() { webView.configuration.userContentController.removeScriptMessageHandler(forName: per) } } public func loadURL(url: URL) { /* if let lan = Languages.preferedLans.first { let queryItem = URLQueryItem(name: "lang", value: lan.rawValue) if var compoments = URLComponents(url: url, resolvingAgainstBaseURL: true){ if let items = compoments.queryItems, items.count > 0{ compoments.queryItems?.append(queryItem) }else{ compoments.queryItems = [queryItem] } let afterUrl = compoments.url targetUrl = afterUrl } dlog("h5 path with language: \(String(describing: targetUrl))") } else { targetUrl = url } */ targetUrl = url } func setupBaseEvents() { weak var wself = self DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { guard let url = self.targetUrl else { assert(false) return } let request = NSMutableURLRequest(url: url) wself?.webView.load(request as URLRequest) } webView.addObserver(self, forKeyPath: "estimatedProgress", options: .new, context: nil) webView.addObserver(self, forKeyPath: "title", options: .new, context: nil) observeRegister = true progressView.setProgress(0, animated: false) } deinit { webView.configuration.userContentController.removeAllUserScripts() if observeRegister { webView.removeObserver(self, forKeyPath: "title") webView.removeObserver(self, forKeyPath: "estimatedProgress") } } func handleInit(msg: JSSDKMessage) { // wait to override the func } } extension H5BaseViewController { @objc func tapNaviBackBtn() { if webView.canGoBack { webView.goBack() } else { close() } } @objc func tapNaviCloseBtn() { close() } } // MARK: - Helper extension H5BaseViewController { func stopHtmlVoice() { // ... let jsaudio = "var vids = document.getElementsByTagName('audio'); for( var i = 0; i < vids.length; i++ ){vids.item(i).pause()}" webView.evaluateJavaScript(jsaudio, completionHandler: nil) } static func clearCache() { // let dataStore = WKWebsiteDataStore.default() // dataStore.fetchDataRecords(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), completionHandler: { records in // for record in records { // WKWebsiteDataStore.default().removeData(ofTypes: record.dataTypes, for: [record], completionHandler: { // print("♻️✅ Webview cache clear successfully\(record)") // }) // } // }) let websiteDataTypes: Set = [ WKWebsiteDataTypeDiskCache, WKWebsiteDataTypeMemoryCache, WKWebsiteDataTypeLocalStorage, WKWebsiteDataTypeWebSQLDatabases, WKWebsiteDataTypeIndexedDBDatabases ] // 从 1970 开始,意味着清除所有历史数据 let dateFrom = Date(timeIntervalSince1970: 0) WKWebsiteDataStore.default().removeData(ofTypes: websiteDataTypes, modifiedSince: dateFrom) { print("WebView data cleared") } } override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { if keyPath == "title" { if navTitleShow { navigationView.titleLabel.text = webView.title } } else if keyPath == "estimatedProgress" { updateProgress(progress: webView.estimatedProgress) } else { } } func updateProgress(progress: Double) { progressView.alpha = 1 // dlog("progress : \(progress)") if progress > lastProgress { progressView.setProgress(Float(progress), animated: true) } else { progressView.setProgress(Float(progress), animated: false) } lastProgress = progress if progress >= 1 { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in self?.progressView.alpha = 0 self?.progressView.setProgress(0, animated: false) self?.lastProgress = 0 } } } } // MARK: - Handle Event extension H5BaseViewController { func handleRoute(msg: JSSDKMessage) { if let uri = msg.uri?.urlValue { navigator.open(uri) } } func handleRequest(msg: JSSDKMessage) { // let requestUri = msg.uri // let params = msg.params // for h5自主调用,静默请求...成功后执行成功回调,失败后执行error方法。 guard let uri = msg.uri else { return } var requestAny: AnyCodable = AnyCodable(value: [:]) let token = UserCore.shared.token if !token.isEmpty { do { // let body = try JSONSerialization.data(withJSONObject: params, options: .prettyPrinted) // let str = String(data: body, encoding: .utf8) var str = "" if let dict = msg.params, let data2: Data = try? JSONSerialization.data(withJSONObject: dict, options: []) { let objcAnyCodable = try! JSONDecoder().decode(AnyCodable.self, from: data2) let backToJson = try! JSONEncoder().encode(objcAnyCodable) let jsonString = String(bytes: backToJson, encoding: .utf8)! str = jsonString } // let key = (token + "AHkt5aUUtO6HZPid").md5().uppercased() // let aes = try AES(key: key, iv: "HBB4UO5kEmM4169Z") // let encrypted = try aes.encrypt(str.bytes) // let result = encrypted.toBase64() // let dic = ["key": result] // dlog("⚠️ 加密前参数:\(str) \n⚠️ 加密结果:\(dic)") // if let data3: Data = try? JSONSerialization.data(withJSONObject: dic, options: []) { // let objcAnyCodable = try! JSONDecoder().decode(AnyCodable.self, from: data3) // requestAny = objcAnyCodable // } } catch { } } else { if let dict = msg.params, let data2: Data = try? JSONSerialization.data(withJSONObject: dict, options: []), let objcAnyCodable = try? JSONDecoder().decode(AnyCodable.self, from: data2) { requestAny = objcAnyCodable } } // let headers = HTTPHeaders(APIConfig.apiHeaders()!) Hud.showIndicator() dlog("☁️h5 request:\(uri) params: \(requestAny)") // AF.request(uri, method: .post, parameters: requestAny, encoder: JSONParameterEncoder.default, headers: headers, interceptor: nil, requestModifier: nil).responseString { [weak self] response in // // dlog("response: \(response)") // self?.view.hideToastActivity() // switch response.result { // case let .success(model): // guard let ltResponse: ResponseData = ResponseData>.deserialize(from: model) else { // return // } // // if ltResponse.status == "OK" { // if let ltArrayResponse = ResponseData>>.deserialize(from: model), let jsonDict = ltArrayResponse.content { // if let data = try? JSONSerialization.data(withJSONObject: jsonDict, options: .prettyPrinted), let str = String(data: data, encoding: .utf8) { // let jsonString = str // content2.toJSONString() ?? "" // let js = "\(msg.success ?? "")((\(jsonString)))" // dlog("✅ success call js:\(js)") // self?.webView.evaluateJavaScript(js, completionHandler: { _, error in // if error != nil { // dlog("❌ exec js error: \(error?.localizedDescription ?? "")") // } // }) // return // } // // } else if let content = ltResponse.content { // let jsonString = content.toJSONString() // let js = "\(msg.success ?? "")((\(jsonString ?? "")))" // dlog("✅ success call js:\(js)") // self?.webView.evaluateJavaScript(js, completionHandler: { _, error in // if error != nil { // dlog("❌ exec js error: \(error?.localizedDescription ?? "")") // } // }) // return // } // // let js = "\(msg.success ?? "")()" // dlog("✅ success call js no content:\(js)") // self?.webView.evaluateJavaScript(js, completionHandler: { _, error in // if error != nil { // dlog("❌ exec js error: \(error?.localizedDescription ?? "")") // } // }) // } else { // // --- 接口错误 // let js = "\(msg.error ?? "")((\(model)))" // dlog("❌ api error: \(js)") // self?.webView.evaluateJavaScript(js, completionHandler: nil) // } // // break // default: // // --- 网络等错误 // UIWindow.key?.makeToast(R.string.localizable.internet_connect_failed.localized()) // let js = "\(msg.error ?? "")()" // dlog("❌ api network error: \(js)") // self?.webView.evaluateJavaScript(js, completionHandler: nil) // break // } // } } func handleLoading(msg: JSSDKMessage) { if msg.status { UIWindow.key?.makeToastActivity() } else { UIWindow.key?.hideToastActivity() } } func handleGetUserInfo(msg: JSSDKMessage) { } func handleModal(msg: JSSDKMessage) { // 取决于msg.type. 暂无 } func handleOpenBrowser(msg: JSSDKMessage) { if let uri = msg.uri, uri.count > 0, let url = URL(string: uri) { UIApplication.shared.open(url, options: [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly: false], completionHandler: nil) } } func handleGetAppversion(msg: JSSDKMessage) { let version = Bundle.appVersion let dict = ["version": version] let data1 = try? JSONSerialization.data(withJSONObject: dict, options: []) let dictJs = String(data: data1!, encoding: .utf8) ?? "" // dict.toJSONString() ?? "" let js = "\(msg.success ?? "")((\(dictJs)))" dlog("✅ success call js:\(js)") webView.evaluateJavaScript(js, completionHandler: { _, error in if error != nil { dlog("❌ exec js error: \(error?.localizedDescription ?? "")") } }) } private func handleDealSendTrack(msg: JSSDKMessage) { if let trackName = msg.name { var params = msg.params ?? [:] if let uid = UserCore.shared.user?.userId { // params.updateValue(uid, forKey: "userId") } // AppAnalytics.commonRecord(trackName, parameters: params) } } } // MARK: - 🔥 WKScriptMessageHandler extension H5BaseViewController: WKScriptMessageHandler { func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { dlog("☁️☁️ h5 message name: \(message.name) message body: \(message.body)") let msgName = message.name guard let body = message.body as? String else { // elog("h5 message failed! : \(msgName)") return } guard let msg = CodableHelper.decode(JSSDKMessage.self, from: body) else { return } if msgName == "route" { msg.msgName = msgName handleRoute(msg: msg) } else if msgName == "request" { msg.msgName = msgName handleRequest(msg: msg) } else if msgName == "setLoading" { msg.msgName = msgName handleLoading(msg: msg) } else if msgName == "closeWebview" { close() } else if msgName == "getUserInfo" { msg.msgName = msgName handleGetUserInfo(msg: msg) } else if msgName == "modal" { msg.msgName = msgName handleModal(msg: msg) } else if msgName == "share" { // to do. } else if msgName == "init" { handleInit(msg: msg) } else if msgName == "openBrowser" { msg.msgName = msgName handleOpenBrowser(msg: msg) } else if msgName == "getAppVersion" { msg.msgName = msgName handleGetAppversion(msg: msg) } else if msgName == "sendTrack" { msg.msgName = msgName handleDealSendTrack(msg: msg) } else { // Please upgrade to the latest version dlog("🛎Please upgrade to the latest version,Unsupported protocol.") } } } // MARK: - WKNavigationDelegate` extension H5BaseViewController: WKNavigationDelegate { func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { guard let urlRequest = navigationAction.request.url?.absoluteString.removingPercentEncoding else { decisionHandler(.cancel) return } dlog("☁️ webview load: \(urlRequest) ☁️") if urlRequest.hasPrefix(AppConst.schemePrefix) { navigator.open(urlRequest) } else if urlRequest.hasPrefix("mailto:") { // open system to send email. if let url = URL(string: urlRequest) { UIApplication.shared.open(url, options: [:], completionHandler: nil) } } decisionHandler(.allow) } func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { dlog("didFinish 🔥🔥🔥\(String(describing: webView.url))") closeButton.isHidden = !webView.canGoBack } }