IQKeyboardManager+UIKeyboardNotification.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. //
  2. // IQKeyboardManager+UIKeyboardNotification.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. // MARK: UIKeyboard Notifications
  26. @available(iOSApplicationExtension, unavailable)
  27. public extension IQKeyboardManager {
  28. private struct AssociatedKeys {
  29. static var keyboardShowing = "keyboardShowing"
  30. static var keyboardShowNotification = "keyboardShowNotification"
  31. static var keyboardFrame = "keyboardFrame"
  32. static var animationDuration = "animationDuration"
  33. static var animationCurve = "animationCurve"
  34. }
  35. /**
  36. Boolean to know if keyboard is showing.
  37. */
  38. @objc private(set) var keyboardShowing: Bool {
  39. get {
  40. return objc_getAssociatedObject(self, &AssociatedKeys.keyboardShowing) as? Bool ?? false
  41. }
  42. set(newValue) {
  43. objc_setAssociatedObject(self, &AssociatedKeys.keyboardShowing, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
  44. }
  45. }
  46. /** To save keyboardWillShowNotification. Needed for enable keyboard functionality. */
  47. internal var keyboardShowNotification: Notification? {
  48. get {
  49. return objc_getAssociatedObject(self, &AssociatedKeys.keyboardShowNotification) as? Notification
  50. }
  51. set(newValue) {
  52. objc_setAssociatedObject(self, &AssociatedKeys.keyboardShowNotification, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
  53. }
  54. }
  55. /** To save keyboard rame. */
  56. @objc private(set) var keyboardFrame: CGRect {
  57. get {
  58. return objc_getAssociatedObject(self, &AssociatedKeys.keyboardFrame) as? CGRect ?? .zero
  59. }
  60. set(newValue) {
  61. objc_setAssociatedObject(self, &AssociatedKeys.keyboardFrame, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
  62. }
  63. }
  64. /** To save keyboard animation duration. */
  65. internal var animationDuration: TimeInterval {
  66. get {
  67. return objc_getAssociatedObject(self, &AssociatedKeys.animationDuration) as? TimeInterval ?? 0.25
  68. }
  69. set(newValue) {
  70. objc_setAssociatedObject(self, &AssociatedKeys.animationDuration, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
  71. }
  72. }
  73. /** To mimic the keyboard animation */
  74. internal var animationCurve: UIView.AnimationOptions {
  75. get {
  76. return objc_getAssociatedObject(self, &AssociatedKeys.animationCurve) as? UIView.AnimationOptions ?? .curveEaseOut
  77. }
  78. set(newValue) {
  79. objc_setAssociatedObject(self, &AssociatedKeys.animationCurve, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
  80. }
  81. }
  82. /* UIKeyboardWillShowNotification. */
  83. @objc internal func keyboardWillShow(_ notification: Notification) {
  84. keyboardShowNotification = notification
  85. // Boolean to know keyboard is showing/hiding
  86. keyboardShowing = true
  87. let oldKBFrame = keyboardFrame
  88. if let info = notification.userInfo {
  89. // Getting keyboard animation.
  90. if let curve = info[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt {
  91. animationCurve = UIView.AnimationOptions(rawValue: curve).union(.beginFromCurrentState)
  92. } else {
  93. animationCurve = UIView.AnimationOptions.curveEaseOut.union(.beginFromCurrentState)
  94. }
  95. // Getting keyboard animation duration
  96. animationDuration = info[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval ?? 0.25
  97. // Getting UIKeyboardSize.
  98. if let kbFrame = info[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
  99. keyboardFrame = kbFrame
  100. showLog("UIKeyboard Frame: \(keyboardFrame)")
  101. }
  102. }
  103. guard privateIsEnabled() else {
  104. restorePosition()
  105. topViewBeginOrigin = IQKeyboardManager.kIQCGPointInvalid
  106. return
  107. }
  108. let startTime = CACurrentMediaTime()
  109. showLog("⌨️>>>>> \(#function) started >>>>>", indentation: 1)
  110. showLog("Notification Object:\(notification.object ?? "NULL")")
  111. // (Bug ID: #5)
  112. if let textFieldView = textFieldView, topViewBeginOrigin.equalTo(IQKeyboardManager.kIQCGPointInvalid) {
  113. // keyboard is not showing(At the beginning only). We should save rootViewRect.
  114. rootViewController = textFieldView.parentContainerViewController()
  115. if let controller = rootViewController {
  116. if rootViewControllerWhilePopGestureRecognizerActive == controller {
  117. topViewBeginOrigin = topViewBeginOriginWhilePopGestureRecognizerActive
  118. } else {
  119. topViewBeginOrigin = controller.view.frame.origin
  120. }
  121. rootViewControllerWhilePopGestureRecognizerActive = nil
  122. topViewBeginOriginWhilePopGestureRecognizerActive = IQKeyboardManager.kIQCGPointInvalid
  123. self.showLog("Saving \(controller) beginning origin: \(self.topViewBeginOrigin)")
  124. }
  125. }
  126. //If last restored keyboard size is different(any orientation accure), then refresh. otherwise not.
  127. if keyboardFrame.equalTo(oldKBFrame) == false {
  128. //If textFieldView is inside UITableViewController then let UITableViewController to handle it (Bug ID: #37) (Bug ID: #76) See note:- https://developer.apple.com/library/ios/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/KeyboardManagement/KeyboardManagement.html If it is UIAlertView textField then do not affect anything (Bug ID: #70).
  129. if keyboardShowing,
  130. let textFieldView = textFieldView,
  131. textFieldView.isAlertViewTextField() == false {
  132. // keyboard is already showing. adjust position.
  133. optimizedAdjustPosition()
  134. }
  135. }
  136. let elapsedTime = CACurrentMediaTime() - startTime
  137. showLog("⌨️<<<<< \(#function) ended: \(elapsedTime) seconds <<<<<", indentation: -1)
  138. }
  139. /* UIKeyboardDidShowNotification. */
  140. @objc internal func keyboardDidShow(_ notification: Notification) {
  141. guard privateIsEnabled(),
  142. let textFieldView = textFieldView,
  143. let parentController = textFieldView.parentContainerViewController(), (parentController.modalPresentationStyle == UIModalPresentationStyle.formSheet || parentController.modalPresentationStyle == UIModalPresentationStyle.pageSheet),
  144. textFieldView.isAlertViewTextField() == false else {
  145. return
  146. }
  147. let startTime = CACurrentMediaTime()
  148. showLog("⌨️>>>>> \(#function) started >>>>>", indentation: 1)
  149. showLog("Notification Object:\(notification.object ?? "NULL")")
  150. self.optimizedAdjustPosition()
  151. let elapsedTime = CACurrentMediaTime() - startTime
  152. showLog("⌨️<<<<< \(#function) ended: \(elapsedTime) seconds <<<<<", indentation: -1)
  153. }
  154. /* UIKeyboardWillHideNotification. So setting rootViewController to it's default frame. */
  155. @objc internal func keyboardWillHide(_ notification: Notification?) {
  156. //If it's not a fake notification generated by [self setEnable:NO].
  157. if notification != nil {
  158. keyboardShowNotification = nil
  159. }
  160. // Boolean to know keyboard is showing/hiding
  161. keyboardShowing = false
  162. if let info = notification?.userInfo {
  163. // Getting keyboard animation.
  164. if let curve = info[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt {
  165. animationCurve = UIView.AnimationOptions(rawValue: curve).union(.beginFromCurrentState)
  166. } else {
  167. animationCurve = UIView.AnimationOptions.curveEaseOut.union(.beginFromCurrentState)
  168. }
  169. // Getting keyboard animation duration
  170. animationDuration = info[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval ?? 0.25
  171. }
  172. //If not enabled then do nothing.
  173. guard privateIsEnabled() else {
  174. return
  175. }
  176. let startTime = CACurrentMediaTime()
  177. showLog("⌨️>>>>> \(#function) started >>>>>", indentation: 1)
  178. showLog("Notification Object:\(notification?.object ?? "NULL")")
  179. //Commented due to #56. Added all the conditions below to handle WKWebView's textFields. (Bug ID: #56)
  180. // We are unable to get textField object while keyboard showing on WKWebView's textField. (Bug ID: #11)
  181. // if (_textFieldView == nil) return
  182. //Restoring the contentOffset of the lastScrollView
  183. if let lastScrollView = lastScrollView {
  184. UIView.animate(withDuration: animationDuration, delay: 0, options: animationCurve, animations: { () -> Void in
  185. if lastScrollView.contentInset != self.startingContentInsets {
  186. self.showLog("Restoring contentInset to: \(self.startingContentInsets)")
  187. lastScrollView.contentInset = self.startingContentInsets
  188. lastScrollView.scrollIndicatorInsets = self.startingScrollIndicatorInsets
  189. }
  190. if lastScrollView.shouldRestoreScrollViewContentOffset, !lastScrollView.contentOffset.equalTo(self.startingContentOffset) {
  191. self.showLog("Restoring contentOffset to: \(self.startingContentOffset)")
  192. let animatedContentOffset = self.textFieldView?.superviewOfClassType(UIStackView.self, belowView: lastScrollView) != nil // (Bug ID: #1365, #1508, #1541)
  193. if animatedContentOffset {
  194. lastScrollView.setContentOffset(self.startingContentOffset, animated: UIView.areAnimationsEnabled)
  195. } else {
  196. lastScrollView.contentOffset = self.startingContentOffset
  197. }
  198. }
  199. // TODO: restore scrollView state
  200. // This is temporary solution. Have to implement the save and restore scrollView state
  201. var superScrollView: UIScrollView? = lastScrollView
  202. while let scrollView = superScrollView {
  203. let contentSize = CGSize(width: max(scrollView.contentSize.width, scrollView.frame.width), height: max(scrollView.contentSize.height, scrollView.frame.height))
  204. let minimumY = contentSize.height - scrollView.frame.height
  205. if minimumY < scrollView.contentOffset.y {
  206. let newContentOffset = CGPoint(x: scrollView.contentOffset.x, y: minimumY)
  207. if scrollView.contentOffset.equalTo(newContentOffset) == false {
  208. let animatedContentOffset = self.textFieldView?.superviewOfClassType(UIStackView.self, belowView: scrollView) != nil // (Bug ID: #1365, #1508, #1541)
  209. if animatedContentOffset {
  210. scrollView.setContentOffset(newContentOffset, animated: UIView.areAnimationsEnabled)
  211. } else {
  212. scrollView.contentOffset = newContentOffset
  213. }
  214. self.showLog("Restoring contentOffset to: \(self.startingContentOffset)")
  215. }
  216. }
  217. superScrollView = scrollView.superviewOfClassType(UIScrollView.self) as? UIScrollView
  218. }
  219. })
  220. }
  221. restorePosition()
  222. //Reset all values
  223. lastScrollView = nil
  224. keyboardFrame = CGRect.zero
  225. startingContentInsets = UIEdgeInsets()
  226. startingScrollIndicatorInsets = UIEdgeInsets()
  227. startingContentOffset = CGPoint.zero
  228. // topViewBeginRect = CGRectZero //Commented due to #82
  229. let elapsedTime = CACurrentMediaTime() - startTime
  230. showLog("⌨️<<<<< \(#function) ended: \(elapsedTime) seconds <<<<<", indentation: -1)
  231. }
  232. @objc internal func keyboardDidHide(_ notification: Notification) {
  233. let startTime = CACurrentMediaTime()
  234. showLog("⌨️>>>>> \(#function) started >>>>>", indentation: 1)
  235. showLog("Notification Object:\(notification.object ?? "NULL")")
  236. topViewBeginOrigin = IQKeyboardManager.kIQCGPointInvalid
  237. keyboardFrame = CGRect.zero
  238. let elapsedTime = CACurrentMediaTime() - startTime
  239. showLog("⌨️<<<<< \(#function) ended: \(elapsedTime) seconds <<<<<", indentation: -1)
  240. }
  241. }