IQKeyboardManager+Toolbar.swift 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. //
  2. // IQKeyboardManager+Toolbar.swift
  3. // https://github.com/hackiftekhar/IQKeyboardManager
  4. // Copyright (c) 2013-20 Iftekhar Qurashi.
  5. //
  6. // Permission is hereby granted, free of charge, to any person obtaining a copy
  7. // of this software and associated documentation files (the "Software"), to deal
  8. // in the Software without restriction, including without limitation the rights
  9. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  10. // copies of the Software, and to permit persons to whom the Software is
  11. // furnished to do so, subject to the following conditions:
  12. //
  13. // The above copyright notice and this permission notice shall be included in
  14. // all copies or substantial portions of the Software.
  15. //
  16. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  22. // THE SOFTWARE.
  23. // import Foundation - UIKit contains Foundation
  24. import UIKit
  25. @available(iOSApplicationExtension, unavailable)
  26. public extension IQKeyboardManager {
  27. /**
  28. Default tag for toolbar with Done button -1002.
  29. */
  30. private static let kIQDoneButtonToolbarTag = -1002
  31. /**
  32. Default tag for toolbar with Previous/Next buttons -1005.
  33. */
  34. private static let kIQPreviousNextButtonToolbarTag = -1005
  35. /** Add toolbar if it is required to add on textFields and it's siblings. */
  36. internal func addToolbarIfRequired() {
  37. //Either there is no inputAccessoryView or if accessoryView is not appropriate for current situation(There is Previous/Next/Done toolbar).
  38. guard let siblings = responderViews(), !siblings.isEmpty,
  39. let textField = textFieldView, textField.responds(to: #selector(setter: UITextField.inputAccessoryView)),
  40. (textField.inputAccessoryView == nil ||
  41. textField.inputAccessoryView?.tag == IQKeyboardManager.kIQPreviousNextButtonToolbarTag ||
  42. textField.inputAccessoryView?.tag == IQKeyboardManager.kIQDoneButtonToolbarTag) else {
  43. return
  44. }
  45. let startTime = CACurrentMediaTime()
  46. showLog(">>>>> \(#function) started >>>>>", indentation: 1)
  47. showLog("Found \(siblings.count) responder sibling(s)")
  48. let rightConfiguration: IQBarButtonItemConfiguration
  49. if let doneBarButtonItemImage = toolbarDoneBarButtonItemImage {
  50. rightConfiguration = IQBarButtonItemConfiguration(image: doneBarButtonItemImage, action: #selector(self.doneAction(_:)))
  51. } else if let doneBarButtonItemText = toolbarDoneBarButtonItemText {
  52. rightConfiguration = IQBarButtonItemConfiguration(title: doneBarButtonItemText, action: #selector(self.doneAction(_:)))
  53. } else {
  54. rightConfiguration = IQBarButtonItemConfiguration(barButtonSystemItem: .done, action: #selector(self.doneAction(_:)))
  55. }
  56. rightConfiguration.accessibilityLabel = toolbarDoneBarButtonItemAccessibilityLabel ?? "Done"
  57. // If only one object is found, then adding only Done button.
  58. if (siblings.count <= 1 && previousNextDisplayMode == .default) || previousNextDisplayMode == .alwaysHide {
  59. textField.addKeyboardToolbarWithTarget(target: self, titleText: (shouldShowToolbarPlaceholder ? textField.drawingToolbarPlaceholder: nil), rightBarButtonConfiguration: rightConfiguration, previousBarButtonConfiguration: nil, nextBarButtonConfiguration: nil)
  60. textField.inputAccessoryView?.tag = IQKeyboardManager.kIQDoneButtonToolbarTag // (Bug ID: #78)
  61. } else if previousNextDisplayMode == .default || previousNextDisplayMode == .alwaysShow {
  62. let prevConfiguration: IQBarButtonItemConfiguration
  63. if let doneBarButtonItemImage = toolbarPreviousBarButtonItemImage {
  64. prevConfiguration = IQBarButtonItemConfiguration(image: doneBarButtonItemImage, action: #selector(self.previousAction(_:)))
  65. } else if let doneBarButtonItemText = toolbarPreviousBarButtonItemText {
  66. prevConfiguration = IQBarButtonItemConfiguration(title: doneBarButtonItemText, action: #selector(self.previousAction(_:)))
  67. } else {
  68. prevConfiguration = IQBarButtonItemConfiguration(image: (UIImage.keyboardPreviousImage() ?? UIImage()), action: #selector(self.previousAction(_:)))
  69. }
  70. prevConfiguration.accessibilityLabel = toolbarPreviousBarButtonItemAccessibilityLabel ?? "Previous"
  71. let nextConfiguration: IQBarButtonItemConfiguration
  72. if let doneBarButtonItemImage = toolbarNextBarButtonItemImage {
  73. nextConfiguration = IQBarButtonItemConfiguration(image: doneBarButtonItemImage, action: #selector(self.nextAction(_:)))
  74. } else if let doneBarButtonItemText = toolbarNextBarButtonItemText {
  75. nextConfiguration = IQBarButtonItemConfiguration(title: doneBarButtonItemText, action: #selector(self.nextAction(_:)))
  76. } else {
  77. nextConfiguration = IQBarButtonItemConfiguration(image: (UIImage.keyboardNextImage() ?? UIImage()), action: #selector(self.nextAction(_:)))
  78. }
  79. nextConfiguration.accessibilityLabel = toolbarNextBarButtonItemAccessibilityLabel ?? "Next"
  80. textField.addKeyboardToolbarWithTarget(target: self, titleText: (shouldShowToolbarPlaceholder ? textField.drawingToolbarPlaceholder: nil), rightBarButtonConfiguration: rightConfiguration, previousBarButtonConfiguration: prevConfiguration, nextBarButtonConfiguration: nextConfiguration)
  81. textField.inputAccessoryView?.tag = IQKeyboardManager.kIQPreviousNextButtonToolbarTag // (Bug ID: #78)
  82. }
  83. let toolbar = textField.keyboardToolbar
  84. //Setting toolbar tintColor // (Enhancement ID: #30)
  85. toolbar.tintColor = shouldToolbarUsesTextFieldTintColor ? textField.tintColor : toolbarTintColor
  86. // Setting toolbar to keyboard.
  87. if let textFieldView = textField as? UITextInput {
  88. //Bar style according to keyboard appearance
  89. switch textFieldView.keyboardAppearance {
  90. case .dark?:
  91. toolbar.barStyle = .black
  92. toolbar.barTintColor = nil
  93. default:
  94. toolbar.barStyle = .default
  95. toolbar.barTintColor = toolbarBarTintColor
  96. }
  97. }
  98. //Setting toolbar title font. // (Enhancement ID: #30)
  99. if shouldShowToolbarPlaceholder, !textField.shouldHideToolbarPlaceholder {
  100. //Updating placeholder font to toolbar. //(Bug ID: #148, #272)
  101. if toolbar.titleBarButton.title == nil ||
  102. toolbar.titleBarButton.title != textField.drawingToolbarPlaceholder {
  103. toolbar.titleBarButton.title = textField.drawingToolbarPlaceholder
  104. }
  105. //Setting toolbar title font. // (Enhancement ID: #30)
  106. toolbar.titleBarButton.titleFont = placeholderFont
  107. //Setting toolbar title color. // (Enhancement ID: #880)
  108. toolbar.titleBarButton.titleColor = placeholderColor
  109. //Setting toolbar button title color. // (Enhancement ID: #880)
  110. toolbar.titleBarButton.selectableTitleColor = placeholderButtonColor
  111. } else {
  112. toolbar.titleBarButton.title = nil
  113. }
  114. //In case of UITableView (Special), the next/previous buttons has to be refreshed everytime. (Bug ID: #56)
  115. textField.keyboardToolbar.previousBarButton.isEnabled = (siblings.first != textField) // If firstTextField, then previous should not be enabled.
  116. textField.keyboardToolbar.nextBarButton.isEnabled = (siblings.last != textField) // If lastTextField then next should not be enaled.
  117. let elapsedTime = CACurrentMediaTime() - startTime
  118. showLog("<<<<< \(#function) ended: \(elapsedTime) seconds <<<<<", indentation: -1)
  119. }
  120. /** Remove any toolbar if it is IQToolbar. */
  121. internal func removeToolbarIfRequired() { // (Bug ID: #18)
  122. guard let siblings = responderViews(), !siblings.isEmpty,
  123. let textField = textFieldView, textField.responds(to: #selector(setter: UITextField.inputAccessoryView)),
  124. (textField.inputAccessoryView == nil ||
  125. textField.inputAccessoryView?.tag == IQKeyboardManager.kIQPreviousNextButtonToolbarTag ||
  126. textField.inputAccessoryView?.tag == IQKeyboardManager.kIQDoneButtonToolbarTag) else {
  127. return
  128. }
  129. let startTime = CACurrentMediaTime()
  130. showLog(">>>>> \(#function) started >>>>>", indentation: 1)
  131. showLog("Found \(siblings.count) responder sibling(s)")
  132. for view in siblings {
  133. if let toolbar = view.inputAccessoryView as? IQToolbar {
  134. //setInputAccessoryView: check (Bug ID: #307)
  135. if view.responds(to: #selector(setter: UITextField.inputAccessoryView)),
  136. (toolbar.tag == IQKeyboardManager.kIQDoneButtonToolbarTag || toolbar.tag == IQKeyboardManager.kIQPreviousNextButtonToolbarTag) {
  137. if let textField = view as? UITextField {
  138. textField.inputAccessoryView = nil
  139. } else if let textView = view as? UITextView {
  140. textView.inputAccessoryView = nil
  141. }
  142. view.reloadInputViews()
  143. }
  144. }
  145. }
  146. let elapsedTime = CACurrentMediaTime() - startTime
  147. showLog("<<<<< \(#function) ended: \(elapsedTime) seconds <<<<<", indentation: -1)
  148. }
  149. /** reloadInputViews to reload toolbar buttons enable/disable state on the fly Enhancement ID #434. */
  150. @objc func reloadInputViews() {
  151. //If enabled then adding toolbar.
  152. if privateIsEnableAutoToolbar() {
  153. self.addToolbarIfRequired()
  154. } else {
  155. self.removeToolbarIfRequired()
  156. }
  157. }
  158. }
  159. // MARK: Previous next button actions
  160. @available(iOSApplicationExtension, unavailable)
  161. public extension IQKeyboardManager {
  162. /**
  163. Returns YES if can navigate to previous responder textField/textView, otherwise NO.
  164. */
  165. @objc var canGoPrevious: Bool {
  166. //If it is not first textField. then it's previous object canBecomeFirstResponder.
  167. guard let textFields = responderViews(), let textFieldRetain = textFieldView, let index = textFields.firstIndex(of: textFieldRetain), index > 0 else {
  168. return false
  169. }
  170. return true
  171. }
  172. /**
  173. Returns YES if can navigate to next responder textField/textView, otherwise NO.
  174. */
  175. @objc var canGoNext: Bool {
  176. //If it is not first textField. then it's previous object canBecomeFirstResponder.
  177. guard let textFields = responderViews(), let textFieldRetain = textFieldView, let index = textFields.firstIndex(of: textFieldRetain), index < textFields.count-1 else {
  178. return false
  179. }
  180. return true
  181. }
  182. /**
  183. Navigate to previous responder textField/textView.
  184. */
  185. @objc @discardableResult func goPrevious() -> Bool {
  186. //If it is not first textField. then it's previous object becomeFirstResponder.
  187. guard let textFields = responderViews(), let textFieldRetain = textFieldView, let index = textFields.firstIndex(of: textFieldRetain), index > 0 else {
  188. return false
  189. }
  190. let nextTextField = textFields[index-1]
  191. let isAcceptAsFirstResponder = nextTextField.becomeFirstResponder()
  192. // If it refuses then becoming previous textFieldView as first responder again. (Bug ID: #96)
  193. if isAcceptAsFirstResponder == false {
  194. //If next field refuses to become first responder then restoring old textField as first responder.
  195. textFieldRetain.becomeFirstResponder()
  196. showLog("Refuses to become first responder: \(nextTextField)")
  197. }
  198. return isAcceptAsFirstResponder }
  199. /**
  200. Navigate to next responder textField/textView.
  201. */
  202. @objc @discardableResult func goNext() -> Bool {
  203. //If it is not first textField. then it's previous object becomeFirstResponder.
  204. guard let textFields = responderViews(), let textFieldRetain = textFieldView, let index = textFields.firstIndex(of: textFieldRetain), index < textFields.count-1 else {
  205. return false
  206. }
  207. let nextTextField = textFields[index+1]
  208. let isAcceptAsFirstResponder = nextTextField.becomeFirstResponder()
  209. // If it refuses then becoming previous textFieldView as first responder again. (Bug ID: #96)
  210. if isAcceptAsFirstResponder == false {
  211. //If next field refuses to become first responder then restoring old textField as first responder.
  212. textFieldRetain.becomeFirstResponder()
  213. showLog("Refuses to become first responder: \(nextTextField)")
  214. }
  215. return isAcceptAsFirstResponder
  216. }
  217. /** previousAction. */
  218. @objc internal func previousAction (_ barButton: IQBarButtonItem) {
  219. //If user wants to play input Click sound.
  220. if shouldPlayInputClicks {
  221. //Play Input Click Sound.
  222. UIDevice.current.playInputClick()
  223. }
  224. guard canGoPrevious, let textFieldRetain = textFieldView else {
  225. return
  226. }
  227. let isAcceptAsFirstResponder = goPrevious()
  228. var invocation = barButton.invocation
  229. var sender = textFieldRetain
  230. //Handling search bar special case
  231. do {
  232. if let searchBar = textFieldRetain.textFieldSearchBar() {
  233. invocation = searchBar.keyboardToolbar.previousBarButton.invocation
  234. sender = searchBar
  235. }
  236. }
  237. if isAcceptAsFirstResponder {
  238. invocation?.invoke(from: sender)
  239. }
  240. }
  241. /** nextAction. */
  242. @objc internal func nextAction (_ barButton: IQBarButtonItem) {
  243. //If user wants to play input Click sound.
  244. if shouldPlayInputClicks {
  245. //Play Input Click Sound.
  246. UIDevice.current.playInputClick()
  247. }
  248. guard canGoNext, let textFieldRetain = textFieldView else {
  249. return
  250. }
  251. let isAcceptAsFirstResponder = goNext()
  252. var invocation = barButton.invocation
  253. var sender = textFieldRetain
  254. //Handling search bar special case
  255. do {
  256. if let searchBar = textFieldRetain.textFieldSearchBar() {
  257. invocation = searchBar.keyboardToolbar.nextBarButton.invocation
  258. sender = searchBar
  259. }
  260. }
  261. if isAcceptAsFirstResponder {
  262. invocation?.invoke(from: sender)
  263. }
  264. }
  265. /** doneAction. Resigning current textField. */
  266. @objc internal func doneAction (_ barButton: IQBarButtonItem) {
  267. //If user wants to play input Click sound.
  268. if shouldPlayInputClicks {
  269. //Play Input Click Sound.
  270. UIDevice.current.playInputClick()
  271. }
  272. guard let textFieldRetain = textFieldView else {
  273. return
  274. }
  275. //Resign textFieldView.
  276. let isResignedFirstResponder = resignFirstResponder()
  277. var invocation = barButton.invocation
  278. var sender = textFieldRetain
  279. //Handling search bar special case
  280. do {
  281. if let searchBar = textFieldRetain.textFieldSearchBar() {
  282. invocation = searchBar.keyboardToolbar.doneBarButton.invocation
  283. sender = searchBar
  284. }
  285. }
  286. if isResignedFirstResponder {
  287. invocation?.invoke(from: sender)
  288. }
  289. }
  290. }