jsdw_ios/QuickLocation/Section/Web/Controller/WebViewController.swift

725 lines
26 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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<NSString>.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<NSString>.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
}
}