react-native-keyboard-controller 1.18.3 → 1.18.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,9 +9,9 @@ import androidx.core.view.WindowInsetsCompat
9
9
  import androidx.core.view.WindowInsetsControllerCompat
10
10
  import com.facebook.react.bridge.ReactApplicationContext
11
11
  import com.facebook.react.bridge.UiThreadUtil
12
- import com.reactnativekeyboardcontroller.extensions.rootView
13
12
  import com.reactnativekeyboardcontroller.log.Logger
14
13
  import com.reactnativekeyboardcontroller.views.EdgeToEdgeReactViewGroup
14
+ import com.reactnativekeyboardcontroller.views.EdgeToEdgeViewRegistry
15
15
  import java.lang.ref.WeakReference
16
16
 
17
17
  private val TAG = StatusBarManagerCompatModuleImpl::class.qualifiedName
@@ -117,8 +117,7 @@ class StatusBarManagerCompatModuleImpl(
117
117
 
118
118
  private fun isEnabled(): Boolean = view()?.active ?: false
119
119
 
120
- private fun view(): EdgeToEdgeReactViewGroup? =
121
- mReactContext.rootView?.findViewWithTag<EdgeToEdgeReactViewGroup>(EdgeToEdgeReactViewGroup.VIEW_TAG)
120
+ private fun view(): EdgeToEdgeReactViewGroup? = EdgeToEdgeViewRegistry.get()
122
121
 
123
122
  companion object {
124
123
  const val NAME = "StatusBarManager"
@@ -19,9 +19,20 @@ import com.reactnativekeyboardcontroller.listeners.KeyboardAnimationCallback
19
19
  import com.reactnativekeyboardcontroller.listeners.KeyboardAnimationCallbackConfig
20
20
  import com.reactnativekeyboardcontroller.log.Logger
21
21
  import com.reactnativekeyboardcontroller.modal.ModalAttachedWatcher
22
+ import java.lang.ref.WeakReference
22
23
 
23
24
  private val TAG = EdgeToEdgeReactViewGroup::class.qualifiedName
24
25
 
26
+ object EdgeToEdgeViewRegistry {
27
+ private var lastCreatedView: WeakReference<EdgeToEdgeReactViewGroup>? = null
28
+
29
+ fun register(view: EdgeToEdgeReactViewGroup) {
30
+ lastCreatedView = WeakReference(view)
31
+ }
32
+
33
+ fun get(): EdgeToEdgeReactViewGroup? = lastCreatedView?.get()
34
+ }
35
+
25
36
  @Suppress("detekt:TooManyFunctions")
26
37
  @SuppressLint("ViewConstructor")
27
38
  class EdgeToEdgeReactViewGroup(
@@ -58,7 +69,7 @@ class EdgeToEdgeReactViewGroup(
58
69
  private val modalAttachedWatcher = ModalAttachedWatcher(this, reactContext, config, ::getKeyboardCallback)
59
70
 
60
71
  init {
61
- tag = VIEW_TAG
72
+ EdgeToEdgeViewRegistry.register(this)
62
73
  }
63
74
 
64
75
  // region View life cycles
@@ -241,8 +252,4 @@ class EdgeToEdgeReactViewGroup(
241
252
  }
242
253
  }
243
254
  // endregion
244
-
245
- companion object {
246
- val VIEW_TAG = EdgeToEdgeReactViewGroup::class.simpleName
247
- }
248
255
  }
@@ -66,16 +66,18 @@ final class KeyboardTrackingView: UIView {
66
66
 
67
67
  translatesAutoresizingMaskIntoConstraints = false
68
68
 
69
- if #available(iOS 17.0, *) {
70
- rootView.keyboardLayoutGuide.usesBottomSafeArea = false
69
+ if #available(iOS 15.0, *) {
70
+ if #available(iOS 17.0, *) {
71
+ rootView.keyboardLayoutGuide.usesBottomSafeArea = false
72
+ }
73
+
74
+ NSLayoutConstraint.activate([
75
+ leadingAnchor.constraint(equalTo: rootView.leadingAnchor, constant: 0),
76
+ trailingAnchor.constraint(equalTo: rootView.trailingAnchor, constant: 0),
77
+ bottomAnchor.constraint(equalTo: rootView.keyboardLayoutGuide.topAnchor, constant: 0),
78
+ heightAnchor.constraint(equalToConstant: 0),
79
+ ])
71
80
  }
72
-
73
- NSLayoutConstraint.activate([
74
- leadingAnchor.constraint(equalTo: rootView.leadingAnchor, constant: 0),
75
- trailingAnchor.constraint(equalTo: rootView.trailingAnchor, constant: 0),
76
- bottomAnchor.constraint(equalTo: rootView.keyboardLayoutGuide.topAnchor, constant: 0),
77
- heightAnchor.constraint(equalToConstant: 0),
78
- ])
79
81
  }
