@nativescript/input-accessory 1.0.0 → 1.0.3
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.
- package/CHANGELOG.md +19 -0
- package/index.ios.js +11 -2
- package/index.ios.js.map +1 -1
- package/package.json +1 -1
- package/platforms/ios/src/KeyboardTrackingView.swift +242 -29
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
## 1.0.2 (2026-05-20)
|
|
2
|
+
|
|
3
|
+
### 🩹 Fixes
|
|
4
|
+
|
|
5
|
+
- **input-accessory:** improved positioning and state handling ([#664](https://github.com/NativeScript/plugins/pull/664))
|
|
6
|
+
|
|
7
|
+
### ❤️ Thank You
|
|
8
|
+
|
|
9
|
+
- Nathan Walker
|
|
10
|
+
|
|
11
|
+
# 1.0.0 (2026-02-26)
|
|
12
|
+
|
|
13
|
+
### 🚀 Features
|
|
14
|
+
|
|
15
|
+
- input accessory ([#659](https://github.com/NativeScript/plugins/pull/659))
|
|
16
|
+
|
|
17
|
+
### ❤️ Thank You
|
|
18
|
+
|
|
19
|
+
- Nathan Walker
|
package/index.ios.js
CHANGED
|
@@ -44,6 +44,14 @@ export class InputAccessoryManager extends InputAccessoryManagerBase {
|
|
|
44
44
|
bottom: 10,
|
|
45
45
|
right: 10,
|
|
46
46
|
});
|
|
47
|
+
nativeTextView.autocorrectionType = 2 /* UITextAutocorrectionType.Yes */;
|
|
48
|
+
nativeTextView.spellCheckingType = 1 /* UITextSpellCheckingType.No */;
|
|
49
|
+
nativeTextView.smartQuotesType = 1 /* UITextSmartQuotesType.No */;
|
|
50
|
+
nativeTextView.smartDashesType = 1 /* UITextSmartDashesType.No */;
|
|
51
|
+
nativeTextView.smartInsertDeleteType = 1 /* UITextSmartInsertDeleteType.No */;
|
|
52
|
+
nativeTextView.inputAssistantItem.leadingBarButtonGroups = Utils.ios.collections.jsArrayToNSArray([]);
|
|
53
|
+
nativeTextView.inputAssistantItem.trailingBarButtonGroups = Utils.ios.collections.jsArrayToNSArray([]);
|
|
54
|
+
this.keyboardTrackingView.setTextInputView(nativeTextView);
|
|
47
55
|
}
|
|
48
56
|
// Run initial layout of children within the accessory dimensions
|
|
49
57
|
setTimeout(() => this.relayoutAccessory(), 50);
|
|
@@ -71,9 +79,10 @@ export class InputAccessoryManager extends InputAccessoryManagerBase {
|
|
|
71
79
|
*/
|
|
72
80
|
dismissKeyboard() {
|
|
73
81
|
if (this.keyboardTrackingView) {
|
|
74
|
-
this.keyboardTrackingView.
|
|
75
|
-
|
|
82
|
+
this.keyboardTrackingView.dismissKeyboard();
|
|
83
|
+
return;
|
|
76
84
|
}
|
|
85
|
+
Utils.dismissKeyboard();
|
|
77
86
|
}
|
|
78
87
|
cleanup() {
|
|
79
88
|
if (this.keyboardTrackingView) {
|
package/index.ios.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.ios.js","sourceRoot":"","sources":["../../../packages/input-accessory/index.ios.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,yBAAyB,EAAwB,MAAM,UAAU,CAAC;AAE3E,MAAM,OAAO,qBAAsB,SAAQ,yBAAyB;IAApE;;QACS,yBAAoB,GAAgC,IAAI,CAAC;QACzD,eAAU,GAAwB,IAAI,CAAC;QACvC,uBAAkB,GAAkB,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"index.ios.js","sourceRoot":"","sources":["../../../packages/input-accessory/index.ios.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nativescript/input-accessory",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "iOS/Android keyboard input accessory for NativeScript chat UIs — docked input bar, interactive dismiss, auto-scroll",
|
|
5
5
|
"main": "index",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -25,15 +25,25 @@ public class KeyboardTrackingView: UIView {
|
|
|
25
25
|
// Maximum height for the input area (prevents infinite growth)
|
|
26
26
|
private let maxAccessoryHeight: CGFloat = 200
|
|
27
27
|
|
|
28
|
-
//
|
|
28
|
+
// Home-indicator padding used when the accessory is visible without the keyboard.
|
|
29
29
|
private var safeAreaBottomInset: CGFloat = 0
|
|
30
30
|
|
|
31
|
+
// Active bottom padding inside the accessory. This is removed while the
|
|
32
|
+
// software keyboard is visible so the bar sits flush above the keyboard.
|
|
33
|
+
private var currentAccessoryBottomInset: CGFloat = 0
|
|
34
|
+
|
|
31
35
|
// Track previous keyboard position to detect show/hide direction
|
|
32
36
|
private var previousKeyboardY: CGFloat = 0
|
|
33
37
|
|
|
34
38
|
// Flag to suppress animation during programmatic keyboard dismiss (tap close).
|
|
35
39
|
private var isDismissingKeyboard: Bool = false
|
|
36
40
|
|
|
41
|
+
// Text input hosted inside the accessory. When it resigns first responder,
|
|
42
|
+
// the tracking view must take first responder back or iOS removes the
|
|
43
|
+
// inputAccessoryView from the screen.
|
|
44
|
+
private weak var textInputView: UIView?
|
|
45
|
+
private var isCleaningUp: Bool = false
|
|
46
|
+
|
|
37
47
|
// Callback for triggering ScrollView content relayout from TypeScript
|
|
38
48
|
private var scrollViewRelayoutCallback: (() -> Void)?
|
|
39
49
|
|
|
@@ -62,6 +72,7 @@ public class KeyboardTrackingView: UIView {
|
|
|
62
72
|
* @param height The height of the input container
|
|
63
73
|
*/
|
|
64
74
|
public func setup(inputContainer: UIView, scrollView: UIScrollView, height: CGFloat) {
|
|
75
|
+
self.isCleaningUp = false
|
|
65
76
|
self.accessoryHeight = height
|
|
66
77
|
self.trackedScrollView = scrollView
|
|
67
78
|
self.contentView = inputContainer
|
|
@@ -81,13 +92,15 @@ public class KeyboardTrackingView: UIView {
|
|
|
81
92
|
}
|
|
82
93
|
}
|
|
83
94
|
|
|
95
|
+
self.currentAccessoryBottomInset = self.safeAreaBottomInset
|
|
96
|
+
|
|
84
97
|
// Total height includes content height + safe area for home indicator
|
|
85
|
-
let totalHeight = height + self.
|
|
98
|
+
let totalHeight = height + self.currentAccessoryBottomInset
|
|
86
99
|
|
|
87
100
|
// Create the accessory view container
|
|
88
101
|
let screenWidth = UIScreen.main.bounds.width
|
|
89
102
|
let accessoryContainer = InputAccessoryContainerView(frame: CGRect(x: 0, y: 0, width: screenWidth, height: totalHeight))
|
|
90
|
-
accessoryContainer.safeAreaBottomInset = self.
|
|
103
|
+
accessoryContainer.safeAreaBottomInset = self.currentAccessoryBottomInset
|
|
91
104
|
accessoryContainer.contentHeight = height
|
|
92
105
|
|
|
93
106
|
// Store original superview and subview index before removal
|
|
@@ -128,14 +141,7 @@ public class KeyboardTrackingView: UIView {
|
|
|
128
141
|
|
|
129
142
|
self._keyboardAccessoryView = accessoryContainer
|
|
130
143
|
|
|
131
|
-
|
|
132
|
-
// fades into the glass accessory (like Apple Notes / iMessage).
|
|
133
|
-
if #available(iOS 26.0, *) {
|
|
134
|
-
let edgeInteraction = UIScrollEdgeElementContainerInteraction()
|
|
135
|
-
edgeInteraction.scrollView = scrollView
|
|
136
|
-
edgeInteraction.edge = .bottom
|
|
137
|
-
accessoryContainer.addInteraction(edgeInteraction)
|
|
138
|
-
}
|
|
144
|
+
addScrollEdgeElementContainerInteractionIfAvailable(to: accessoryContainer, scrollView: scrollView)
|
|
139
145
|
|
|
140
146
|
// Set up scroll view for interactive dismiss
|
|
141
147
|
scrollView.keyboardDismissMode = .interactive
|
|
@@ -163,6 +169,26 @@ public class KeyboardTrackingView: UIView {
|
|
|
163
169
|
self?.becomeFirstResponder()
|
|
164
170
|
}
|
|
165
171
|
}
|
|
172
|
+
|
|
173
|
+
private func addScrollEdgeElementContainerInteractionIfAvailable(to containerView: UIView, scrollView: UIScrollView) {
|
|
174
|
+
guard #available(iOS 26.0, *) else { return }
|
|
175
|
+
|
|
176
|
+
// UIScrollEdgeElementContainerInteraction is an iOS 26 SDK symbol. Resolve
|
|
177
|
+
// it dynamically so the plugin still compiles with Xcode 16.x / iOS 18.x SDKs.
|
|
178
|
+
let interactionClass = (
|
|
179
|
+
NSClassFromString("UIScrollEdgeElementContainerInteraction")
|
|
180
|
+
?? NSClassFromString("UIKit.UIScrollEdgeElementContainerInteraction")
|
|
181
|
+
) as? NSObject.Type
|
|
182
|
+
|
|
183
|
+
guard let interactionClass = interactionClass else { return }
|
|
184
|
+
|
|
185
|
+
let interaction = interactionClass.init()
|
|
186
|
+
interaction.setValue(scrollView, forKey: "scrollView")
|
|
187
|
+
interaction.setValue(NSNumber(value: UIRectEdge.bottom.rawValue), forKey: "edge")
|
|
188
|
+
|
|
189
|
+
guard let edgeInteraction = interaction as? UIInteraction else { return }
|
|
190
|
+
containerView.addInteraction(edgeInteraction)
|
|
191
|
+
}
|
|
166
192
|
|
|
167
193
|
/**
|
|
168
194
|
* Set the callback for relayouting ScrollView content
|
|
@@ -172,6 +198,78 @@ public class KeyboardTrackingView: UIView {
|
|
|
172
198
|
self.scrollViewRelayoutCallback = callback
|
|
173
199
|
}
|
|
174
200
|
|
|
201
|
+
/**
|
|
202
|
+
* Register the editable view hosted inside the accessory. This lets the
|
|
203
|
+
* plugin recover when callers use UIApplication/endEditing based dismissal
|
|
204
|
+
* instead of InputAccessoryManager.dismissKeyboard().
|
|
205
|
+
*/
|
|
206
|
+
public func setTextInputView(_ textInputView: UIView) {
|
|
207
|
+
self.textInputView = textInputView
|
|
208
|
+
suppressTextInputAssistant(for: textInputView)
|
|
209
|
+
|
|
210
|
+
if textInputView is UITextView {
|
|
211
|
+
NotificationCenter.default.addObserver(
|
|
212
|
+
self,
|
|
213
|
+
selector: #selector(textInputDidBeginEditing(_:)),
|
|
214
|
+
name: UITextView.textDidBeginEditingNotification,
|
|
215
|
+
object: textInputView
|
|
216
|
+
)
|
|
217
|
+
NotificationCenter.default.addObserver(
|
|
218
|
+
self,
|
|
219
|
+
selector: #selector(textInputDidEndEditing(_:)),
|
|
220
|
+
name: UITextView.textDidEndEditingNotification,
|
|
221
|
+
object: textInputView
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if textInputView is UITextField {
|
|
226
|
+
NotificationCenter.default.addObserver(
|
|
227
|
+
self,
|
|
228
|
+
selector: #selector(textInputDidBeginEditing(_:)),
|
|
229
|
+
name: UITextField.textDidBeginEditingNotification,
|
|
230
|
+
object: textInputView
|
|
231
|
+
)
|
|
232
|
+
NotificationCenter.default.addObserver(
|
|
233
|
+
self,
|
|
234
|
+
selector: #selector(textInputDidEndEditing(_:)),
|
|
235
|
+
name: UITextField.textDidEndEditingNotification,
|
|
236
|
+
object: textInputView
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private func suppressTextInputAssistant(for textInputView: UIView) {
|
|
242
|
+
textInputView.inputAssistantItem.leadingBarButtonGroups = []
|
|
243
|
+
textInputView.inputAssistantItem.trailingBarButtonGroups = []
|
|
244
|
+
|
|
245
|
+
if let textView = textInputView as? UITextView {
|
|
246
|
+
textView.autocorrectionType = .no
|
|
247
|
+
textView.spellCheckingType = .no
|
|
248
|
+
textView.smartQuotesType = .no
|
|
249
|
+
textView.smartDashesType = .no
|
|
250
|
+
textView.smartInsertDeleteType = .no
|
|
251
|
+
textView.textContentType = nil
|
|
252
|
+
if #available(iOS 17.0, *) {
|
|
253
|
+
textView.inlinePredictionType = .no
|
|
254
|
+
}
|
|
255
|
+
textView.reloadInputViews()
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if let textField = textInputView as? UITextField {
|
|
260
|
+
textField.autocorrectionType = .no
|
|
261
|
+
textField.spellCheckingType = .no
|
|
262
|
+
textField.smartQuotesType = .no
|
|
263
|
+
textField.smartDashesType = .no
|
|
264
|
+
textField.smartInsertDeleteType = .no
|
|
265
|
+
textField.textContentType = nil
|
|
266
|
+
if #available(iOS 17.0, *) {
|
|
267
|
+
textField.inlinePredictionType = .no
|
|
268
|
+
}
|
|
269
|
+
textField.reloadInputViews()
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
175
273
|
/**
|
|
176
274
|
* Trigger NativeScript content remeasurement for ScrollView after frame resize
|
|
177
275
|
*/
|
|
@@ -240,15 +338,19 @@ public class KeyboardTrackingView: UIView {
|
|
|
240
338
|
let window = accessoryView.window,
|
|
241
339
|
let scrollView = trackedScrollView else { return }
|
|
242
340
|
|
|
243
|
-
let frameInWindow = accessoryView.convert(accessoryView.bounds, to: window)
|
|
244
|
-
let accessoryTop = frameInWindow.origin.y
|
|
245
|
-
|
|
246
341
|
let screenHeight = UIScreen.main.bounds.height
|
|
342
|
+
var frameInWindow = accessoryView.convert(accessoryView.bounds, to: window)
|
|
343
|
+
var accessoryTop = frameInWindow.origin.y
|
|
344
|
+
updateAccessoryBottomInsetForAccessoryPosition(accessoryTop, screenHeight: screenHeight)
|
|
345
|
+
frameInWindow = accessoryView.convert(accessoryView.bounds, to: window)
|
|
346
|
+
accessoryTop = frameInWindow.origin.y
|
|
347
|
+
|
|
247
348
|
let scrollViewTopInWindow = scrollView.superview?.convert(
|
|
248
349
|
scrollView.frame.origin, to: nil).y ?? scrollView.frame.origin.y
|
|
249
350
|
|
|
250
351
|
let targetFrameHeight = max(100, screenHeight - scrollViewTopInWindow)
|
|
251
|
-
let
|
|
352
|
+
let accessoryTotalHeight = self.accessoryHeight + self.currentAccessoryBottomInset
|
|
353
|
+
let keyboardOverlap = max(accessoryTotalHeight, screenHeight - accessoryTop)
|
|
252
354
|
|
|
253
355
|
let frameChanged = abs(targetFrameHeight - scrollView.frame.size.height) > 1
|
|
254
356
|
let insetChanged = abs(keyboardOverlap - scrollView.contentInset.bottom) > 1
|
|
@@ -287,7 +389,8 @@ public class KeyboardTrackingView: UIView {
|
|
|
287
389
|
let targetFrameHeight = max(100, screenHeight - scrollViewTopInWindow)
|
|
288
390
|
|
|
289
391
|
// contentInset tracks the moving keyboard+accessory area
|
|
290
|
-
let
|
|
392
|
+
let accessoryTotalHeight = self.accessoryHeight + self.currentAccessoryBottomInset
|
|
393
|
+
let keyboardOverlap = max(accessoryTotalHeight, screenHeight - accessoryTop)
|
|
291
394
|
|
|
292
395
|
let frameChanged = abs(targetFrameHeight - scrollView.frame.size.height) > 0.5
|
|
293
396
|
let insetChanged = abs(keyboardOverlap - scrollView.contentInset.bottom) > 0.5
|
|
@@ -334,11 +437,13 @@ public class KeyboardTrackingView: UIView {
|
|
|
334
437
|
|
|
335
438
|
self.accessoryHeight = clampedHeight
|
|
336
439
|
|
|
337
|
-
// Total height includes
|
|
338
|
-
|
|
440
|
+
// Total height includes the currently active bottom inset. This is
|
|
441
|
+
// zero while the keyboard is open and safe-area padding when closed.
|
|
442
|
+
let totalHeight = clampedHeight + self.currentAccessoryBottomInset
|
|
339
443
|
|
|
340
444
|
// Update the container's heights
|
|
341
445
|
accessoryView.contentHeight = clampedHeight
|
|
446
|
+
accessoryView.safeAreaBottomInset = self.currentAccessoryBottomInset
|
|
342
447
|
|
|
343
448
|
// Update via the internal height constraint - the reliable way to resize inputAccessoryViews
|
|
344
449
|
accessoryView.updateHeightConstraint(totalHeight)
|
|
@@ -375,6 +480,32 @@ public class KeyboardTrackingView: UIView {
|
|
|
375
480
|
}
|
|
376
481
|
return nil
|
|
377
482
|
}
|
|
483
|
+
|
|
484
|
+
private func updateAccessoryBottomInset(_ bottomInset: CGFloat) {
|
|
485
|
+
guard abs(self.currentAccessoryBottomInset - bottomInset) > 0.5 else {
|
|
486
|
+
return
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
self.currentAccessoryBottomInset = bottomInset
|
|
490
|
+
|
|
491
|
+
guard let accessoryView = _keyboardAccessoryView else {
|
|
492
|
+
return
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
accessoryView.safeAreaBottomInset = bottomInset
|
|
496
|
+
accessoryView.updateHeightConstraint(self.accessoryHeight + bottomInset)
|
|
497
|
+
|
|
498
|
+
if let contentView = self.contentView {
|
|
499
|
+
contentView.frame = CGRect(x: 0, y: 0, width: accessoryView.bounds.width, height: self.accessoryHeight)
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
private func updateAccessoryBottomInsetForAccessoryPosition(_ accessoryTop: CGFloat, screenHeight: CGFloat) {
|
|
504
|
+
let accessoryOnlyThreshold = self.accessoryHeight + self.safeAreaBottomInset + 10
|
|
505
|
+
let keyboardOverlap = max(0, screenHeight - accessoryTop)
|
|
506
|
+
let isKeyboardShowing = keyboardOverlap > accessoryOnlyThreshold
|
|
507
|
+
updateAccessoryBottomInset(isKeyboardShowing ? 0 : self.safeAreaBottomInset)
|
|
508
|
+
}
|
|
378
509
|
|
|
379
510
|
/**
|
|
380
511
|
* Show the keyboard (make text field first responder)
|
|
@@ -401,6 +532,78 @@ public class KeyboardTrackingView: UIView {
|
|
|
401
532
|
}
|
|
402
533
|
}
|
|
403
534
|
|
|
535
|
+
/**
|
|
536
|
+
* Dismiss the software keyboard while keeping the inputAccessoryView alive.
|
|
537
|
+
* This transfers first responder from the hosted TextView/TextField back to
|
|
538
|
+
* KeyboardTrackingView, producing an accessory-only state instead of fully
|
|
539
|
+
* removing the input bar.
|
|
540
|
+
*/
|
|
541
|
+
public func dismissKeyboard() {
|
|
542
|
+
stopInteractiveTracking()
|
|
543
|
+
setDismissingKeyboard()
|
|
544
|
+
|
|
545
|
+
if !self.isFirstResponder {
|
|
546
|
+
self.becomeFirstResponder()
|
|
547
|
+
} else {
|
|
548
|
+
self.reloadInputViews()
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// The frame notification usually handles this, but finalize on the next
|
|
552
|
+
// turn as a guard against UIKit sending notification(s) before the
|
|
553
|
+
// responder transfer settles.
|
|
554
|
+
DispatchQueue.main.async { [weak self] in
|
|
555
|
+
self?.finalizeScrollViewHeight()
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
@objc private func textInputDidBeginEditing(_ notification: Notification) {
|
|
560
|
+
if let textInputView = notification.object as? UIView {
|
|
561
|
+
suppressTextInputAssistant(for: textInputView)
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
@objc private func textInputDidEndEditing(_ notification: Notification) {
|
|
566
|
+
guard !isCleaningUp,
|
|
567
|
+
let accessoryView = _keyboardAccessoryView,
|
|
568
|
+
accessoryView.window != nil else { return }
|
|
569
|
+
|
|
570
|
+
restoreAccessoryFirstResponderIfNeeded()
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
private func restoreAccessoryFirstResponderIfNeeded() {
|
|
574
|
+
guard !isCleaningUp,
|
|
575
|
+
let accessoryView = _keyboardAccessoryView,
|
|
576
|
+
let window = accessoryView.window else { return }
|
|
577
|
+
|
|
578
|
+
if let activeResponder = findFirstResponder(in: window),
|
|
579
|
+
activeResponder !== self,
|
|
580
|
+
!isView(activeResponder, descendantOf: accessoryView) {
|
|
581
|
+
return
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
guard !self.isFirstResponder else { return }
|
|
585
|
+
|
|
586
|
+
setDismissingKeyboard()
|
|
587
|
+
DispatchQueue.main.async { [weak self] in
|
|
588
|
+
guard let self = self, !self.isCleaningUp else { return }
|
|
589
|
+
if !self.isFirstResponder {
|
|
590
|
+
self.becomeFirstResponder()
|
|
591
|
+
}
|
|
592
|
+
self.finalizeScrollViewHeight()
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
private func isView(_ view: UIView, descendantOf ancestor: UIView) -> Bool {
|
|
597
|
+
var currentView: UIView? = view
|
|
598
|
+
while let candidate = currentView {
|
|
599
|
+
if candidate === ancestor {
|
|
600
|
+
return true
|
|
601
|
+
}
|
|
602
|
+
currentView = candidate.superview
|
|
603
|
+
}
|
|
604
|
+
return false
|
|
605
|
+
}
|
|
606
|
+
|
|
404
607
|
// MARK: - Keyboard Handling
|
|
405
608
|
|
|
406
609
|
@objc private func keyboardWillChangeFrame(_ notification: Notification) {
|
|
@@ -437,14 +640,6 @@ public class KeyboardTrackingView: UIView {
|
|
|
437
640
|
// the translucent accessory AND the keyboard (for blur-through visibility).
|
|
438
641
|
let targetFrameHeight = max(100, screenHeight - scrollViewTopInWindow)
|
|
439
642
|
|
|
440
|
-
// contentInset covers the full keyboard+accessory area from the screen bottom.
|
|
441
|
-
// Clamp to at least the accessory height — the accessory is always visible
|
|
442
|
-
// (KeyboardTrackingView is always first responder), so the overlap never drops
|
|
443
|
-
// below it. This prevents a transient inset=0 state during first-responder
|
|
444
|
-
// transfers (e.g., UITextView → KeyboardTrackingView) that would cause a scroll jump.
|
|
445
|
-
let accessoryTotalHeight = self.accessoryHeight + self.safeAreaBottomInset
|
|
446
|
-
let keyboardOverlap = max(accessoryTotalHeight, screenHeight - endFrame.origin.y)
|
|
447
|
-
|
|
448
643
|
// Detect keyboard showing/hiding for scroll behavior.
|
|
449
644
|
// iOS includes the inputAccessoryView in the reported keyboard frame,
|
|
450
645
|
// so when only the accessory is visible (keyboard hidden), endFrame.origin.y
|
|
@@ -454,6 +649,16 @@ public class KeyboardTrackingView: UIView {
|
|
|
454
649
|
let isKeyboardShowing = endFrame.origin.y < screenHeight - accessoryOnlyThreshold
|
|
455
650
|
let wasKeyboardHidden = previousKeyboardY >= screenHeight - accessoryOnlyThreshold
|
|
456
651
|
let keyboardJustAppeared = isKeyboardShowing && wasKeyboardHidden
|
|
652
|
+
let desiredBottomInset: CGFloat = isKeyboardShowing ? 0 : self.safeAreaBottomInset
|
|
653
|
+
updateAccessoryBottomInset(desiredBottomInset)
|
|
654
|
+
|
|
655
|
+
// contentInset covers the full keyboard+accessory area from the screen bottom.
|
|
656
|
+
// Clamp to at least the accessory height — the accessory is always visible
|
|
657
|
+
// (KeyboardTrackingView is always first responder), so the overlap never drops
|
|
658
|
+
// below it. This prevents a transient inset=0 state during first-responder
|
|
659
|
+
// transfers (e.g., UITextView → KeyboardTrackingView) that would cause a scroll jump.
|
|
660
|
+
let accessoryTotalHeight = self.accessoryHeight + self.currentAccessoryBottomInset
|
|
661
|
+
let keyboardOverlap = max(accessoryTotalHeight, screenHeight - endFrame.origin.y)
|
|
457
662
|
|
|
458
663
|
// Store current position for next comparison
|
|
459
664
|
previousKeyboardY = endFrame.origin.y
|
|
@@ -488,12 +693,18 @@ public class KeyboardTrackingView: UIView {
|
|
|
488
693
|
scrollView.contentInset.bottom = keyboardOverlap
|
|
489
694
|
scrollView.verticalScrollIndicatorInsets.bottom = keyboardOverlap
|
|
490
695
|
|
|
491
|
-
//
|
|
696
|
+
// Preserve visual position for short content. Clamping to zero while
|
|
697
|
+
// the keyboard is handing off causes a visible up/down jump, and the
|
|
698
|
+
// chat view will issue its own scroll after adding the sent message.
|
|
492
699
|
let contentHeight = scrollView.contentSize.height
|
|
493
700
|
let visibleHeight = targetFrameHeight - keyboardOverlap
|
|
494
701
|
let maxOffset = max(0, contentHeight - visibleHeight)
|
|
495
|
-
|
|
496
|
-
|
|
702
|
+
if contentHeight > visibleHeight {
|
|
703
|
+
let clampedOffset = max(0, min(currentOffset, maxOffset))
|
|
704
|
+
if abs(clampedOffset - currentOffset) > 0.5 {
|
|
705
|
+
scrollView.contentOffset = CGPoint(x: 0, y: clampedOffset)
|
|
706
|
+
}
|
|
707
|
+
}
|
|
497
708
|
|
|
498
709
|
self.relayoutScrollViewContent()
|
|
499
710
|
return
|
|
@@ -537,11 +748,13 @@ public class KeyboardTrackingView: UIView {
|
|
|
537
748
|
// MARK: - Cleanup
|
|
538
749
|
|
|
539
750
|
public func cleanup() {
|
|
751
|
+
isCleaningUp = true
|
|
540
752
|
stopInteractiveTracking()
|
|
541
753
|
trackedScrollView?.panGestureRecognizer.removeTarget(self, action: #selector(handleScrollViewPan(_:)))
|
|
542
754
|
NotificationCenter.default.removeObserver(self)
|
|
543
755
|
self._keyboardAccessoryView = nil
|
|
544
756
|
self.contentView = nil
|
|
757
|
+
self.textInputView = nil
|
|
545
758
|
self.trackedScrollView = nil
|
|
546
759
|
self.scrollViewRelayoutCallback = nil
|
|
547
760
|
self.resignFirstResponder()
|