TextKeyboardManager.swift 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  1. //
  2. // TextKeyboardManager.swift
  3. // BSText
  4. //
  5. // Created by BlueSky on 2018/11/12.
  6. // Copyright © 2019 GeekBruce. All rights reserved.
  7. //
  8. import UIKit
  9. fileprivate var _TextKeyboardViewFrameObserverKey: Int = 0
  10. /// Observer for view's frame/bounds/center/transform
  11. fileprivate class TextKeyboardViewFrameObserver: NSObject {
  12. private var keyboardView: UIView?
  13. @objc public var notifyBlock: ((_ keyboard: UIView?) -> Void)?
  14. @objc(addToKeyboardView:) public func addTo(keyboardView: UIView?) {
  15. if self.keyboardView == keyboardView {
  16. return
  17. }
  18. if let _ = self.keyboardView {
  19. removeFrameObserver()
  20. objc_setAssociatedObject(self.keyboardView!, &_TextKeyboardViewFrameObserverKey, nil, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
  21. }
  22. self.keyboardView = keyboardView
  23. if keyboardView != nil {
  24. addFrameObserver()
  25. }
  26. objc_setAssociatedObject(keyboardView!, &_TextKeyboardViewFrameObserverKey, self, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
  27. }
  28. func removeFrameObserver() {
  29. keyboardView?.removeObserver(self, forKeyPath: "frame")
  30. keyboardView?.removeObserver(self, forKeyPath: "center")
  31. keyboardView?.removeObserver(self, forKeyPath: "bounds")
  32. keyboardView?.removeObserver(self, forKeyPath: "transform")
  33. keyboardView = nil
  34. }
  35. func addFrameObserver() {
  36. if keyboardView == nil {
  37. return
  38. }
  39. keyboardView?.addObserver(self, forKeyPath: "frame", options: [], context: nil)
  40. keyboardView?.addObserver(self, forKeyPath: "center", options: [], context: nil)
  41. keyboardView?.addObserver(self, forKeyPath: "bounds", options: [], context: nil)
  42. keyboardView?.addObserver(self, forKeyPath: "transform", options: [], context: nil)
  43. }
  44. public class func observerForView(_ keyboardView: UIView?) -> TextKeyboardViewFrameObserver? {
  45. guard let k = keyboardView else {
  46. return nil
  47. }
  48. return objc_getAssociatedObject(k, &_TextKeyboardViewFrameObserverKey) as? TextKeyboardViewFrameObserver;
  49. }
  50. deinit {
  51. removeFrameObserver()
  52. }
  53. override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
  54. let isPrior: Bool = ((change?[.notificationIsPriorKey] as? Int) != 0)
  55. if isPrior {
  56. return
  57. }
  58. let changeKind = NSKeyValueChange(rawValue: UInt((change?[.kindKey] as? Int) ?? 0))
  59. if changeKind != .setting {
  60. return
  61. }
  62. var newVal = change?[.newKey]
  63. if (newVal as? NSNull) == NSNull() {
  64. newVal = nil
  65. }
  66. if (notifyBlock != nil) {
  67. notifyBlock!(keyboardView)
  68. }
  69. }
  70. }
  71. /**
  72. The TextKeyboardObserver protocol defines the method you can use
  73. to receive system keyboard change information.
  74. */
  75. @objc public protocol TextKeyboardObserver: NSObjectProtocol {
  76. @objc optional func keyboardChanged(with transition: TextKeyboardTransition)
  77. }
  78. // TODO: - here should use struct in pure Swift
  79. /**
  80. System keyboard transition information.
  81. Use -[TextKeyboardManager convertRect:toView:] to convert frame to specified view.
  82. */
  83. public class TextKeyboardTransition: NSObject {
  84. ///< Keyboard visible before transition.
  85. @objc public var fromVisible = false
  86. ///< Keyboard visible after transition.
  87. @objc public var toVisible = false
  88. ///< Keyboard frame before transition.
  89. @objc public var fromFrame = CGRect.zero
  90. ///< Keyboard frame after transition.
  91. @objc public var toFrame = CGRect.zero
  92. ///< Keyboard transition animation duration.
  93. @objc public var animationDuration: TimeInterval = 0
  94. ///< Keyboard transition animation curve.
  95. @objc public var animationCurve = UIView.AnimationCurve.easeInOut
  96. ///< Keybaord transition animation option.
  97. @objc public var animationOption = UIView.AnimationOptions.layoutSubviews
  98. }
  99. /**
  100. A TextKeyboardManager object lets you get the system keyboard information,
  101. and track the keyboard visible/frame/transition.
  102. @discussion You should access this class in main thread.
  103. */
  104. public class TextKeyboardManager: NSObject {
  105. /// Get the keyboard window. nil if there's no keyboard window.
  106. @objc public var keyboardWindow: UIWindow? {
  107. guard let app = TextUtilities.sharedApplication else {
  108. return nil
  109. }
  110. for window in app.windows {
  111. if (_getKeyboardView(from: window) != nil) {
  112. return window
  113. }
  114. }
  115. let window: UIWindow? = app.keyWindow
  116. if (_getKeyboardView(from: window) != nil) {
  117. return window
  118. }
  119. var kbWindows = [UIWindow]()
  120. for window in app.windows {
  121. let windowName = NSStringFromClass(type(of: window))
  122. if _systemVersion < 9 {
  123. // UITextEffectsWindow
  124. if windowName.length == 19 && windowName.hasPrefix("UI") && windowName.hasSuffix("TextEffectsWindow") {
  125. kbWindows.append(window)
  126. }
  127. } else {
  128. // UIRemoteKeyboardWindow
  129. if windowName.length == 22 && windowName.hasPrefix("UI") && windowName.hasSuffix("RemoteKeyboardWindow") {
  130. kbWindows.append(window)
  131. }
  132. }
  133. }
  134. if kbWindows.count == 1 {
  135. return kbWindows.first
  136. }
  137. return nil
  138. }
  139. /// Get the keyboard view. nil if there's no keyboard view.
  140. @objc public var keyboardView: UIView? {
  141. let app: UIApplication? = TextUtilities.sharedApplication
  142. if app == nil {
  143. return nil
  144. }
  145. var window: UIWindow? = nil
  146. var view: UIView? = nil
  147. for window in app?.windows ?? [] {
  148. view = _getKeyboardView(from: window)
  149. if view != nil {
  150. return view
  151. }
  152. }
  153. window = app?.keyWindow
  154. view = _getKeyboardView(from: window)
  155. if view != nil {
  156. return view
  157. }
  158. return nil
  159. }
  160. /// Whether the keyboard is visible.
  161. @objc public var keyboardVisible: Bool {
  162. guard let window = keyboardWindow else {
  163. return false
  164. }
  165. guard let view = keyboardView else {
  166. return false
  167. }
  168. let rect: CGRect = window.bounds.intersection(view.frame)
  169. if rect.isNull {
  170. return false
  171. }
  172. if rect.isInfinite {
  173. return false
  174. }
  175. return rect.size.width > 0 && rect.size.height > 0
  176. }
  177. /// Get the keyboard frame. CGRectNull if there's no keyboard view.
  178. /// Use convertRect:toView: to convert frame to specified view.
  179. @objc public var keyboardFrame: CGRect {
  180. guard let keyboard = keyboardView else {
  181. return CGRect.null
  182. }
  183. var frame = CGRect.null
  184. if let window = keyboard.window {
  185. frame = window.convert(keyboard.frame, to: nil)
  186. } else {
  187. frame = keyboard.frame
  188. }
  189. return frame
  190. }
  191. @objc public class func startManager() -> Void {
  192. let _ = `default`
  193. }
  194. /// Get the default manager (returns nil in App Extension).
  195. @objc(defaultManager)
  196. public static let `default` = TextKeyboardManager()
  197. override private init() {
  198. observers = NSHashTable(options: [.weakMemory, .objectPointerPersonality], capacity: 0)
  199. super.init()
  200. NotificationCenter.default.addObserver(self, selector: #selector(self._keyboardFrameWillChange(_:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
  201. // for iPad (iOS 9)
  202. if _systemVersion >= 9 {
  203. NotificationCenter.default.addObserver(self, selector: #selector(self._keyboardFrameDidChange(_:)), name: UIResponder.keyboardDidChangeFrameNotification, object: nil)
  204. }
  205. }
  206. func _initFrameObserver() {
  207. guard let keyboardView = self.keyboardView else {
  208. return
  209. }
  210. weak var _self = self
  211. var observer: TextKeyboardViewFrameObserver? = TextKeyboardViewFrameObserver.observerForView(keyboardView)
  212. if observer == nil {
  213. observer = TextKeyboardViewFrameObserver()
  214. observer?.notifyBlock = { keyboard in
  215. _self!._keyboardFrameChanged(keyboard)
  216. }
  217. observer?.addTo(keyboardView: keyboardView)
  218. }
  219. }
  220. /**
  221. Add an observer to manager to get keyboard change information.
  222. This method makes a weak reference to the observer.
  223. @param observer An observer.
  224. This method will do nothing if the observer is nil, or already added.
  225. */
  226. @objc(addObserver:)
  227. public func add(observer: TextKeyboardObserver?) {
  228. guard let observer = observer else {
  229. return
  230. }
  231. observers.add(observer)
  232. }
  233. /**
  234. Remove an observer from manager.
  235. @param observer An observer.
  236. This method will do nothing if the observer is nil, or not in manager.
  237. */
  238. @objc(removeObserver:)
  239. public func remove(observer: TextKeyboardObserver?) {
  240. guard let observer = observer else {
  241. return
  242. }
  243. observers.remove(observer)
  244. }
  245. private var observers: NSHashTable<TextKeyboardObserver>
  246. private var fromFrame = CGRect.zero
  247. private var fromVisible = false
  248. private var notificationFromFrame = CGRect.zero
  249. private var notificationToFrame = CGRect.zero
  250. private var notificationDuration: TimeInterval = 0
  251. private var notificationCurve = UIView.AnimationCurve.linear
  252. private var hasNotification = false
  253. private var observedToFrame = CGRect.zero
  254. private var hasObservedChange = false
  255. private var lastIsNotification = false
  256. // MARK: - private
  257. private let _systemVersion = Double(UIDevice.current.systemVersion) ?? 0
  258. private func _getKeyboardView(from window: UIWindow?) -> UIView? {
  259. /*
  260. iOS 8:
  261. UITextEffectsWindow
  262. UIInputSetContainerView
  263. UIInputSetHostView << keyboard
  264. iOS 9:
  265. UIRemoteKeyboardWindow
  266. UIInputSetContainerView
  267. UIInputSetHostView << keyboard
  268. */
  269. guard let window = window else {
  270. return nil
  271. }
  272. // Get the window
  273. let windowName = NSStringFromClass(type(of: window))
  274. if _systemVersion < 9 {
  275. // UITextEffectsWindow
  276. if windowName.length != 19 {
  277. return nil
  278. }
  279. if !windowName.hasPrefix("UI") {
  280. return nil
  281. }
  282. if !windowName.hasSuffix("TextEffectsWindow") {
  283. return nil
  284. }
  285. } else {
  286. // UIRemoteKeyboardWindow
  287. if windowName.length != 22 {
  288. return nil
  289. }
  290. if !windowName.hasPrefix("UI") {
  291. return nil
  292. }
  293. if !windowName.hasSuffix("RemoteKeyboardWindow") {
  294. return nil
  295. }
  296. }
  297. // Get the view
  298. // UIInputSetContainerView
  299. for view: UIView in window.subviews {
  300. let viewName = NSStringFromClass(type(of: view))
  301. if viewName.length != 23 {
  302. continue
  303. }
  304. if !viewName.hasPrefix("UI") {
  305. continue
  306. }
  307. if !viewName.hasSuffix("InputSetContainerView") {
  308. continue
  309. }
  310. // UIInputSetHostView
  311. for subView: UIView in view.subviews {
  312. let subViewName = NSStringFromClass(type(of: subView))
  313. if subViewName.length != 18 {
  314. continue
  315. }
  316. if !subViewName.hasPrefix("UI") {
  317. continue
  318. }
  319. if !subViewName.hasSuffix("InputSetHostView") {
  320. continue
  321. }
  322. return subView
  323. }
  324. }
  325. return nil
  326. }
  327. @objc private func _keyboardFrameWillChange(_ notif: Notification?) {
  328. guard let notif = notif else {
  329. return
  330. }
  331. guard notif.name == UIResponder.keyboardWillChangeFrameNotification else {
  332. return
  333. }
  334. guard let info = notif.userInfo else {
  335. return
  336. }
  337. _initFrameObserver()
  338. let beforeValue = info[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue
  339. let afterValue = info[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue
  340. let curveNumber = info[UIResponder.keyboardAnimationCurveUserInfoKey] as! Int
  341. let durationNumber = info[UIResponder.keyboardAnimationDurationUserInfoKey] as! Double
  342. let before: CGRect = beforeValue?.cgRectValue ?? .zero
  343. let after: CGRect = afterValue?.cgRectValue ?? .zero
  344. let curve: UIView.AnimationCurve = UIView.AnimationCurve(rawValue: curveNumber)!
  345. let duration = durationNumber
  346. // ignore zero end frame
  347. if (after.size.width <= 0) && (after.size.height <= 0) {
  348. return
  349. }
  350. notificationFromFrame = before
  351. notificationToFrame = after
  352. notificationCurve = curve
  353. notificationDuration = duration
  354. hasNotification = true
  355. lastIsNotification = true
  356. NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(self._notifyAllObservers), object: nil)
  357. if duration == 0 {
  358. perform(#selector(self._notifyAllObservers), with: nil, afterDelay: 0, inModes: [.common])
  359. } else {
  360. _notifyAllObservers()
  361. }
  362. }
  363. @objc private func _keyboardFrameDidChange(_ notif: Notification?) {
  364. guard let notif = notif else {
  365. return
  366. }
  367. guard notif.name == UIResponder.keyboardDidChangeFrameNotification else {
  368. return
  369. }
  370. guard let info = notif.userInfo else {
  371. return
  372. }
  373. _initFrameObserver()
  374. let afterValue = info[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue
  375. let after: CGRect = afterValue?.cgRectValue ?? .zero
  376. // ignore zero end frame
  377. if (after.size.width <= 0) && (after.size.height <= 0) {
  378. return
  379. }
  380. notificationToFrame = after
  381. notificationCurve = UIView.AnimationCurve.easeInOut
  382. notificationDuration = 0
  383. hasNotification = true
  384. lastIsNotification = true
  385. NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(self._notifyAllObservers), object: nil)
  386. perform(#selector(self._notifyAllObservers), with: nil, afterDelay: 0, inModes: [.common])
  387. }
  388. private func _keyboardFrameChanged(_ keyboard: UIView?) {
  389. if keyboard != keyboardView {
  390. return
  391. }
  392. if let window = keyboard?.window {
  393. observedToFrame = window.convert(keyboard?.frame ?? CGRect.zero, to: nil)
  394. } else {
  395. observedToFrame = (keyboard?.frame)!
  396. }
  397. hasObservedChange = true
  398. lastIsNotification = false
  399. NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(self._notifyAllObservers), object: nil)
  400. perform(#selector(self._notifyAllObservers), with: nil, afterDelay: 0, inModes: [.common])
  401. }
  402. @objc private func _notifyAllObservers() {
  403. guard let app = TextUtilities.sharedApplication else {
  404. return
  405. }
  406. let keyboard: UIView? = keyboardView
  407. var window: UIWindow? = keyboard?.window
  408. if window == nil {
  409. window = app.keyWindow
  410. }
  411. if window == nil {
  412. window = app.windows.first
  413. }
  414. guard let w = window else {
  415. return
  416. }
  417. let trans = TextKeyboardTransition()
  418. // from
  419. if fromFrame.size.width == 0 && fromFrame.size.height == 0 {
  420. // first notify
  421. fromFrame.size.width = w.bounds.size.width
  422. fromFrame.size.height = trans.toFrame.size.height
  423. fromFrame.origin.x = trans.toFrame.origin.x
  424. fromFrame.origin.y = w.bounds.size.height
  425. }
  426. trans.fromFrame = fromFrame
  427. trans.fromVisible = fromVisible
  428. // to
  429. if lastIsNotification || (hasObservedChange && observedToFrame.equalTo(notificationToFrame)) {
  430. trans.toFrame = notificationToFrame
  431. trans.animationDuration = notificationDuration
  432. trans.animationCurve = notificationCurve
  433. trans.animationOption = UIView.AnimationOptions(rawValue: UInt(notificationCurve.rawValue << 16))
  434. } else {
  435. trans.toFrame = observedToFrame
  436. }
  437. if window != nil && trans.toFrame.size.width > 0 && trans.toFrame.size.height > 0 {
  438. let rect: CGRect = w.bounds.intersection(trans.toFrame)
  439. if !rect.isNull && !rect.isEmpty {
  440. trans.toVisible = true
  441. }
  442. }
  443. if !trans.toFrame.equalTo(fromFrame) {
  444. for (_, observer) in observers.objectEnumerator().enumerated() {
  445. guard let o = observer as? TextKeyboardObserver else {
  446. return
  447. }
  448. if o.responds(to: #selector(TextKeyboardObserver.keyboardChanged(with:))) {
  449. o.keyboardChanged!(with: trans)
  450. }
  451. }
  452. }
  453. hasNotification = false
  454. hasObservedChange = false
  455. fromFrame = trans.toFrame
  456. fromVisible = trans.toVisible
  457. }
  458. /**
  459. Convert rect to specified view or window.
  460. @param rect The frame rect.
  461. @param view A specified view or window (pass nil to convert for main window).
  462. @return The converted rect in specifeid view.
  463. */
  464. @objc(convertRect:toView:)
  465. public func convert(_ rect: CGRect, to view: UIView?) -> CGRect {
  466. var rect = rect
  467. guard let app = TextUtilities.sharedApplication else {
  468. return CGRect.zero
  469. }
  470. if rect.isNull {
  471. return rect
  472. }
  473. if rect.isInfinite {
  474. return rect
  475. }
  476. var mainWindow: UIWindow? = app.keyWindow
  477. if mainWindow == nil {
  478. mainWindow = app.windows.first
  479. }
  480. if mainWindow == nil {
  481. // no window ?!
  482. if view != nil {
  483. view?.convert(rect, from: nil)
  484. } else {
  485. return rect
  486. }
  487. }
  488. rect = mainWindow?.convert(rect, from: nil) ?? CGRect.zero
  489. if view == nil {
  490. return mainWindow?.convert(rect, to: nil) ?? CGRect.zero
  491. }
  492. if view == mainWindow {
  493. return rect
  494. }
  495. let toWindow = (view is UIWindow) ? (view as? UIWindow) : view?.window
  496. if mainWindow == nil || toWindow == nil {
  497. return mainWindow?.convert(rect, to: view) ?? CGRect.zero
  498. }
  499. if mainWindow == toWindow {
  500. return mainWindow?.convert(rect, to: view) ?? CGRect.zero
  501. }
  502. // in different window
  503. rect = mainWindow?.convert(rect, to: mainWindow) ?? CGRect.zero
  504. rect = toWindow?.convert(rect, from: mainWindow) ?? CGRect.zero
  505. rect = view?.convert(rect, from: toWindow) ?? CGRect.zero
  506. return rect
  507. }
  508. }
  509. extension UIApplication {
  510. private static let runOnce: Void = {
  511. TextKeyboardManager.startManager()
  512. }()
  513. override open var next: UIResponder? {
  514. // Called before applicationDidFinishLaunching
  515. UIApplication.runOnce
  516. return super.next
  517. }
  518. }