80
82
 
81
83
  @objc private func keyboardWillAppear(_ notification: Notification) {
@@ -111,6 +113,11 @@ final class KeyboardTrackingView: UIView {
111
113
 
112
114
  // for `keyboardLayoutGuide` case we can just read keyboard position directly - no interpolation needed
113
115
  if #available(iOS 26.0, *) {
116
+ // when we are the top position KVO takes `inputAccessoryView` into consideration,
117
+ // so we handle it here
118
+ if keyboardPosition == keyboardHeight {
119
+ return keyboardPosition - KeyboardAreaExtender.shared.offset
120
+ }
114
121
  return keyboardPosition
115
122
  }
116
123
 
@@ -127,6 +134,6 @@ final class KeyboardTrackingView: UIView {
127
134
  currentValue: keyboardPosition
128
135
  )
129
136
 
130
- return position
137
+ return position - KeyboardAreaExtender.shared.offset
131
138
  }
132
139
  }
@@ -0,0 +1,21 @@
1
+ //
2
+ // KeyboardMovementObserver+Animation.swift
3
+ // Pods
4
+ //
5
+ // Created by Kiryl Ziusko on 07/08/2025.
6
+ //
7
+
8
+ extension KeyboardMovementObserver {
9
+ func initializeAnimation(fromValue: Double, toValue: Double) {
10
+ for key in ["position", "opacity"] {
11
+ if let keyboardAnimation = keyboardTrackingView.view?.layer.presentation()?.animation(forKey: key) {
12
+ if let springAnimation = keyboardAnimation as? CASpringAnimation {
13
+ animation = SpringAnimation(animation: springAnimation, fromValue: fromValue, toValue: toValue)
14
+ } else if let basicAnimation = keyboardAnimation as? CABasicAnimation {
15
+ animation = TimingAnimation(animation: basicAnimation, fromValue: fromValue, toValue: toValue)
16
+ }
17
+ return
18
+ }
19
+ }
20
+ }
21
+ }
@@ -0,0 +1,56 @@
1
+ //
2
+ // KeyboardMovementObserver+Interactive.swift
3
+ // Pods
4
+ //
5
+ // Created by Kiryl Ziusko on 07/08/2025.
6
+ //
7
+
8
+ extension KeyboardMovementObserver {
9
+ func setupKVObserver() {
10
+ guard interactiveKeyboardObserver == nil, let view = keyboardTrackingView.view else { return }
11
+
12
+ interactiveKeyboardObserver = view.observe(\.center, options: .new) { [weak self] _, change in
13
+ guard let self = self, let changeValue = change.newValue else { return }
14
+
15
+ self.keyboardDidMoveInteractively(changeValue: changeValue)
16
+ }
17
+ }
18
+
19
+ func removeKVObserver() {
20
+ interactiveKeyboardObserver?.invalidate()
21
+ interactiveKeyboardObserver = nil
22
+ }
23
+
24
+ private func keyboardDidMoveInteractively(changeValue: CGPoint) {
25
+ if UIResponder.isKeyboardPreloading {
26
+ return
27
+ }
28
+ // if we are currently animating keyboard -> we need to ignore values from KVO
29
+ if !displayLink.isPaused {
30
+ return
31
+ }
32
+
33
+ let position = keyboardTrackingView.interactive(point: changeValue)
34
+
35
+ if position == KeyboardTrackingView.invalidPosition {
36
+ return
37
+ }
38
+
39
+ if position == 0 {
40
+ // it will be triggered before `keyboardWillDisappear` and
41
+ // we don't need to trigger `onInteractive` handler for that
42
+ // since it will be handled in `keyboardWillDisappear` function
43
+ return
44
+ }
45
+
46
+ prevKeyboardPosition = position
47
+
48
+ onEvent(
49
+ "onKeyboardMoveInteractive",
50
+ position as NSNumber,
51
+ position / CGFloat(keyboardHeight) as NSNumber,
52
+ -1,
53
+ tag
54
+ )
55
+ }
56
+ }
@@ -0,0 +1,47 @@
1
+ //
2
+ // KeyboardMovementObserver+Lifecycle.swift
3
+ // Pods
4
+ //
5
+ // Created by Kiryl Ziusko on 07/08/2025.
6
+ //
7
+
8
+ public extension KeyboardMovementObserver {
9
+ @objc func mount() {
10
+ if isMounted {
11
+ return
12
+ }
13
+
14
+ isMounted = true
15
+
16
+ NotificationCenter.default.addObserver(
17
+ self,
18
+ selector: #selector(keyboardWillDisappear),
19
+ name: UIResponder.keyboardWillHideNotification,
20
+ object: nil
21
+ )
22
+ NotificationCenter.default.addObserver(
23
+ self,
24
+ selector: #selector(keyboardWillAppear),
25
+ name: UIResponder.keyboardWillShowNotification,
26
+ object: nil
27
+ )
28
+ NotificationCenter.default.addObserver(
29
+ self,
30
+ selector: #selector(keyboardDidAppear),
31
+ name: UIResponder.keyboardDidShowNotification,
32
+ object: nil
33
+ )
34
+ NotificationCenter.default.addObserver(
35
+ self,
36
+ selector: #selector(keyboardDidDisappear),
37
+ name: UIResponder.keyboardDidHideNotification,
38
+ object: nil
39
+ )
40
+ }
41
+
42
+ @objc func unmount() {
43
+ isMounted = false
44
+ // swiftlint:disable:next notification_center_detachment
45
+ NotificationCenter.default.removeObserver(self)
46
+ }
47
+ }
@@ -0,0 +1,88 @@
1
+ //
2
+ // KeyboardMovementObserver+Listeners.swift
3
+ // Pods
4
+ //
5
+ // Created by Kiryl Ziusko on 07/08/2025.
6
+ //
7
+
8
+ extension KeyboardMovementObserver {
9
+ @objc func keyboardWillAppear(_ notification: Notification) {
10
+ guard !KeyboardEventsIgnorer.shared.shouldIgnore, !UIResponder.isKeyboardPreloading else { return }
11
+
12
+ let (duration, frame) = notification.keyboardMetaData()
13
+ if let keyboardFrame = frame {
14
+ tag = UIResponder.current.reactViewTag
15
+ let keyboardHeight = keyboardFrame.cgRectValue.size.height
16
+ self.keyboardHeight = keyboardHeight
17
+ self.duration = duration
18
+ didShowDeadline = Date.currentTimeStamp + Int64(duration)
19
+
20
+ onRequestAnimation()
21
+ onEvent("onKeyboardMoveStart", Float(self.keyboardHeight) as NSNumber, 1, duration as NSNumber, tag)
22
+ onNotify("KeyboardController::keyboardWillShow", buildEventParams(self.keyboardHeight, duration, tag))
23
+
24
+ setupKeyboardWatcher()
25
+ initializeAnimation(fromValue: prevKeyboardPosition, toValue: self.keyboardHeight)
26
+ }
27
+ }
28
+
29
+ @objc func keyboardWillDisappear(_ notification: Notification) {
30
+ guard !UIResponder.isKeyboardPreloading else { return }
31
+ let (duration, _) = notification.keyboardMetaData()
32
+ tag = UIResponder.current.reactViewTag
33
+ self.duration = duration
34
+
35
+ onRequestAnimation()
36
+ onEvent("onKeyboardMoveStart", 0, 0, duration as NSNumber, tag)
37
+ onNotify("KeyboardController::keyboardWillHide", buildEventParams(0, duration, tag))
38
+
39
+ setupKeyboardWatcher()
40
+ removeKVObserver()
41
+ initializeAnimation(fromValue: prevKeyboardPosition, toValue: 0)
42
+ }
43
+
44
+ @objc func keyboardDidAppear(_ notification: Notification) {
45
+ guard !UIResponder.isKeyboardPreloading else { return }
46
+
47
+ let timestamp = Date.currentTimeStamp
48
+ let (duration, frame) = notification.keyboardMetaData()
49
+ if let keyboardFrame = frame {
50
+ let (position, _) = keyboardTrackingView.view.frameTransitionInWindow
51
+ let keyboardHeight = keyboardFrame.cgRectValue.size.height
52
+ tag = UIResponder.current.reactViewTag
53
+ self.keyboardHeight = keyboardHeight
54
+
55
+ guard !KeyboardEventsIgnorer.shared.shouldIgnore else {
56
+ KeyboardEventsIgnorer.shared.shouldIgnoreKeyboardEvents = false
57
+ return
58
+ }
59
+
60
+ // if the event is caught in between it's highly likely that it could be a "resize" event
61
+ // so we just read actual keyboard frame value in this case
62
+ let height = timestamp >= didShowDeadline ? self.keyboardHeight : position - KeyboardAreaExtender.shared.offset
63
+ // always limit progress to the maximum possible value
64
+ let progress = min(height / self.keyboardHeight, 1.0)
65
+
66
+ onCancelAnimation()
67
+ onEvent("onKeyboardMoveEnd", height as NSNumber, progress as NSNumber, duration as NSNumber, tag)
68
+ onNotify("KeyboardController::keyboardDidShow", buildEventParams(height, duration, tag))
69
+
70
+ removeKeyboardWatcher()
71
+ setupKVObserver()
72
+ animation = nil
73
+ }
74
+ }
75
+
76
+ @objc func keyboardDidDisappear(_ notification: Notification) {
77
+ guard !UIResponder.isKeyboardPreloading else { return }
78
+ let (duration, _) = notification.keyboardMetaData()
79
+ tag = UIResponder.current.reactViewTag
80
+
81
+ onCancelAnimation()
82
+ onEvent("onKeyboardMoveEnd", 0 as NSNumber, 0, duration as NSNumber, tag)
83
+ onNotify("KeyboardController::keyboardDidHide", buildEventParams(0, duration, tag))
84
+
85
+ removeKeyboardWatcher()
86
+ animation = nil
87
+ }
88
+ }
@@ -0,0 +1,72 @@
1
+ //
2
+ // KeyboardMovementObserver+Watcher.swift
3
+ // Pods
4
+ //
5
+ // Created by Kiryl Ziusko on 07/08/2025.
6
+ //
7
+
8
+ extension KeyboardMovementObserver {
9
+ @objc func setupKeyboardWatcher() {
10
+ // sometimes `will` events can be called multiple times.
11
+ // To avoid double re-creation of listener we are adding this condition
12
+ // (if active link is present, then no need to re-setup a listener)
13
+ if !displayLink.isPaused {
14
+ return
15
+ }
16
+
17
+ displayLink.isPaused = false
18
+ }
19
+
20
+ @objc func removeKeyboardWatcher() {
21
+ displayLink.isPaused = true
22
+ }
23
+
24
+ @objc func updateKeyboardFrame(link: CADisplayLink) {
25
+ if keyboardTrackingView.view == nil {
26
+ return
27
+ }
28
+
29
+ let (visibleKeyboardHeight, keyboardFrameY) = keyboardTrackingView.view.frameTransitionInWindow
30
+ var keyboardPosition = visibleKeyboardHeight - KeyboardAreaExtender.shared.offset
31
+
32
+ if keyboardPosition == prevKeyboardPosition || keyboardFrameY == 0 {
33
+ return
34
+ }
35
+
36
+ if animation == nil {
37
+ initializeAnimation(fromValue: prevKeyboardPosition, toValue: keyboardHeight)
38
+ }
39
+
40
+ prevKeyboardPosition = keyboardPosition
41
+
42
+ if let animation = animation {
43
+ let baseDuration = animation.timingAt(value: keyboardPosition)
44
+
45
+ #if targetEnvironment(simulator)
46
+ // on iOS simulator we can not use static interval
47
+ // (from my observation from frame to frame we may have different delays)
48
+ // so for now we use approximation - we add a difference as
49
+ // beginTime - keyboardEventTime (but only in 0..0.016 range)
50
+ // and it gives satisfactory results (better than static delays)
51
+ let duration = baseDuration + animation.diff
52
+ #else
53
+ // 2 frames because we read previous frame, but need to calculate the next frame
54
+ let duration = baseDuration + link.duration * 2
55
+ #endif
56
+
57
+ let position = CGFloat(animation.valueAt(time: duration))
58
+ // handles a case when final frame has final destination (i. e. 0 or 291)
59
+ // but CASpringAnimation can never get to this final destination
60
+ let race: (CGFloat, CGFloat) -> CGFloat = animation.isIncreasing ? max : min
61
+ keyboardPosition = race(position, keyboardPosition)
62
+ }
63
+
64
+ onEvent(
65
+ "onKeyboardMove",
66
+ keyboardPosition as NSNumber,
67
+ keyboardPosition / CGFloat(keyboardHeight) as NSNumber,
68
+ duration as NSNumber,
69
+ tag
70
+ )
71
+ }
72
+ }
@@ -0,0 +1,63 @@
1
+ //
2
+ // KeyboardMovementObserver.swift
3
+ // KeyboardController
4
+ //
5
+ // Created by Kiryl Ziusko on 2.08.22.
6
+ // Copyright © 2022 Facebook. All rights reserved.
7
+ //
8
+
9
+ import Foundation
10
+ import UIKit
11
+
12
+ @objc(KeyboardMovementObserver)
13
+ public class KeyboardMovementObserver: NSObject {
14
+ // class members
15
+ var onEvent: (NSString, NSNumber, NSNumber, NSNumber, NSNumber) -> Void
16
+ var onNotify: (String, Any) -> Void
17
+ // animation
18
+ var onRequestAnimation: () -> Void
19
+ var onCancelAnimation: () -> Void
20
+ // progress tracker
21
+ var keyboardTrackingView = KeyboardTrackingView()
22
+ var animation: KeyboardAnimation?
23
+
24
+ var prevKeyboardPosition = 0.0
25
+ var displayLink: CADisplayLink!
26
+ var interactiveKeyboardObserver: NSKeyValueObservation?
27
+ var isMounted = false
28
+ // state variables
29
+ private var _keyboardHeight: CGFloat = 0.0
30
+ var keyboardHeight: CGFloat {
31
+ get { _keyboardHeight - KeyboardAreaExtender.shared.offset }
32
+ set { _keyboardHeight = newValue }
33
+ }
34
+
35
+ var duration = 0
36
+ var tag: NSNumber = -1
37
+ var didShowDeadline: Int64 = 0
38
+
39
+ @objc public init(
40
+ handler: @escaping (NSString, NSNumber, NSNumber, NSNumber, NSNumber) -> Void,
41
+ onNotify: @escaping (String, Any) -> Void,
42
+ onRequestAnimation: @escaping () -> Void,
43
+ onCancelAnimation: @escaping () -> Void
44
+ ) {
45
+ onEvent = handler
46
+ self.onNotify = onNotify
47
+ self.onRequestAnimation = onRequestAnimation
48
+ self.onCancelAnimation = onCancelAnimation
49
+
50
+ super.init()
51
+
52
+ displayLink = CADisplayLink(target: self, selector: #selector(updateKeyboardFrame))
53
+ displayLink.preferredFramesPerSecond = 120 // will fallback to 60 fps for devices without Pro Motion display
54
+ displayLink.add(to: .main, forMode: .common)
55
+ displayLink.isPaused = true
56
+ }
57
+
58
+ deinit {
59
+ displayLink.invalidate()
60
+ displayLink = nil
61
+ NotificationCenter.default.removeObserver(self)
62
+ }
63
+ }
@@ -10,11 +10,13 @@ import UIKit
10
10
  @objc
11
11
  public class KeyboardExtenderContainerView: NSObject {
12
12
  @objc public static func create(frame: CGRect, contentView: UIView) -> UIView {
13
- if #available(iOS 26.0, *) {
14
- return ModernContainerView(frame: frame, contentView: contentView)
15
- } else {
16
- return LegacyContainerView(frame: frame, contentView: contentView)
17
- }
13
+ #if canImport(UIKit.UIGlassEffect)
14
+ if #available(iOS 26.0, *) {
15
+ return ModernContainerView(frame: frame, contentView: contentView)
16
+ }
17
+ #endif
18
+
19
+ return LegacyContainerView(frame: frame, contentView: contentView)
18
20
  }
