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

701 lines
25 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 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
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" {
let btn = UIButton(type: .custom)
btn.setImage(UIImage(named: "Common/back"), for: .normal)
btn.addTarget(self, action: #selector(leftBackClicked(_:)), for: .touchUpInside)
navigationItem.leftBarButtonItem = UIBarButtonItem(customView: btn)
}
// 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(type: .custom)
backBtn.frame = CGRect(x: 0, y: 0, width: 19, height: 19)
backBtn.setImage(UIImage(named: "Common/back"), for: .normal)
backBtn.addTarget(self, action: #selector(leftBackClicked(_:)), for: .touchUpInside)
// backBtn.tintColor = .black
backBtn.tag = 1
let backItem = UIBarButtonItem(customView: backBtn)
let popBtn = UIButton(type: .custom)
popBtn.frame = CGRect(x: 0, y: 0, width: 19, height: 19)
popBtn.setImage(UIImage(named: "Common/x_black"), for: .normal)
popBtn.addTarget(self, action: #selector(leftBackClicked(_:)), for: .touchUpInside)
// popBtn.tintColor = .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 {
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 ?? ""
}
}).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 {
DLToast.showError(error)
} 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 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: - 打开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 "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
}
}