WebViewViewController.swift 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. import UIKit
  2. import WebKit
  3. class WebViewViewController: UIViewController, WKNavigationDelegate {
  4. public lazy var webView: WKWebView = {
  5. let view = WKWebView(frame: .zero, configuration: configuration)
  6. view.navigationDelegate = self
  7. return view
  8. }()
  9. lazy var searchController: UISearchController = {
  10. let searchController = UISearchController(searchResultsController: nil)
  11. searchController.obscuresBackgroundDuringPresentation = false
  12. searchController.searchBar.placeholder = String.localized("search")
  13. searchController.searchBar.delegate = self
  14. searchController.delegate = self
  15. searchController.searchBar.inputAccessoryView = accessoryViewContainer
  16. searchController.searchBar.autocorrectionType = .yes
  17. searchController.searchBar.keyboardType = .default
  18. return searchController
  19. }()
  20. lazy var accessoryViewContainer: InputBarAccessoryView = {
  21. let inputBar = InputBarAccessoryView()
  22. inputBar.setMiddleContentView(searchAccessoryBar, animated: false)
  23. inputBar.sendButton.isHidden = true
  24. inputBar.delegate = self
  25. return inputBar
  26. }()
  27. public lazy var searchAccessoryBar: ChatSearchAccessoryBar = {
  28. let view = ChatSearchAccessoryBar()
  29. view.delegate = self
  30. view.translatesAutoresizingMaskIntoConstraints = false
  31. view.isEnabled = false
  32. return view
  33. }()
  34. private lazy var keyboardManager: KeyboardManager? = {
  35. let manager = KeyboardManager()
  36. return manager
  37. }()
  38. private var debounceTimer: Timer?
  39. private var initializedSearch = false
  40. open var allowSearch = false
  41. open var configuration: WKWebViewConfiguration {
  42. let preferences = WKPreferences()
  43. let config = WKWebViewConfiguration()
  44. if #available(iOS 14.0, *) {
  45. config.defaultWebpagePreferences.allowsContentJavaScript = false
  46. } else {
  47. preferences.javaScriptEnabled = false
  48. }
  49. config.preferences = preferences
  50. return config
  51. }
  52. init() {
  53. super.init(nibName: nil, bundle: nil)
  54. hidesBottomBarWhenPushed = true
  55. }
  56. required init?(coder: NSCoder) {
  57. fatalError("init(coder:) has not been implemented")
  58. }
  59. func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
  60. if navigationAction.navigationType == .linkActivated,
  61. let url = navigationAction.request.url,
  62. url.host != nil,
  63. UIApplication.shared.canOpenURL(url) {
  64. UIApplication.shared.open(url)
  65. decisionHandler(.cancel)
  66. return
  67. }
  68. decisionHandler(.allow)
  69. }
  70. // MARK: - lifecycle
  71. override func viewDidLoad() {
  72. super.viewDidLoad()
  73. setupSubviews()
  74. keyboardManager?.bind(to: webView.scrollView)
  75. keyboardManager?.on(event: .didHide) { [weak self] _ in
  76. self?.webView.scrollView.contentInset.bottom = 0
  77. }
  78. }
  79. override func viewDidDisappear(_ animated: Bool) {
  80. keyboardManager = nil
  81. }
  82. // MARK: - setup + configuration
  83. private func setupSubviews() {
  84. view.addSubview(webView)
  85. webView.translatesAutoresizingMaskIntoConstraints = false
  86. webView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 0).isActive = true
  87. webView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 0).isActive = true
  88. webView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: 0).isActive = true
  89. webView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 0).isActive = true
  90. webView.scrollView.keyboardDismissMode = .interactive
  91. webView.scrollView.contentInset.bottom = 0
  92. if allowSearch, #available(iOS 14.0, *) {
  93. navigationItem.searchController = searchController
  94. }
  95. accessoryViewContainer.setLeftStackViewWidthConstant(to: 0, animated: false)
  96. accessoryViewContainer.setRightStackViewWidthConstant(to: 0, animated: false)
  97. accessoryViewContainer.padding = UIEdgeInsets(top: 6, left: 0, bottom: 6, right: 0)
  98. }
  99. private func initSearch() {
  100. guard let path = Bundle.main.url(forResource: "search", withExtension: "js", subdirectory: "Assets") else {
  101. logger.error("internal search js not found")
  102. return
  103. }
  104. do {
  105. let data: Data = try Data(contentsOf: path)
  106. let jsCode: String = String(decoding: data, as: UTF8.self)
  107. // inject the search code
  108. webView.evaluateJavaScript(jsCode, completionHandler: { _, error in
  109. if let error = error {
  110. logger.error(error)
  111. }
  112. })
  113. } catch {
  114. logger.error("could not load javascript: \(error)")
  115. }
  116. }
  117. private func find(text: String) {
  118. highlightAllOccurencesOf(string: text)
  119. updateAccessoryBar()
  120. }
  121. private func highlightAllOccurencesOf(string: String) {
  122. // search function
  123. let searchString = "WKWebView_HighlightAllOccurencesOfString('\(string)')"
  124. // perform search
  125. webView.evaluateJavaScript(searchString, completionHandler: { _, error in
  126. if let error = error {
  127. logger.error(error)
  128. }
  129. })
  130. }
  131. private func updateAccessoryBar() {
  132. handleSearchResultCount { [weak self] result in
  133. guard let self = self else { return }
  134. logger.debug("found \(result) elements")
  135. self.searchAccessoryBar.isEnabled = result > 0
  136. self.handleCurrentlySelected { [weak self] position in
  137. self?.searchAccessoryBar.updateSearchResult(sum: result, position: position == -1 ? 0 : position + 1)
  138. }
  139. }
  140. }
  141. private func handleSearchResultCount( completionHandler: @escaping (_ result: Int) -> Void) {
  142. getInt(key: "WKWebView_SearchResultCount", completionHandler: completionHandler)
  143. }
  144. private func handleCurrentlySelected( completionHandler: @escaping (_ result: Int) -> Void) {
  145. getInt(key: "WKWebView_CurrentlySelected", completionHandler: completionHandler)
  146. }
  147. private func getInt(key: String, completionHandler: @escaping (_ result: Int) -> Void) {
  148. webView.evaluateJavaScript(key) { (result, error) in
  149. if let error = error {
  150. logger.error(error)
  151. } else if result != nil,
  152. let integerResult = result as? Int {
  153. completionHandler(integerResult)
  154. }
  155. }
  156. }
  157. private func removeAllHighlights() {
  158. webView.evaluateJavaScript("WKWebView_RemoveAllHighlights()", completionHandler: nil)
  159. updateAccessoryBar()
  160. }
  161. private func searchNext() {
  162. webView.evaluateJavaScript("WKWebView_SearchNext()", completionHandler: nil)
  163. updateAccessoryBar()
  164. }
  165. private func searchPrevious() {
  166. webView.evaluateJavaScript("WKWebView_SearchPrev()", completionHandler: nil)
  167. updateAccessoryBar()
  168. }
  169. }
  170. extension WebViewViewController: UISearchBarDelegate, UISearchControllerDelegate {
  171. func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
  172. debounceTimer?.invalidate()
  173. debounceTimer = Timer.scheduledTimer(withTimeInterval: 0.25, repeats: false) { [weak self] _ in
  174. logger.debug("search for \(searchText)")
  175. if searchText.isEmpty {
  176. self?.removeAllHighlights()
  177. } else {
  178. self?.find(text: searchText)
  179. }
  180. }
  181. }
  182. func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
  183. let text = searchController.searchBar.text ?? ""
  184. self.find(text: text)
  185. }
  186. func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
  187. self.removeAllHighlights()
  188. }
  189. func willPresentSearchController(_ searchController: UISearchController) {
  190. if !initializedSearch {
  191. initializedSearch = true
  192. initSearch()
  193. }
  194. }
  195. }
  196. extension WebViewViewController: ChatSearchDelegate {
  197. func onSearchPreviousPressed() {
  198. logger.debug("onSearchPrevious pressed")
  199. self.searchPrevious()
  200. }
  201. func onSearchNextPressed() {
  202. logger.debug("onSearchNextPressed pressed")
  203. self.searchNext()
  204. }
  205. }
  206. extension WebViewViewController: InputBarAccessoryViewDelegate {
  207. func inputBar(_ inputBar: InputBarAccessoryView, didAdaptToKeyboard height: CGFloat) {
  208. logger.debug("didAdaptToKeyboard: \(height)")
  209. self.webView.scrollView.contentInset.bottom = height
  210. }
  211. }