// // TerminalStatisticsH5ViewController.swift // GXM-CRM // // Created by jsonmess on 2017/11/22. // Copyright © 2017年 DemoOrg. All rights reserved. // import SnapKit import UIKit @preconcurrency import WebKit import MJRefresh import SwiftyJSON import Kingfisher import KingfisherWebP internal import Alamofire import SwiftyUserDefaults import RxSwift import RxCocoa // 页面加载状态 enum LoadingStatus: Int { // 初始状态 case initial = 0 // 加载中.. case inLoading = 1 // 加载完成 case success = 2 // 加载失败 case faild = 3 } enum ScriptMessageName: String, CaseIterable { case updateStatusBarStyle case openPageRouter case openPage case openWebView case buyInquiryItem case openIM case toast case dialog case showBigImage case showCamera case showAlbum case showDatePicker case backConfirm case closeWebView } enum WebViewError: Error { case downloadImage } extension WebViewError: LocalizedError { var errorDescription: String? { switch self { case .downloadImage: return "图片下载失败" } } } class FullscreenWebView: WKWebView { @available(iOS 11.0, *) override var safeAreaInsets: UIEdgeInsets { return .zero } } // sourcery: router="web", name="Web页面" @objcMembers class WebViewController: BaseViewController {//}, InitRoutable { private static let uniqueProcessPool = WKProcessPool() override var isNavigationBarHidden: Bool { // guard let fullscreen = fullscreen, fullscreen else { // return false // } return fullscreen } override var preferredStatusBarStyle: UIStatusBarStyle { guard let style = style, style == "default" else { return .lightContent } return .default } override var isChangeNavigationBarStyle: Bool { guard let style = style, style == "lightContent" else { return true } return false } // sourcery: parameter private var url: String // sourcery: parameter private var isShare: Bool? // sourcery: parameter private var fullscreen: Bool = false // sourcery: parameter private var style: String? // private var flag: String = "" // private var popConfirmTitle: String = "" private var popConfirmContent: String = "" // private var popType: Int = 0 // 0: 不需要二次确认 1: 返回原生 2: 返回h5上一页 private var operation = WebOperations() private let commonMessageHandler = CommonMessageHandler() private let commonMessageNames: [ScriptMessageName] = ScriptMessageName.allCases private var webViewImageCache = ImageCache(name: "WebView") var webView: WKWebView! private var progressView: UIProgressView! private let downloadImageMaxSize = 1024 var canGoBack = true var isHome = false var isMounted = false var hasPullRefresh = true var isH5Pay = false typealias MessageHandler = (name: String, handler: WKScriptMessageHandler) var messageHandlers: [MessageHandler] = [] init(url: String, isShare: Bool?, fullscreen: Bool?, style: String?) { self.url = url self.isShare = isShare self.fullscreen = fullscreen ?? false self.style = style super.init(nibName: nil, bundle: nil) } init(url: String) { self.url = url super.init(nibName: nil, bundle: nil) } init(url: String, style: String) { self.url = url self.style = style super.init(nibName: nil, bundle: nil) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } @MainActor deinit { guard webView != nil else { return } let userContentController = webView.configuration.userContentController let messageNames = commonMessageNames.map { $0.rawValue } messageNames.forEach { userContentController.removeScriptMessageHandler(forName: $0) } messageHandlers.forEach { userContentController.removeScriptMessageHandler(forName: $0.name) } webView = nil // let dateFrom: NSDate = NSDate.init(timeIntervalSince1970: 0) // let websiteDataTypes = WKWebsiteDataStore.allWebsiteDataTypes() // WKWebsiteDataStore.default().removeData(ofTypes: websiteDataTypes , modifiedSince: dateFrom as Date) { // print("清空缓存完成") // } } override func viewDidLoad() { super.viewDidLoad() fd_interactivePopDisabled = true if style == "default" { // setupLeftBarButtonItem(iconColor: "gray") } // setupErrorView() setupView() addObservers() reload() bindAction() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) // setupNavigationBar(titleColor: style=="default" ? Color.rgb0E0E0E : .white, // barTinColor: style=="default" ? .white : Color.mainColor) } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) // setupNavigationBar(titleColor: .white, barTinColor: Color.mainColor) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) // MTAHybrid.restart(webView) onPageShow() } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) // MTAHybrid.stop(webView) } @objc func videoDidRotate() { self.setNeedsStatusBarAppearanceUpdate() } override var prefersStatusBarHidden: Bool { return UIApplication.shared.statusBarOrientation.isLandscape } private func setupView() { let configuration = WKWebViewConfiguration() let userContentController = WKUserContentController() commonMessageHandler.webViewController = self let messageNames = commonMessageNames.map { $0.rawValue } messageNames.forEach { userContentController.add(commonMessageHandler, name: $0) } messageHandlers.forEach { userContentController.add($0.handler, name: $0.name) } configuration.processPool = WebViewController.uniqueProcessPool configuration.userContentController = userContentController configuration.applicationNameForUserAgent = configuration.applicationNameForUserAgent?.appending("/wlwm") // 访问文件路径 configuration.preferences.setValue(true, forKey: "allowFileAccessFromFileURLs") // 配置相册访问权限 configuration.allowsInlineMediaPlayback = true configuration.mediaTypesRequiringUserActionForPlayback = .all configuration.allowsPictureInPictureMediaPlayback = true let mWebView = FullscreenWebView(frame: CGRect.zero, configuration: configuration) webView = mWebView mWebView.uiDelegate = self mWebView.navigationDelegate = self mWebView.allowsBackForwardNavigationGestures = true // mWebView.customUserAgent = userAgent?.appending("/osell") if #available(iOS 16.4, *) { mWebView.isInspectable = true } view.addSubview(mWebView) // mWebView.snp.makeConstraints { make in // make.edges.equalToSuperview() // } mWebView.layoutChain .top(fullscreen ? 0 : kNaviHeight) .edgesHorzontal() .bottom() // .bottom(kSafeBottomMargin) if #available(iOS 11.0, *) { mWebView.scrollView.contentInsetAdjustmentBehavior = .automatic } // ProgressView let mProgressView = UIProgressView(frame: CGRect.zero) progressView = mProgressView mProgressView.progress = 0.0 mProgressView.progressTintColor = ThemeManager.shared.color.mainColor mProgressView.backgroundColor = UIColor.clear mProgressView.trackTintColor = UIColor.clear view.addSubview(mProgressView) mProgressView.snp.makeConstraints { maker in maker.trailing.leading.equalToSuperview() maker.top.equalToSuperview() maker.height.equalTo(2.0) } } // 创建空白页 private func setupErrorView() { let errorImage = UIImage(named: "EmptyDataSet/network_error") let errorStatusImageView = UIImageView(image: errorImage) errorStatusImageView.contentMode = .scaleAspectFit view.addSubview(errorStatusImageView) let errorStatusLabel = UILabel() errorStatusLabel.text = "oapp_NetworkFailed".localizedString errorStatusLabel.font = UIFont.systemFont(ofSize: 13, weight: .medium) // errorStatusLabel.textColor = UIColor.themeTC4 view.addSubview(errorStatusLabel) let refreshBtn = UIButton(type: .custom) refreshBtn.setImage(UIImage(named: "EmptyDataSet/empty_reload"), for: .normal) refreshBtn.addTarget(self, action: #selector(reload), for: .touchUpInside) view.addSubview(refreshBtn) // contraints errorStatusImageView.snp.makeConstraints { (maker) in maker.centerX.equalToSuperview() maker.centerY.equalToSuperview().offset(-70.0) } errorStatusLabel.snp.makeConstraints { (maker) in maker.top.equalTo(errorStatusImageView.snp.bottom).offset(15.0) maker.centerX.equalToSuperview() } refreshBtn.snp.makeConstraints { (maker) in maker.top.equalTo(errorStatusLabel.snp.bottom).offset(39.0) maker.centerX.equalToSuperview() } } // MARK: - LeftBarButtonItems private func setupLeftBarButtonItem(iconColor: String) { guard !isNavigationBarHidden else { return } // let backItem = UIBarButtonItem(image: UIImage(named: "Common/back_\(iconColor)"), // style: .plain, // target: self, action: #selector(leftBackClicked(_:))) // backItem.tintColor = iconColor=="white" ? .white : .black // backItem.tag = 1 let backBtn = UIButton(frame: CGRect(x: 0, y: 0, width: 19, height: 19)) backBtn.setImage(UIImage(named: "Common/back_\(iconColor)"), for: .normal) backBtn.addTarget(self, action: #selector(leftBackClicked(_:)), for: .touchUpInside) backBtn.tintColor = iconColor=="white" ? .white : .black backBtn.tag = 1 let backItem = UIBarButtonItem(customView: backBtn) let popBtn = UIButton(frame: CGRect(x: 0, y: 0, width: 19, height: 19)) popBtn.setImage(UIImage(named: "Common/close_black"), for: .normal) popBtn.addTarget(self, action: #selector(leftBackClicked(_:)), for: .touchUpInside) popBtn.tintColor = iconColor=="white" ? .white : .black popBtn.tag = 2 let popItem = UIBarButtonItem(customView: popBtn) // let popItem = UIBarButtonItem(image: UIImage(named: "Common/close_black"), // style: .plain, // target: self, // action: #selector(leftBackClicked(_:))) // popItem.tintColor = iconColor=="white" ? .white : .black // popItem.tag = 2 navigationItem.leftBarButtonItems = [backItem, popItem] } override func leftBackClicked(_ item: UIBarButtonItem) { if canGoBack { // 1.如果有缓存页,则加载上一页 if webView.canGoBack { webView.goBack() } else { // 2.否则关闭 if isPresented { dismiss(animated: true, completion: nil) } else { navigationController?.popViewController(animated: true) } } } else { if isH5Pay { NotificationCenter.default.post(name: Notification.Name("RequestPayStatus"), object: nil) } navigationController?.popViewController(animated: true) } } // MARK: bindAction private func bindAction() { } // MARK: Observer private func addObservers() { self.webView.rx.observeWeakly(String.self, "title").subscribe(onNext: { [weak self] (title) in guard let self = self else { return } if self.isHome { self.navTitle = self.webView.title ?? "" } else { self.navTitle = self.webView.title ?? ""//self.webView.title == "物联物美" ? "便民商圈" : self.webView.title ?? "" } }).disposed(by: disposeBag) self.webView.rx.observeWeakly(Double.self, "estimatedProgress").subscribe(onNext: { [weak self] _ in UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseInOut, animations: { if let progres = self?.webView.estimatedProgress { self?.progressView?.progress = Float(progres) } }, completion: nil) }).disposed(by: disposeBag) } // 加载 request func loadRequest(_ loadUrl: URL) { updateShow(status: .initial) var request = URLRequest(url: loadUrl, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 15) webView?.load(request) } // 重新加载 @objc func reload() { updateShow(status: .initial) // WKWebsiteDataStore.default().removeData(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), // modifiedSince: Date(timeIntervalSince1970: 0), completionHandler:{ // debugPrint("清除web缓存") // }) if webView.url != nil { webView.reload() } else if let loadUrl = URL(string: url) { // 重新加载初始页 loadRequest(loadUrl) } } //UI显示加载 状态机 private func updateShow(status: LoadingStatus) { switch status { case .initial: self.webView.isHidden = false self.progressView?.isHidden = false self.progressView?.progress = 0.0 case .inLoading: self.webView.isHidden = false self.progressView?.isHidden = false case .success: self.webView.isHidden = false self.progressView?.isHidden = true case .faild: self.webView.isHidden = true self.progressView?.isHidden = true } switch status { case .initial: break case .inLoading: break case .success, .faild: if hasPullRefresh { self.webView.scrollView.mj_header?.endRefreshing() } } } // 显示Toast private func showLoading(body: Any) { guard let config = body as? String, let show = BoolType(rawValue: config) else { return } show.boolValue ? dl.showLoading() : dl.dismiss() } // 隐藏键盘 private func hideKeyboard() { webView.endEditing(true) } // 下拉刷新header // private func enablePullRefresh(body: Any) { // guard let config = body as? String, // let enable = BoolType(rawValue: config) else { return } // if enable.boolValue { // let header = createRefreshHeader() // webView.scrollView.mj_header = header // } else { // webView.scrollView.mj_header = nil // } // } // 保存图片 // private func saveImage(body: Any) { // ProgressHUD.show(inController: self) // let config = JSON(body) // guard let url = config["image"].url else { return } // let completion: CompletionHandler = { [weak self] (image, _, _, _) in // guard let self = self else { return } // guard let image = image else { // if let callback = config["callback"].string { // self.jsCallback(method: callback, result: false) // } // ProgressHUD.dismiss(forController: self) // return // } // let selector = #selector(self.image(_:didFinishSavingWithError:context:)) // if let callback = config["callback"].string { // let pointer = Unmanaged.passRetained(callback as NSString).toOpaque() // let context = UnsafeMutableRawPointer(pointer) // UIImageWriteToSavedPhotosAlbum(image, self, selector, context) // } else { // UIImageWriteToSavedPhotosAlbum(image, self, selector, nil) // } // } // KingfisherManager.shared.retrieveImage(with: url, // options: KingfisherManager.shared.defaultOptions, // progressBlock: nil, // completionHandler: completion) // } @objc private func image(_ image: UIImage, didFinishSavingWithError error: NSError?, context: UnsafeRawPointer) { if let error = error { error.isShowErrorTips ? DLToast.showError(text: error.localizedDescription) : () } else { DLToast.showSuccess(text: "oapp_Success".localizedString) } let method = unsafeBitCast(context, to: NSString.self) as String if !method.isEmpty { let result = error == nil jsCallback(method: method, result: result) } Unmanaged.fromOpaque(context).release() } private func jsCallback(method: String, result: Bool) { let callbackName = method + "(\(result))" self.webView.evaluateJavaScript(callbackName, completionHandler: nil) } // MARK: - JS to Native private func jsCallback(method: String, params: String, secondParams: String="") { let callbackName = method + (secondParams.isEmpty ? "(\(params))" : "('\(params)', '\(secondParams)')") webView.evaluateJavaScript(callbackName, completionHandler: nil) } // MARK: Native to JS // 页面路由 func openPageRouter(body: Any) { guard let body = body as? [String: Any], let url = body["url"] as? String else { return } AppRouter.push(url) } // 页面显示 func onPageShow() { webView.evaluateJavaScript("try { window.onPageShow(); } catch (e) {}", completionHandler: nil) } // MAKR: - 状态栏样式 func updateStatusBarStyle(body: Any) { guard let body = body as? [String: Any], let style = body["style"] as? String else { return } self.style = style setNeedsStatusBarAppearanceUpdate() } // MARK: - 打开原生页面(无参数) func openPage(body: Any) { guard let body = body as? [String: Any], let pageName = body["pageName"] as? String else { return } if let class_VC = NSClassFromString("dinoGo.\(pageName)") as? BaseViewController.Type { let vc = class_VC.init() navigationController?.pushViewController(vc, animated: true) } } // MARK: - 打开WebView func openWebView(body: Any) { guard let body = body as? [String: Any], let url = body["url"] as? String else { return } let vc = WebViewController(url: url, style: "default") navigationController?.pushViewController(vc, animated: true) } // MARK: - 弹框提示 func dialog(body: Any) { guard let body = body as? [String: Any], let title = body["title"] as? String, let message = body["message"] as? String else { return } showAlert(title: title, message: message, cancelText: "") } // MARK: - 显示toast func toast(body: Any) { guard let body = body as? [String: Any], let message = body["message"] as? String, let type = body["type"] as? Int else { return } switch type { // 0 纯文字 1 成功 2 失败 case 0: DLToast.show(text: message) case 1: DLToast.showSuccess(text: message) case 2: DLToast.showError(text: message) default: break } } // MARK: - 返回上一级页面 二次确认 func backConfirm(body: Any) { guard let body = body as? [String: Any] else { return } if let title = body["title"] as? String { popConfirmTitle = title } if let content = body["content"] as? String { popConfirmContent = content } if let type = body["type"] as? Int { popType = type } else if let type = body["type"] as? String { popType = type.integer } } // MARK: - 退出webview func closeWebView(body: Any) { navigationController?.popViewController(animated: true) } } /// MARK: WKNavigationDelegate extension WebViewController: WKNavigationDelegate { func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { updateShow(status: .inLoading) } func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { } func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { updateShow(status: .success) } func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { updateShow(status: .faild) } func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { updateShow(status: .faild) } func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { let actionPolicy = self.operation.checkAndBlockInternalUrlScheme(request: navigationAction.request) if let url = navigationAction.request.url {//, actionPolicy == .cancel { // navigator.open(url) print(url.absoluteString) } decisionHandler(actionPolicy) // 延迟0.1秒检查返回 DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: { // 设置返回按钮 if self.isHome { if webView.canGoBack { // self.setupLeftBarButtonItem(iconColor: "white") } else { // self.navigationItem.leftBarButtonItem = nil } } }) } } // MARK: WKUIDelegate extension WebViewController: WKUIDelegate { func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) { let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "确定", style: .cancel, handler: { _ in completionHandler() })) present(alert, animated: true, completion: nil) } func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) { let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "确定", style: .default, handler: { _ in completionHandler(true) })) alert.addAction(UIAlertAction(title: "取消", style: .cancel, handler: { _ in completionHandler(false) })) present(alert, animated: true, completion: nil) } func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void) { let alert = UIAlertController(title: title, message: prompt, preferredStyle: .alert) alert.addTextField { textField in textField.text = defaultText } alert.addAction(UIAlertAction(title: "确定", style: .default, handler: { _ in let inputText = alert.textFields?.first?.text completionHandler(inputText) })) alert.addAction(UIAlertAction(title: "取消", style: .cancel, handler: { _ in completionHandler(nil) })) present(alert, animated: true, completion: nil) } } private class CommonMessageHandler: NSObject, WKScriptMessageHandler { weak var webViewController: WebViewController? func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { switch message.name { case "updateStatusBarStyle": webViewController?.updateStatusBarStyle(body: message.body) case "openPageRouter": webViewController?.openPageRouter(body: message.body) case "openPage": webViewController?.openPage(body: message.body) case "dialog": webViewController?.dialog(body: message.body) case "toast": webViewController?.toast(body: message.body) case "backConfirm": webViewController?.backConfirm(body: message.body) case "closeWebView": webViewController?.closeWebView(body: message.body) default: break } } } // MARK: UIDocumentInteractionControllerDelegate extension WebViewController: UIDocumentInteractionControllerDelegate { func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController { return self } }