IQKeyboardManager+Position.swift 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717
  1. //
  2. // IQKeyboardManager+Position.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. private struct AssociatedKeys {
  28. static var movedDistance = "movedDistance"
  29. static var movedDistanceChanged = "movedDistanceChanged"
  30. static var lastScrollView = "lastScrollView"
  31. static var startingContentOffset = "startingContentOffset"
  32. static var startingScrollIndicatorInsets = "startingScrollIndicatorInsets"
  33. static var startingContentInsets = "startingContentInsets"
  34. static var startingTextViewContentInsets = "startingTextViewContentInsets"
  35. static var startingTextViewScrollIndicatorInsets = "startingTextViewScrollIndicatorInsets"
  36. static var isTextViewContentInsetChanged = "isTextViewContentInsetChanged"
  37. static var hasPendingAdjustRequest = "hasPendingAdjustRequest"
  38. }
  39. /**
  40. moved distance to the top used to maintain distance between keyboard and textField. Most of the time this will be a positive value.
  41. */
  42. @objc private(set) var movedDistance: CGFloat {
  43. get {
  44. return objc_getAssociatedObject(self, &AssociatedKeys.movedDistance) as? CGFloat ?? 0.0
  45. }
  46. set(newValue) {
  47. objc_setAssociatedObject(self, &AssociatedKeys.movedDistance, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
  48. movedDistanceChanged?(movedDistance)
  49. }
  50. }
  51. /**
  52. Will be called then movedDistance will be changed
  53. */
  54. @objc var movedDistanceChanged: ((CGFloat) -> Void)? {
  55. get {
  56. return objc_getAssociatedObject(self, &AssociatedKeys.movedDistanceChanged) as? ((CGFloat) -> Void)
  57. }
  58. set(newValue) {
  59. objc_setAssociatedObject(self, &AssociatedKeys.movedDistanceChanged, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
  60. movedDistanceChanged?(movedDistance)
  61. }
  62. }
  63. /** Variable to save lastScrollView that was scrolled. */
  64. internal weak var lastScrollView: UIScrollView? {
  65. get {
  66. return (objc_getAssociatedObject(self, &AssociatedKeys.lastScrollView) as? WeakObjectContainer)?.object as? UIScrollView
  67. }
  68. set(newValue) {
  69. objc_setAssociatedObject(self, &AssociatedKeys.lastScrollView, WeakObjectContainer(object: newValue), .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
  70. }
  71. }
  72. /** LastScrollView's initial contentOffset. */
  73. internal var startingContentOffset: CGPoint {
  74. get {
  75. return objc_getAssociatedObject(self, &AssociatedKeys.startingContentOffset) as? CGPoint ?? IQKeyboardManager.kIQCGPointInvalid
  76. }
  77. set(newValue) {
  78. objc_setAssociatedObject(self, &AssociatedKeys.startingContentOffset, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
  79. }
  80. }
  81. /** LastScrollView's initial scrollIndicatorInsets. */
  82. internal var startingScrollIndicatorInsets: UIEdgeInsets {
  83. get {
  84. return objc_getAssociatedObject(self, &AssociatedKeys.startingScrollIndicatorInsets) as? UIEdgeInsets ?? .init()
  85. }
  86. set(newValue) {
  87. objc_setAssociatedObject(self, &AssociatedKeys.startingScrollIndicatorInsets, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
  88. }
  89. }
  90. /** LastScrollView's initial contentInsets. */
  91. internal var startingContentInsets: UIEdgeInsets {
  92. get {
  93. return objc_getAssociatedObject(self, &AssociatedKeys.startingContentInsets) as? UIEdgeInsets ?? .init()
  94. }
  95. set(newValue) {
  96. objc_setAssociatedObject(self, &AssociatedKeys.startingContentInsets, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
  97. }
  98. }
  99. /** used to adjust contentInset of UITextView. */
  100. internal var startingTextViewContentInsets: UIEdgeInsets {
  101. get {
  102. return objc_getAssociatedObject(self, &AssociatedKeys.startingTextViewContentInsets) as? UIEdgeInsets ?? .init()
  103. }
  104. set(newValue) {
  105. objc_setAssociatedObject(self, &AssociatedKeys.startingTextViewContentInsets, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
  106. }
  107. }
  108. /** used to adjust scrollIndicatorInsets of UITextView. */
  109. internal var startingTextViewScrollIndicatorInsets: UIEdgeInsets {
  110. get {
  111. return objc_getAssociatedObject(self, &AssociatedKeys.startingTextViewScrollIndicatorInsets) as? UIEdgeInsets ?? .init()
  112. }
  113. set(newValue) {
  114. objc_setAssociatedObject(self, &AssociatedKeys.startingTextViewScrollIndicatorInsets, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
  115. }
  116. }
  117. /** used with textView to detect a textFieldView contentInset is changed or not. (Bug ID: #92)*/
  118. internal var isTextViewContentInsetChanged: Bool {
  119. get {
  120. return objc_getAssociatedObject(self, &AssociatedKeys.isTextViewContentInsetChanged) as? Bool ?? false
  121. }
  122. set(newValue) {
  123. objc_setAssociatedObject(self, &AssociatedKeys.isTextViewContentInsetChanged, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
  124. }
  125. }
  126. /** To know if we have any pending request to adjust view position. */
  127. private var hasPendingAdjustRequest: Bool {
  128. get {
  129. return objc_getAssociatedObject(self, &AssociatedKeys.hasPendingAdjustRequest) as? Bool ?? false
  130. }
  131. set(newValue) {
  132. objc_setAssociatedObject(self, &AssociatedKeys.hasPendingAdjustRequest, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
  133. }
  134. }
  135. internal func optimizedAdjustPosition() {
  136. if !hasPendingAdjustRequest {
  137. hasPendingAdjustRequest = true
  138. OperationQueue.main.addOperation {
  139. self.adjustPosition()
  140. self.hasPendingAdjustRequest = false
  141. }
  142. }
  143. }
  144. /* Adjusting RootViewController's frame according to interface orientation. */
  145. private func adjustPosition() {
  146. // We are unable to get textField object while keyboard showing on WKWebView's textField. (Bug ID: #11)
  147. guard hasPendingAdjustRequest,
  148. let textFieldView = textFieldView,
  149. let rootController = textFieldView.parentContainerViewController(),
  150. let window = keyWindow(),
  151. let textFieldViewRectInWindow = textFieldView.superview?.convert(textFieldView.frame, to: window),
  152. let textFieldViewRectInRootSuperview = textFieldView.superview?.convert(textFieldView.frame, to: rootController.view?.superview) else {
  153. return
  154. }
  155. let startTime = CACurrentMediaTime()
  156. showLog(">>>>> \(#function) started >>>>>", indentation: 1)
  157. // Getting RootViewOrigin.
  158. var rootViewOrigin = rootController.view.frame.origin
  159. //Maintain keyboardDistanceFromTextField
  160. var specialKeyboardDistanceFromTextField = textFieldView.keyboardDistanceFromTextField
  161. if let searchBar = textFieldView.textFieldSearchBar() {
  162. specialKeyboardDistanceFromTextField = searchBar.keyboardDistanceFromTextField
  163. }
  164. let newKeyboardDistanceFromTextField = (specialKeyboardDistanceFromTextField == kIQUseDefaultKeyboardDistance) ? keyboardDistanceFromTextField : specialKeyboardDistanceFromTextField
  165. var kbSize = keyboardFrame.size
  166. do {
  167. var kbFrame = keyboardFrame
  168. kbFrame.origin.y -= newKeyboardDistanceFromTextField
  169. kbFrame.size.height += newKeyboardDistanceFromTextField
  170. //Calculating actual keyboard covered size respect to window, keyboard frame may be different when hardware keyboard is attached (Bug ID: #469) (Bug ID: #381) (Bug ID: #1506)
  171. let intersectRect = kbFrame.intersection(window.frame)
  172. if intersectRect.isNull {
  173. kbSize = CGSize(width: kbFrame.size.width, height: 0)
  174. } else {
  175. kbSize = intersectRect.size
  176. }
  177. }
  178. let statusBarHeight: CGFloat
  179. let navigationBarAreaHeight: CGFloat
  180. if let navigationController = rootController.navigationController {
  181. navigationBarAreaHeight = navigationController.navigationBar.frame.maxY
  182. } else {
  183. #if swift(>=5.1)
  184. if #available(iOS 13, *) {
  185. statusBarHeight = window.windowScene?.statusBarManager?.statusBarFrame.height ?? 0
  186. } else {
  187. statusBarHeight = UIApplication.shared.statusBarFrame.height
  188. }
  189. #else
  190. statusBarHeight = UIApplication.shared.statusBarFrame.height
  191. #endif
  192. navigationBarAreaHeight = statusBarHeight
  193. }
  194. let layoutAreaHeight: CGFloat = rootController.view.layoutMargins.bottom
  195. let isTextView: Bool
  196. let isNonScrollableTextView: Bool
  197. if let textView = textFieldView as? UIScrollView, textFieldView.responds(to: #selector(getter: UITextView.isEditable)) {
  198. isTextView = true
  199. isNonScrollableTextView = !textView.isScrollEnabled
  200. } else {
  201. isTextView = false
  202. isNonScrollableTextView = false
  203. }
  204. let topLayoutGuide: CGFloat = max(navigationBarAreaHeight, layoutAreaHeight) + 5
  205. let bottomLayoutGuide: CGFloat = (isTextView && !isNonScrollableTextView) ? 0 : rootController.view.layoutMargins.bottom //Validation of textView for case where there is a tab bar at the bottom or running on iPhone X and textView is at the bottom.
  206. let visibleHeight: CGFloat = window.frame.height-kbSize.height
  207. // Move positive = textField is hidden.
  208. // Move negative = textField is showing.
  209. // Calculating move position.
  210. var move: CGFloat
  211. //Special case: when the textView is not scrollable, then we'll be scrolling to the bottom part and let hide the top part above
  212. if isNonScrollableTextView {
  213. move = textFieldViewRectInWindow.maxY - visibleHeight + bottomLayoutGuide
  214. } else {
  215. move = min(textFieldViewRectInRootSuperview.minY-(topLayoutGuide), textFieldViewRectInWindow.maxY - visibleHeight + bottomLayoutGuide)
  216. }
  217. showLog("Need to move: \(move)")
  218. var superScrollView: UIScrollView?
  219. var superView = textFieldView.superviewOfClassType(UIScrollView.self) as? UIScrollView
  220. //Getting UIScrollView whose scrolling is enabled. // (Bug ID: #285)
  221. while let view = superView {
  222. if view.isScrollEnabled, !view.shouldIgnoreScrollingAdjustment {
  223. superScrollView = view
  224. break
  225. } else {
  226. // Getting it's superScrollView. // (Enhancement ID: #21, #24)
  227. superView = view.superviewOfClassType(UIScrollView.self) as? UIScrollView
  228. }
  229. }
  230. //If there was a lastScrollView. // (Bug ID: #34)
  231. if let lastScrollView = lastScrollView {
  232. //If we can't find current superScrollView, then setting lastScrollView to it's original form.
  233. if superScrollView == nil {
  234. if lastScrollView.contentInset != self.startingContentInsets {
  235. showLog("Restoring contentInset to: \(startingContentInsets)")
  236. UIView.animate(withDuration: animationDuration, delay: 0, options: animationCurve, animations: { () -> Void in
  237. lastScrollView.contentInset = self.startingContentInsets
  238. lastScrollView.scrollIndicatorInsets = self.startingScrollIndicatorInsets
  239. })
  240. }
  241. if lastScrollView.shouldRestoreScrollViewContentOffset, !lastScrollView.contentOffset.equalTo(startingContentOffset) {
  242. showLog("Restoring contentOffset to: \(startingContentOffset)")
  243. let animatedContentOffset = textFieldView.superviewOfClassType(UIStackView.self, belowView: lastScrollView) != nil // (Bug ID: #1365, #1508, #1541)
  244. if animatedContentOffset {
  245. lastScrollView.setContentOffset(startingContentOffset, animated: UIView.areAnimationsEnabled)
  246. } else {
  247. lastScrollView.contentOffset = startingContentOffset
  248. }
  249. }
  250. startingContentInsets = UIEdgeInsets()
  251. startingScrollIndicatorInsets = UIEdgeInsets()
  252. startingContentOffset = CGPoint.zero
  253. self.lastScrollView = nil
  254. } else if superScrollView != lastScrollView { //If both scrollView's are different, then reset lastScrollView to it's original frame and setting current scrollView as last scrollView.
  255. if lastScrollView.contentInset != self.startingContentInsets {
  256. showLog("Restoring contentInset to: \(startingContentInsets)")
  257. UIView.animate(withDuration: animationDuration, delay: 0, options: animationCurve, animations: { () -> Void in
  258. lastScrollView.contentInset = self.startingContentInsets
  259. lastScrollView.scrollIndicatorInsets = self.startingScrollIndicatorInsets
  260. })
  261. }
  262. if lastScrollView.shouldRestoreScrollViewContentOffset, !lastScrollView.contentOffset.equalTo(startingContentOffset) {
  263. showLog("Restoring contentOffset to: \(startingContentOffset)")
  264. let animatedContentOffset = textFieldView.superviewOfClassType(UIStackView.self, belowView: lastScrollView) != nil // (Bug ID: #1365, #1508, #1541)
  265. if animatedContentOffset {
  266. lastScrollView.setContentOffset(startingContentOffset, animated: UIView.areAnimationsEnabled)
  267. } else {
  268. lastScrollView.contentOffset = startingContentOffset
  269. }
  270. }
  271. self.lastScrollView = superScrollView
  272. if let scrollView = superScrollView {
  273. startingContentInsets = scrollView.contentInset
  274. startingContentOffset = scrollView.contentOffset
  275. #if swift(>=5.1)
  276. if #available(iOS 11.1, *) {
  277. startingScrollIndicatorInsets = scrollView.verticalScrollIndicatorInsets
  278. } else {
  279. startingScrollIndicatorInsets = scrollView.scrollIndicatorInsets
  280. }
  281. #else
  282. startingScrollIndicatorInsets = scrollView.scrollIndicatorInsets
  283. #endif
  284. }
  285. showLog("Saving ScrollView New contentInset: \(startingContentInsets) and contentOffset: \(startingContentOffset)")
  286. }
  287. //Else the case where superScrollView == lastScrollView means we are on same scrollView after switching to different textField. So doing nothing, going ahead
  288. } else if let unwrappedSuperScrollView = superScrollView { //If there was no lastScrollView and we found a current scrollView. then setting it as lastScrollView.
  289. lastScrollView = unwrappedSuperScrollView
  290. startingContentInsets = unwrappedSuperScrollView.contentInset
  291. startingContentOffset = unwrappedSuperScrollView.contentOffset
  292. #if swift(>=5.1)
  293. if #available(iOS 11.1, *) {
  294. startingScrollIndicatorInsets = unwrappedSuperScrollView.verticalScrollIndicatorInsets
  295. } else {
  296. startingScrollIndicatorInsets = unwrappedSuperScrollView.scrollIndicatorInsets
  297. }
  298. #else
  299. startingScrollIndicatorInsets = unwrappedSuperScrollView.scrollIndicatorInsets
  300. #endif
  301. showLog("Saving ScrollView contentInset: \(startingContentInsets) and contentOffset: \(startingContentOffset)")
  302. }
  303. // Special case for ScrollView.
  304. // If we found lastScrollView then setting it's contentOffset to show textField.
  305. if let lastScrollView = lastScrollView {
  306. //Saving
  307. var lastView = textFieldView
  308. var superScrollView = self.lastScrollView
  309. while let scrollView = superScrollView {
  310. var shouldContinue = false
  311. if move > 0 {
  312. shouldContinue = move > (-scrollView.contentOffset.y - scrollView.contentInset.top)
  313. } else if let tableView = scrollView.superviewOfClassType(UITableView.self) as? UITableView {
  314. shouldContinue = scrollView.contentOffset.y > 0
  315. if shouldContinue, let tableCell = textFieldView.superviewOfClassType(UITableViewCell.self) as? UITableViewCell, let indexPath = tableView.indexPath(for: tableCell), let previousIndexPath = tableView.previousIndexPath(of: indexPath) {
  316. let previousCellRect = tableView.rectForRow(at: previousIndexPath)
  317. if !previousCellRect.isEmpty {
  318. let previousCellRectInRootSuperview = tableView.convert(previousCellRect, to: rootController.view.superview)
  319. move = min(0, previousCellRectInRootSuperview.maxY - topLayoutGuide)
  320. }
  321. }
  322. } else if let collectionView = scrollView.superviewOfClassType(UICollectionView.self) as? UICollectionView {
  323. shouldContinue = scrollView.contentOffset.y > 0
  324. if shouldContinue, let collectionCell = textFieldView.superviewOfClassType(UICollectionViewCell.self) as? UICollectionViewCell, let indexPath = collectionView.indexPath(for: collectionCell), let previousIndexPath = collectionView.previousIndexPath(of: indexPath), let attributes = collectionView.layoutAttributesForItem(at: previousIndexPath) {
  325. let previousCellRect = attributes.frame
  326. if !previousCellRect.isEmpty {
  327. let previousCellRectInRootSuperview = collectionView.convert(previousCellRect, to: rootController.view.superview)
  328. move = min(0, previousCellRectInRootSuperview.maxY - topLayoutGuide)
  329. }
  330. }
  331. } else {
  332. if isNonScrollableTextView {
  333. shouldContinue = textFieldViewRectInWindow.maxY < visibleHeight + bottomLayoutGuide
  334. if shouldContinue {
  335. move = min(0, textFieldViewRectInWindow.maxY - visibleHeight + bottomLayoutGuide)
  336. }
  337. } else {
  338. shouldContinue = textFieldViewRectInRootSuperview.minY < topLayoutGuide
  339. if shouldContinue {
  340. move = min(0, textFieldViewRectInRootSuperview.minY - topLayoutGuide)
  341. }
  342. }
  343. }
  344. //Looping in upper hierarchy until we don't found any scrollView in it's upper hirarchy till UIWindow object.
  345. if shouldContinue {
  346. var tempScrollView = scrollView.superviewOfClassType(UIScrollView.self) as? UIScrollView
  347. var nextScrollView: UIScrollView?
  348. while let view = tempScrollView {
  349. if view.isScrollEnabled, !view.shouldIgnoreScrollingAdjustment {
  350. nextScrollView = view
  351. break
  352. } else {
  353. tempScrollView = view.superviewOfClassType(UIScrollView.self) as? UIScrollView
  354. }
  355. }
  356. //Getting lastViewRect.
  357. if let lastViewRect = lastView.superview?.convert(lastView.frame, to: scrollView) {
  358. //Calculating the expected Y offset from move and scrollView's contentOffset.
  359. var shouldOffsetY = scrollView.contentOffset.y - min(scrollView.contentOffset.y, -move)
  360. //Rearranging the expected Y offset according to the view.
  361. if isNonScrollableTextView {
  362. shouldOffsetY = min(shouldOffsetY, lastViewRect.maxY - visibleHeight + bottomLayoutGuide)
  363. } else {
  364. shouldOffsetY = min(shouldOffsetY, lastViewRect.minY)
  365. }
  366. //[_textFieldView isKindOfClass:[UITextView class]] If is a UITextView type
  367. //nextScrollView == nil If processing scrollView is last scrollView in upper hierarchy (there is no other scrollView upper hierrchy.)
  368. //[_textFieldView isKindOfClass:[UITextView class]] If is a UITextView type
  369. //shouldOffsetY >= 0 shouldOffsetY must be greater than in order to keep distance from navigationBar (Bug ID: #92)
  370. if isTextView, !isNonScrollableTextView,
  371. nextScrollView == nil,
  372. shouldOffsetY >= 0 {
  373. // Converting Rectangle according to window bounds.
  374. if let currentTextFieldViewRect = textFieldView.superview?.convert(textFieldView.frame, to: window) {
  375. //Calculating expected fix distance which needs to be managed from navigation bar
  376. let expectedFixDistance: CGFloat = currentTextFieldViewRect.minY - topLayoutGuide
  377. //Now if expectedOffsetY (superScrollView.contentOffset.y + expectedFixDistance) is lower than current shouldOffsetY, which means we're in a position where navigationBar up and hide, then reducing shouldOffsetY with expectedOffsetY (superScrollView.contentOffset.y + expectedFixDistance)
  378. shouldOffsetY = min(shouldOffsetY, scrollView.contentOffset.y + expectedFixDistance)
  379. //Setting move to 0 because now we don't want to move any view anymore (All will be managed by our contentInset logic.
  380. move = 0
  381. } else {
  382. //Subtracting the Y offset from the move variable, because we are going to change scrollView's contentOffset.y to shouldOffsetY.
  383. move -= (shouldOffsetY-scrollView.contentOffset.y)
  384. }
  385. } else {
  386. //Subtracting the Y offset from the move variable, because we are going to change scrollView's contentOffset.y to shouldOffsetY.
  387. move -= (shouldOffsetY-scrollView.contentOffset.y)
  388. }
  389. let newContentOffset = CGPoint(x: scrollView.contentOffset.x, y: shouldOffsetY)
  390. if scrollView.contentOffset.equalTo(newContentOffset) == false {
  391. showLog("old contentOffset: \(scrollView.contentOffset) new contentOffset: \(newContentOffset)")
  392. self.showLog("Remaining Move: \(move)")
  393. //Getting problem while using `setContentOffset:animated:`, So I used animation API.
  394. UIView.animate(withDuration: animationDuration, delay: 0, options: animationCurve, animations: { () -> Void in
  395. let animatedContentOffset = textFieldView.superviewOfClassType(UIStackView.self, belowView: scrollView) != nil // (Bug ID: #1365, #1508, #1541)
  396. if animatedContentOffset {
  397. scrollView.setContentOffset(newContentOffset, animated: UIView.areAnimationsEnabled)
  398. } else {
  399. scrollView.contentOffset = newContentOffset
  400. }
  401. }, completion: { _ in
  402. if scrollView is UITableView || scrollView is UICollectionView {
  403. //This will update the next/previous states
  404. self.addToolbarIfRequired()
  405. }
  406. })
  407. }
  408. }
  409. // Getting next lastView & superScrollView.
  410. lastView = scrollView
  411. superScrollView = nextScrollView
  412. } else {
  413. move = 0
  414. break
  415. }
  416. }
  417. //Updating contentInset
  418. if let lastScrollViewRect = lastScrollView.superview?.convert(lastScrollView.frame, to: window),
  419. lastScrollView.shouldIgnoreContentInsetAdjustment == false {
  420. var bottomInset: CGFloat = (kbSize.height)-(window.frame.height-lastScrollViewRect.maxY)
  421. var bottomScrollIndicatorInset = bottomInset - newKeyboardDistanceFromTextField
  422. // Update the insets so that the scroll vew doesn't shift incorrectly when the offset is near the bottom of the scroll view.
  423. bottomInset = max(startingContentInsets.bottom, bottomInset)
  424. bottomScrollIndicatorInset = max(startingScrollIndicatorInsets.bottom, bottomScrollIndicatorInset)
  425. if #available(iOS 11, *) {
  426. bottomInset -= lastScrollView.safeAreaInsets.bottom
  427. bottomScrollIndicatorInset -= lastScrollView.safeAreaInsets.bottom
  428. }
  429. var movedInsets = lastScrollView.contentInset
  430. movedInsets.bottom = bottomInset
  431. if lastScrollView.contentInset != movedInsets {
  432. showLog("old ContentInset: \(lastScrollView.contentInset) new ContentInset: \(movedInsets)")
  433. UIView.animate(withDuration: animationDuration, delay: 0, options: animationCurve, animations: { () -> Void in
  434. lastScrollView.contentInset = movedInsets
  435. var newScrollIndicatorInset: UIEdgeInsets
  436. #if swift(>=5.1)
  437. if #available(iOS 11.1, *) {
  438. newScrollIndicatorInset = lastScrollView.verticalScrollIndicatorInsets
  439. } else {
  440. newScrollIndicatorInset = lastScrollView.scrollIndicatorInsets
  441. }
  442. #else
  443. newScrollIndicatorInset = lastScrollView.scrollIndicatorInsets
  444. #endif
  445. newScrollIndicatorInset.bottom = bottomScrollIndicatorInset
  446. lastScrollView.scrollIndicatorInsets = newScrollIndicatorInset
  447. })
  448. }
  449. }
  450. }
  451. //Going ahead. No else if.
  452. //Special case for UITextView(Readjusting textView.contentInset when textView hight is too big to fit on screen)
  453. //_lastScrollView If not having inside any scrollView, (now contentInset manages the full screen textView.
  454. //[_textFieldView isKindOfClass:[UITextView class]] If is a UITextView type
  455. if let textView = textFieldView as? UIScrollView, textView.isScrollEnabled, textFieldView.responds(to: #selector(getter: UITextView.isEditable)) {
  456. // CGRect rootSuperViewFrameInWindow = [_rootViewController.view.superview convertRect:_rootViewController.view.superview.bounds toView:keyWindow];
  457. //
  458. // CGFloat keyboardOverlapping = CGRectGetMaxY(rootSuperViewFrameInWindow) - keyboardYPosition;
  459. //
  460. // CGFloat textViewHeight = MIN(CGRectGetHeight(_textFieldView.frame), (CGRectGetHeight(rootSuperViewFrameInWindow)-topLayoutGuide-keyboardOverlapping));
  461. let keyboardYPosition = window.frame.height - (kbSize.height-newKeyboardDistanceFromTextField)
  462. var rootSuperViewFrameInWindow = window.frame
  463. if let rootSuperview = rootController.view.superview {
  464. rootSuperViewFrameInWindow = rootSuperview.convert(rootSuperview.bounds, to: window)
  465. }
  466. let keyboardOverlapping = rootSuperViewFrameInWindow.maxY - keyboardYPosition
  467. let textViewHeight = min(textView.frame.height, rootSuperViewFrameInWindow.height-topLayoutGuide-keyboardOverlapping)
  468. if textView.frame.size.height-textView.contentInset.bottom>textViewHeight {
  469. //_isTextViewContentInsetChanged, If frame is not change by library in past, then saving user textView properties (Bug ID: #92)
  470. if !self.isTextViewContentInsetChanged {
  471. self.startingTextViewContentInsets = textView.contentInset
  472. #if swift(>=5.1)
  473. if #available(iOS 11.1, *) {
  474. self.startingTextViewScrollIndicatorInsets = textView.verticalScrollIndicatorInsets
  475. } else {
  476. self.startingTextViewScrollIndicatorInsets = textView.scrollIndicatorInsets
  477. }
  478. #else
  479. self.startingTextViewScrollIndicatorInsets = textView.scrollIndicatorInsets
  480. #endif
  481. }
  482. self.isTextViewContentInsetChanged = true
  483. var newContentInset = textView.contentInset
  484. newContentInset.bottom = textView.frame.size.height-textViewHeight
  485. if #available(iOS 11, *) {
  486. newContentInset.bottom -= textView.safeAreaInsets.bottom
  487. }
  488. if textView.contentInset != newContentInset {
  489. self.showLog("\(textFieldView) Old UITextView.contentInset: \(textView.contentInset) New UITextView.contentInset: \(newContentInset)")
  490. UIView.animate(withDuration: animationDuration, delay: 0, options: animationCurve, animations: { () -> Void in
  491. textView.contentInset = newContentInset
  492. textView.scrollIndicatorInsets = newContentInset
  493. }, completion: { (_) -> Void in })
  494. }
  495. }
  496. }
  497. // +Positive or zero.
  498. if move >= 0 {
  499. rootViewOrigin.y = max(rootViewOrigin.y - move, min(0, -(kbSize.height-newKeyboardDistanceFromTextField)))
  500. if rootController.view.frame.origin.equalTo(rootViewOrigin) == false {
  501. showLog("Moving Upward")
  502. UIView.animate(withDuration: animationDuration, delay: 0, options: animationCurve, animations: { () -> Void in
  503. var rect = rootController.view.frame
  504. rect.origin = rootViewOrigin
  505. rootController.view.frame = rect
  506. //Animating content if needed (Bug ID: #204)
  507. if self.layoutIfNeededOnUpdate {
  508. //Animating content (Bug ID: #160)
  509. rootController.view.setNeedsLayout()
  510. rootController.view.layoutIfNeeded()
  511. }
  512. self.showLog("Set \(rootController) origin to: \(rootViewOrigin)")
  513. })
  514. }
  515. movedDistance = (topViewBeginOrigin.y-rootViewOrigin.y)
  516. } else { // -Negative
  517. let disturbDistance: CGFloat = rootViewOrigin.y-topViewBeginOrigin.y
  518. // disturbDistance Negative = frame disturbed.
  519. // disturbDistance positive = frame not disturbed.
  520. if disturbDistance <= 0 {
  521. rootViewOrigin.y -= max(move, disturbDistance)
  522. if rootController.view.frame.origin.equalTo(rootViewOrigin) == false {
  523. showLog("Moving Downward")
  524. // Setting adjusted rootViewRect
  525. // Setting adjusted rootViewRect
  526. UIView.animate(withDuration: animationDuration, delay: 0, options: animationCurve, animations: { () -> Void in
  527. var rect = rootController.view.frame
  528. rect.origin = rootViewOrigin
  529. rootController.view.frame = rect
  530. //Animating content if needed (Bug ID: #204)
  531. if self.layoutIfNeededOnUpdate {
  532. //Animating content (Bug ID: #160)
  533. rootController.view.setNeedsLayout()
  534. rootController.view.layoutIfNeeded()
  535. }
  536. self.showLog("Set \(rootController) origin to: \(rootViewOrigin)")
  537. })
  538. }
  539. movedDistance = (topViewBeginOrigin.y-rootViewOrigin.y)
  540. }
  541. }
  542. let elapsedTime = CACurrentMediaTime() - startTime
  543. showLog("<<<<< \(#function) ended: \(elapsedTime) seconds <<<<<", indentation: -1)
  544. }
  545. internal func restorePosition() {
  546. hasPendingAdjustRequest = false
  547. // Setting rootViewController frame to it's original position. // (Bug ID: #18)
  548. guard topViewBeginOrigin.equalTo(IQKeyboardManager.kIQCGPointInvalid) == false, let rootViewController = rootViewController else {
  549. return
  550. }
  551. if rootViewController.view.frame.origin.equalTo(self.topViewBeginOrigin) == false {
  552. //Used UIViewAnimationOptionBeginFromCurrentState to minimize strange animations.
  553. UIView.animate(withDuration: animationDuration, delay: 0, options: animationCurve, animations: { () -> Void in
  554. self.showLog("Restoring \(rootViewController) origin to: \(self.topViewBeginOrigin)")
  555. // Setting it's new frame
  556. var rect = rootViewController.view.frame
  557. rect.origin = self.topViewBeginOrigin
  558. rootViewController.view.frame = rect
  559. //Animating content if needed (Bug ID: #204)
  560. if self.layoutIfNeededOnUpdate {
  561. //Animating content (Bug ID: #160)
  562. rootViewController.view.setNeedsLayout()
  563. rootViewController.view.layoutIfNeeded()
  564. }
  565. })
  566. }
  567. self.movedDistance = 0
  568. if rootViewController.navigationController?.interactivePopGestureRecognizer?.state == .began {
  569. self.rootViewControllerWhilePopGestureRecognizerActive = rootViewController
  570. self.topViewBeginOriginWhilePopGestureRecognizerActive = self.topViewBeginOrigin
  571. }
  572. self.rootViewController = nil
  573. }
  574. }