19
21
  }
20
22
 
@@ -86,21 +88,23 @@ private class ModernContainerView: BaseContainerView {
86
88
  }
87
89
 
88
90
  private func setupVisualEffect() {
89
- guard let glassEffectClass = NSClassFromString("UIGlassEffect") as? UIVisualEffect.Type else { return }
90
-
91
- let isDark = FocusedInputHolder.shared.get()?.keyboardAppearanceValue == "dark"
92
- let glassEffect = glassEffectClass.init()
93
- let color = isDark ? UIColor.black.withAlphaComponent(0.3) : UIColor.gray.withAlphaComponent(0.3)
94
- glassEffect.setValue(color, forKey: "tintColor")
95
- glassEffect.setValue(true, forKey: "interactive")
96
-
97
- visualEffectView = UIVisualEffectView(effect: glassEffect)
98
- visualEffectView?.overrideUserInterfaceStyle = isDark ? .dark : .light
99
-
100
- if let visualEffectView = visualEffectView {
101
- visualEffectView.contentView.addSubview(contentView)
102
- addSubview(visualEffectView)
103
- }
91
+ #if canImport(UIKit.UIGlassEffect)
92
+ let isDark = FocusedInputHolder.shared.get()?.keyboardAppearanceValue == "dark"
93
+ let glassEffect = UIGlassEffect()
94
+ let color =
95
+ isDark ? UIColor.black.withAlphaComponent(0.3) : UIColor.gray.withAlphaComponent(0.3)
96
+ glassEffect.tintColor = color
97
+ glassEffect.isInteractive = true
98
+
99
+ visualEffectView = UIVisualEffectView(effect: glassEffect)
100
+ visualEffectView?.overrideUserInterfaceStyle = isDark ? .dark : .light
101
+ visualEffectView?.cornerConfiguration = .capsule()
102
+
103
+ if let visualEffectView = visualEffectView {
104
+ visualEffectView.contentView.addSubview(contentView)
105
+ addSubview(visualEffectView)
106
+ }
107
+ #endif
104
108
  }
