490 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Swift
		
	
	
	
		
		
			
		
	
	
			490 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Swift
		
	
	
	
|  | // | |||
|  | //  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<String> = [ | |||
|  |             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<Dictionary<String, Any>>.deserialize(from: model) else { | |||
|  | //                    return | |||
|  | //                } | |||
|  | // | |||
|  | //                if ltResponse.status == "OK" { | |||
|  | //                    if let ltArrayResponse = ResponseData<Array<Dictionary<String, Any>>>.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 | |||
|  |     } | |||
|  | } |