LNWebViewController.swift 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. //
  2. // LNWebViewController.swift
  3. // Lanu
  4. //
  5. // Created by OneeChan on 2025/12/12.
  6. //
  7. import Foundation
  8. import UIKit
  9. import SnapKit
  10. import WebKit
  11. import Combine
  12. import AutoCodable
  13. class LNJumpWebViewConfig {
  14. var url: String
  15. var customTitle: String = ""
  16. var showNavigationBar = false
  17. var setSafeArea = false
  18. static func normalConfig(url: String) -> LNJumpWebViewConfig {
  19. let config = LNJumpWebViewConfig(url: url)
  20. config.showNavigationBar = true
  21. config.setSafeArea = true
  22. return config
  23. }
  24. init(url: String, title: String = "") {
  25. self.url = url
  26. customTitle = title
  27. }
  28. init(param: LNWebPageDeeplinkParams) {
  29. url = param.url
  30. showNavigationBar = param.header
  31. setSafeArea = param.safeArea
  32. }
  33. }
  34. extension UIView {
  35. func pushToWebView(_ config: LNJumpWebViewConfig) {
  36. if config.url.isDeeplink, let url = URL(string: config.url) {
  37. LNDeeplinkManager.shared.handleDeepLink(url)
  38. return
  39. }
  40. let vc = LNWebViewController(config: config)
  41. navigationController?.pushViewController(vc, animated: true)
  42. }
  43. func pushToWebView(_ param: LNWebPageDeeplinkParams) {
  44. let config = LNJumpWebViewConfig(param: param)
  45. let vc = LNWebViewController(config: config)
  46. navigationController?.pushViewController(vc, animated: true)
  47. }
  48. }
  49. class LNWebViewController: LNViewController {
  50. private let config: LNJumpWebViewConfig
  51. private var webView: WKWebView?
  52. init(config: LNJumpWebViewConfig) {
  53. self.config = config
  54. super.init(nibName: nil, bundle: nil)
  55. }
  56. required init?(coder: NSCoder) {
  57. fatalError("init(coder:) has not been implemented")
  58. }
  59. override func viewDidLoad() {
  60. super.viewDidLoad()
  61. setupViews()
  62. loadUrl()
  63. }
  64. }
  65. extension LNWebViewController {
  66. private func loadUrl() {
  67. guard let url = URL(string: config.url) else { return }
  68. webView?.load(URLRequest(url: url))
  69. }
  70. }
  71. extension LNWebViewController: WKNavigationDelegate {
  72. func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction,
  73. decisionHandler: @escaping @MainActor (WKNavigationActionPolicy) -> Void) {
  74. guard let url = navigationAction.request.url,
  75. let scheme = url.scheme else {
  76. decisionHandler(.allow) // 无 URL/Schema,允许默认行为
  77. return
  78. }
  79. guard scheme.lowercased() == LNDeeplinkUrls.appScheme else {
  80. decisionHandler(.allow)
  81. return
  82. }
  83. let fullPath = if let host = url.host {
  84. "\(host)\(url.path)"
  85. } else {
  86. url.path
  87. }
  88. if fullPath == LNDeeplinkUrls.web.closeWindow.deeplinkPath {
  89. navigationController?.popViewController(animated: true)
  90. } else if fullPath == LNDeeplinkUrls.web.userCancellation.deeplinkPath {
  91. LNAccountManager.shared.clean()
  92. } else {
  93. LNDeeplinkManager.shared.handleDeepLink(url)
  94. }
  95. decisionHandler(.cancel)
  96. }
  97. }
  98. extension LNWebViewController {
  99. private func setupViews() {
  100. if !config.customTitle.isEmpty {
  101. title = config.customTitle
  102. }
  103. showNavigationBar = config.showNavigationBar
  104. let webView = buildWebView()
  105. view.addSubview(webView)
  106. webView.snp.makeConstraints { make in
  107. make.horizontalEdges.equalToSuperview()
  108. if config.showNavigationBar || !config.setSafeArea {
  109. make.top.equalToSuperview()
  110. } else if config.setSafeArea {
  111. make.top.equalToSuperview().offset(UIView.statusBarHeight)
  112. }
  113. if config.setSafeArea {
  114. make.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom)
  115. } else {
  116. make.bottom.equalToSuperview()
  117. }
  118. }
  119. }
  120. private func buildWebView() -> UIView {
  121. let webConfig = WKWebViewConfiguration()
  122. setupDefaultLocalStorage(webConfig)
  123. let webView = WKWebView(frame: .zero, configuration: webConfig)
  124. webView.navigationDelegate = self
  125. #if DEBUG
  126. if #available(iOS 16.4, *) {
  127. webView.isInspectable = true
  128. }
  129. #endif
  130. webView.scrollView.contentInsetAdjustmentBehavior = .never
  131. webView.scrollView.bounces = false
  132. setupDefaultCookie(webView)
  133. webView.publisher(for: \.title).removeDuplicates().sink
  134. { [weak self] webTitle in
  135. guard let self else { return }
  136. guard config.customTitle.isEmpty else { return }
  137. title = webTitle
  138. }.store(in: &bag)
  139. self.webView = webView
  140. return webView
  141. }
  142. private func setupDefaultLocalStorage(_ webConfig: WKWebViewConfiguration) {
  143. let tokenScript = WKUserScript(
  144. source: "window.localStorage.setItem('GAMI-web_auth_token', '\(LNAccountManager.shared.token)')",
  145. injectionTime: .atDocumentStart, forMainFrameOnly: true)
  146. webConfig.userContentController.addUserScript(tokenScript)
  147. let languageScript = WKUserScript(
  148. source: "window.localStorage.setItem('GAMI-web_app_locale', '\(LNAppConfig.shared.curLang.bundleName)')",
  149. injectionTime: .atDocumentStart, forMainFrameOnly: true)
  150. webConfig.userContentController.addUserScript(languageScript)
  151. var header: [String: Any] = [:]
  152. header["time"] = curTimeInMicro
  153. header["network"] = LNNetworkMonitor.curNetworkType.desc
  154. header["version"] = curBuildVersion
  155. header["api"] = 1
  156. header["channel"] = "appStore"
  157. header["platform"] = "2"
  158. header["device"] = curDeviceModelName
  159. header["app"] = curAppBundleIdentifier
  160. header["udid"] = curDeviceId
  161. header["token"] = LNAccountManager.shared.token
  162. if let jsonData = try? JSONSerialization.data(withJSONObject: header),
  163. let jsonStr = String(data: jsonData, encoding: .utf8) {
  164. let headerScript = WKUserScript(
  165. source: "javascript:window.GAMI_BRIDGE={'requestHeader': \(jsonStr), 'safeArea': \"\(UIView.statusBarHeight)|\(view.safeBottomInset)\"}",
  166. injectionTime: .atDocumentStart,
  167. forMainFrameOnly: true)
  168. webConfig.userContentController.addUserScript(headerScript)
  169. }
  170. }
  171. private func setupDefaultCookie(_ webView: WKWebView) {
  172. // 创建 Cookie
  173. if let tokenCookie = HTTPCookie(properties: [
  174. .domain: ".gami.vip",
  175. .path: "/",
  176. .name: "GAMI-cookie_auth_token",
  177. .value: LNAccountManager.shared.token,
  178. .secure: "TRUE",
  179. .expires: NSDate(timeIntervalSinceNow: 3600)
  180. ]) {
  181. webView.configuration.websiteDataStore.httpCookieStore.setCookie(tokenCookie)
  182. }
  183. if let languageCookie = HTTPCookie(properties: [
  184. .domain: ".gami.vip",
  185. .path: "/",
  186. .name: "GAMI-cookie_app_locale",
  187. .value: LNAppConfig.shared.curLang.bundleName,
  188. .secure: "TRUE",
  189. .expires: NSDate(timeIntervalSinceNow: 3600)
  190. ]) {
  191. webView.configuration.websiteDataStore.httpCookieStore.setCookie(languageCookie)
  192. }
  193. if let languageCookie = HTTPCookie(properties: [
  194. .domain: ".gami.vip",
  195. .path: "/",
  196. .name: "GAMI-cookie_safe_area",
  197. .value: "\(UIView.statusBarHeight)|\(view.safeBottomInset)",
  198. .secure: "TRUE",
  199. .expires: NSDate(timeIntervalSinceNow: 3600)
  200. ]) {
  201. webView.configuration.websiteDataStore.httpCookieStore.setCookie(languageCookie)
  202. }
  203. }
  204. }