105
109
 
106
110
  override func updateContentFrame(desiredHeight: CGFloat) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-keyboard-controller",
3
- "version": "1.18.3",
3
+ "version": "1.18.4",
4
4
  "description": "Keyboard manager which works in identical way on both iOS and Android",
5
5
  "main": "lib/commonjs/index",
6
6
  "module": "lib/module/index",
@@ -1,307 +0,0 @@
1
- //
2
- // KeyboardMovementObserver.swift
3
- // KeyboardController
4
- //
5
- // Created by Kiryl Ziusko on 2.08.22.
6
- // Copyright © 2022 Facebook. All rights reserved.
7
- //
8
-
9
- import Foundation
10
- import UIKit
11
-
12
- @objc(KeyboardMovementObserver)
13
- public class KeyboardMovementObserver: NSObject {
14
- // class members
15
- var onEvent: (NSString, NSNumber, NSNumber, NSNumber, NSNumber) -> Void
16
- var onNotify: (String, Any) -> Void
17
- // animation
18
- var onRequestAnimation: () -> Void
19
- var onCancelAnimation: () -> Void
20
- // progress tracker
21
- private var keyboardTrackingView = KeyboardTrackingView()
22
-
23
- private var prevKeyboardPosition = 0.0
24
- private var displayLink: CADisplayLink!
25
- private var interactiveKeyboardObserver: NSKeyValueObservation?
26
- private var isMounted = false
27
- // state variables
28
- private var _keyboardHeight: CGFloat = 0.0
29
- private var keyboardHeight: CGFloat {
30
- get { _keyboardHeight - KeyboardAreaExtender.shared.offset }
31
- set { _keyboardHeight = newValue }
32
- }
33
-
34
- private var duration = 0
35
- private var tag: NSNumber = -1
36
- private var animation: KeyboardAnimation?
37
- private var didShowDeadline: Int64 = 0
38
-
39
- @objc public init(
40
- handler: @escaping (NSString, NSNumber, NSNumber, NSNumber, NSNumber) -> Void,
41
- onNotify: @escaping (String, Any) -> Void,
42
- onRequestAnimation: @escaping () -> Void,
43
- onCancelAnimation: @escaping () -> Void
44
- ) {
45
- onEvent = handler
46
- self.onNotify = onNotify
47
- self.onRequestAnimation = onRequestAnimation
48
- self.onCancelAnimation = onCancelAnimation
49
-
50
- super.init()
51
-
52
- displayLink = CADisplayLink(target: self, selector: #selector(updateKeyboardFrame))
53
- displayLink.preferredFramesPerSecond = 120 // will fallback to 60 fps for devices without Pro Motion display
54
- displayLink.add(to: .main, forMode: .common)
55
- displayLink.isPaused = true
56
- }
57
-
58
- deinit {
59
- displayLink.invalidate()
60
- displayLink = nil
61
- }
62
-
63
- @objc public func mount() {
64
- if isMounted {
65
- return
66
- }
67
-
68
- isMounted = true
69
-
70
- NotificationCenter.default.addObserver(
71
- self,
72
- selector: #selector(keyboardWillDisappear),
73
- name: UIResponder.keyboardWillHideNotification,
74
- object: nil
75
- )
76
- NotificationCenter.default.addObserver(
77
- self,
78
- selector: #selector(keyboardWillAppear),
79
- name: UIResponder.keyboardWillShowNotification,
80
- object: nil
81
- )
82
- NotificationCenter.default.addObserver(
83
- self,
84
- selector: #selector(keyboardDidAppear),
85
- name: UIResponder.keyboardDidShowNotification,
86
- object: nil
87
- )
88
- NotificationCenter.default.addObserver(
89
- self,
90
- selector: #selector(keyboardDidDisappear),
91
- name: UIResponder.keyboardDidHideNotification,
92
- object: nil
93
- )
94
- }
95
-
96
- private func setupKVObserver() {
97
- guard interactiveKeyboardObserver == nil, let view = keyboardTrackingView.view else { return }
98
-
99
- interactiveKeyboardObserver = view.observe(\.center, options: .new) { [weak self] _, change in
100
- guard let self = self, let changeValue = change.newValue else { return }
101
-
102
- self.keyboardDidMoveInteractively(changeValue: changeValue)
103
- }
104
- }
105
-
106
- private func removeKVObserver() {
107
- interactiveKeyboardObserver?.invalidate()
108
- interactiveKeyboardObserver = nil
109
- }
110
-
111
- private func keyboardDidMoveInteractively(changeValue: CGPoint) {
112
- if UIResponder.isKeyboardPreloading {
113
- return
114
- }
115
- // if we are currently animating keyboard -> we need to ignore values from KVO
116
- if !displayLink.isPaused {
117
- return
118
- }
119
-
120
- let interactive = keyboardTrackingView.interactive(point: changeValue)
121
-
122
- if interactive == KeyboardTrackingView.invalidPosition {
123
- return
124
- }
125
- let position = interactive - KeyboardAreaExtender.shared.offset
126
-
127
- if position == 0 {
128
- // it will be triggered before `keyboardWillDisappear` and
129
- // we don't need to trigger `onInteractive` handler for that
130
- // since it will be handled in `keyboardWillDisappear` function
131
- return
132
- }
133
-
134
- prevKeyboardPosition = position
135
-
136
- onEvent(
137
- "onKeyboardMoveInteractive",
138
- position as NSNumber,
139
- position / CGFloat(keyboardHeight) as NSNumber,
140
- -1,
141
- tag
142
- )
143
- }
144
-
145
- @objc public func unmount() {
146
- isMounted = false
147
- // swiftlint:disable:next notification_center_detachment
148
- NotificationCenter.default.removeObserver(self)
149
- }
150
-
151
- @objc func keyboardWillAppear(_ notification: Notification) {
152
- guard !KeyboardEventsIgnorer.shared.shouldIgnore, !UIResponder.isKeyboardPreloading else { return }
153
-
154
- let (duration, frame) = notification.keyboardMetaData()
155
- if let keyboardFrame = frame {
156
- tag = UIResponder.current.reactViewTag
157
- let keyboardHeight = keyboardFrame.cgRectValue.size.height
158
- self.keyboardHeight = keyboardHeight
159
- self.duration = duration
160
- didShowDeadline = Date.currentTimeStamp + Int64(duration)
161
-
162
- onRequestAnimation()
163
- onEvent("onKeyboardMoveStart", Float(self.keyboardHeight) as NSNumber, 1, duration as NSNumber, tag)
164
- onNotify("KeyboardController::keyboardWillShow", buildEventParams(self.keyboardHeight, duration, tag))
165
-
166
- setupKeyboardWatcher()
167
- initializeAnimation(fromValue: prevKeyboardPosition, toValue: self.keyboardHeight)
168
- }
169
- }
170
-
171
- @objc func keyboardWillDisappear(_ notification: Notification) {
172
- guard !UIResponder.isKeyboardPreloading else { return }
173
- let (duration, _) = notification.keyboardMetaData()
174
- tag = UIResponder.current.reactViewTag
175
- self.duration = duration
176
-
177
- onRequestAnimation()
178
- onEvent("onKeyboardMoveStart", 0, 0, duration as NSNumber, tag)
179
- onNotify("KeyboardController::keyboardWillHide", buildEventParams(0, duration, tag))
180
-
181
- setupKeyboardWatcher()
182
- removeKVObserver()
183
- initializeAnimation(fromValue: prevKeyboardPosition, toValue: 0)
184
- }
185
-
186
- @objc func keyboardDidAppear(_ notification: Notification) {
187
- guard !UIResponder.isKeyboardPreloading else { return }
188
-
189
- let timestamp = Date.currentTimeStamp
190
- let (duration, frame) = notification.keyboardMetaData()
191
- if let keyboardFrame = frame {
192
- let (position, _) = keyboardTrackingView.view.frameTransitionInWindow
193
- let keyboardHeight = keyboardFrame.cgRectValue.size.height
194
- tag = UIResponder.current.reactViewTag
195
- self.keyboardHeight = keyboardHeight
196
-
197
- guard !KeyboardEventsIgnorer.shared.shouldIgnore else {
198
- KeyboardEventsIgnorer.shared.shouldIgnoreKeyboardEvents = false
199
- return
200
- }
201
-
202
- // if the event is caught in between it's highly likely that it could be a "resize" event
203
- // so we just read actual keyboard frame value in this case
204
- let height = timestamp >= didShowDeadline ? self.keyboardHeight : position - KeyboardAreaExtender.shared.offset
205
- // always limit progress to the maximum possible value
206
- let progress = min(height / self.keyboardHeight, 1.0)
207
-
208
- onCancelAnimation()
209
- onEvent("onKeyboardMoveEnd", height as NSNumber, progress as NSNumber, duration as NSNumber, tag)
210
- onNotify("KeyboardController::keyboardDidShow", buildEventParams(height, duration, tag))
211
-
212
- removeKeyboardWatcher()
213
- setupKVObserver()
214
- animation = nil
215
- }
216
- }
217
-
218
- @objc func keyboardDidDisappear(_ notification: Notification) {
219
- guard !UIResponder.isKeyboardPreloading else { return }
220
- let (duration, _) = notification.keyboardMetaData()
221
- tag = UIResponder.current.reactViewTag
222
-
223
- onCancelAnimation()
224
- onEvent("onKeyboardMoveEnd", 0 as NSNumber, 0, duration as NSNumber, tag)
225
- onNotify("KeyboardController::keyboardDidHide", buildEventParams(0, duration, tag))
226
-
227
- removeKeyboardWatcher()
228
- animation = nil
229
- }
230
-
231
- @objc func setupKeyboardWatcher() {
232
- // sometimes `will` events can be called multiple times.
233
- // To avoid double re-creation of listener we are adding this condition
234
- // (if active link is present, then no need to re-setup a listener)
235
- if !displayLink.isPaused {
236
- return
237
- }
238
-
239
- displayLink.isPaused = false
240
- }
241
-
242
- @objc func removeKeyboardWatcher() {
243
- displayLink.isPaused = true
244
- }
245
-
246
- func initializeAnimation(fromValue: Double, toValue: Double) {
247
- for key in ["position", "opacity"] {
248
- if let keyboardAnimation = keyboardTrackingView.view?.layer.presentation()?.animation(forKey: key) {
249
- if let springAnimation = keyboardAnimation as? CASpringAnimation {
250
- animation = SpringAnimation(animation: springAnimation, fromValue: fromValue, toValue: toValue)
251
- } else if let basicAnimation = keyboardAnimation as? CABasicAnimation {
252
- animation = TimingAnimation(animation: basicAnimation, fromValue: fromValue, toValue: toValue)
253
- }
254
- return
255
- }
256
- }
257
- }
258
-
259
- @objc func updateKeyboardFrame(link: CADisplayLink) {
260
- if keyboardTrackingView.view == nil {
261
- return
262
- }
263
-
264
- let (visibleKeyboardHeight, keyboardFrameY) = keyboardTrackingView.view.frameTransitionInWindow
265
- var keyboardPosition = visibleKeyboardHeight - KeyboardAreaExtender.shared.offset
266
-
267
- if keyboardPosition == prevKeyboardPosition || keyboardFrameY == 0 {
268
- return
269
- }
270
-
271
- if animation == nil {
272
- initializeAnimation(fromValue: prevKeyboardPosition, toValue: keyboardHeight)
273
- }
274
-
275
- prevKeyboardPosition = keyboardPosition
276
-
277
- if let animation = animation {
278
- let baseDuration = animation.timingAt(value: keyboardPosition)
279
-
280
- #if targetEnvironment(simulator)
281
- // on iOS simulator we can not use static interval
282
- // (from my observation from frame to frame we may have different delays)
283
- // so for now we use approximation - we add a difference as
284
- // beginTime - keyboardEventTime (but only in 0..0.016 range)
285
- // and it gives satisfactory results (better than static delays)
286
- let duration = baseDuration + animation.diff
287
- #else
288
- // 2 frames because we read previous frame, but need to calculate the next frame
289
- let duration = baseDuration + link.duration * 2
290
- #endif
291
-
292
- let position = CGFloat(animation.valueAt(time: duration))
293
- // handles a case when final frame has final destination (i. e. 0 or 291)
294
- // but CASpringAnimation can never get to this final destination
295
- let race: (CGFloat, CGFloat) -> CGFloat = animation.isIncreasing ? max : min
296
- keyboardPosition = race(position, keyboardPosition)
297
- }
298
-
299
- onEvent(
300
- "onKeyboardMove",
301
- keyboardPosition as NSNumber,
302
- keyboardPosition / CGFloat(keyboardHeight) as NSNumber,
303
- duration as NSNumber,
304
- tag
305
- )
306
- }
307
- }