@nativescript/input-accessory 1.0.0 → 1.0.2
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 +9 -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 +221 -21
package/CHANGELOG.md
ADDED
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.2",
|
|
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
|
|
@@ -172,6 +185,78 @@ public class KeyboardTrackingView: UIView {
|
|
|
172
185
|
self.scrollViewRelayoutCallback = callback
|
|
173
186
|
}
|
|
174
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Register the editable view hosted inside the accessory. This lets the
|
|
190
|
+
* plugin recover when callers use UIApplication/endEditing based dismissal
|
|
191
|
+
* instead of InputAccessoryManager.dismissKeyboard().
|
|
192
|
+
*/
|
|
193
|
+
public func setTextInputView(_ textInputView: UIView) {
|
|
194
|
+
self.textInputView = textInputView
|
|
195
|
+
suppressTextInputAssistant(for: textInputView)
|
|
196
|
+
|
|
197
|
+
if textInputView is UITextView {
|
|
198
|
+
NotificationCenter.default.addObserver(
|
|
199
|
+
self,
|
|
200
|
+
selector: #selector(textInputDidBeginEditing(_:)),
|
|
201
|
+
name: UITextView.textDidBeginEditingNotification,
|
|
202
|
+
object: textInputView
|
|
203
|
+
)
|
|
204
|
+
NotificationCenter.default.addObserver(
|
|
205
|
+
self,
|
|
206
|
+
selector: #selector(textInputDidEndEditing(_:)),
|
|
207
|
+
name: UITextView.textDidEndEditingNotification,
|
|
208
|
+
object: textInputView
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if textInputView is UITextField {
|
|
213
|
+
NotificationCenter.default.addObserver(
|
|
214
|
+
self,
|
|
215
|
+
selector: #selector(textInputDidBeginEditing(_:)),
|
|
216
|
+
name: UITextField.textDidBeginEditingNotification,
|
|
217
|
+
object: textInputView
|
|
218
|
+
)
|
|
219
|
+
NotificationCenter.default.addObserver(
|
|
220
|
+
self,
|
|
221
|
+
selector: #selector(textInputDidEndEditing(_:)),
|
|
222
|
+
name: UITextField.textDidEndEditingNotification,
|
|
223
|
+
object: textInputView
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private func suppressTextInputAssistant(for textInputView: UIView) {
|
|
229
|
+
textInputView.inputAssistantItem.leadingBarButtonGroups = []
|
|
230
|
+
textInputView.inputAssistantItem.trailingBarButtonGroups = []
|
|
231
|
+
|
|
232
|
+
if let textView = textInputView as? UITextView {
|
|
233
|
+
textView.autocorrectionType = .no
|
|
234
|
+
textView.spellCheckingType = .no
|
|
235
|
+
textView.smartQuotesType = .no
|
|
236
|
+
textView.smartDashesType = .no
|
|
237
|
+
textView.smartInsertDeleteType = .no
|
|
238
|
+
textView.textContentType = nil
|
|
239
|
+
if #available(iOS 17.0, *) {
|
|
240
|
+
textView.inlinePredictionType = .no
|
|
241
|
+
}
|
|
242
|
+
textView.reloadInputViews()
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if let textField = textInputView as? UITextField {
|
|
247
|
+
textField.autocorrectionType = .no
|
|
248
|
+
textField.spellCheckingType = .no
|
|
249
|
+
textField.smartQuotesType = .no
|
|
250
|
+
textField.smartDashesType = .no
|
|
251
|
+
textField.smartInsertDeleteType = .no
|
|
252
|
+
textField.textContentType = nil
|
|
253
|
+
if #available(iOS 17.0, *) {
|
|
254
|
+
textField.inlinePredictionType = .no
|
|
255
|
+
}
|
|
256
|
+
textField.reloadInputViews()
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
175
260
|
/**
|
|
176
261
|
* Trigger NativeScript content remeasurement for ScrollView after frame resize
|
|
177
262
|
*/
|
|
@@ -240,15 +325,19 @@ public class KeyboardTrackingView: UIView {
|
|
|
240
325
|
let window = accessoryView.window,
|
|
241
326
|
let scrollView = trackedScrollView else { return }
|
|
242
327
|
|
|
243
|
-
let frameInWindow = accessoryView.convert(accessoryView.bounds, to: window)
|
|
244
|
-
let accessoryTop = frameInWindow.origin.y
|
|
245
|
-
|
|
246
328
|
let screenHeight = UIScreen.main.bounds.height
|
|
329
|
+
var frameInWindow = accessoryView.convert(accessoryView.bounds, to: window)
|
|
330
|
+
var accessoryTop = frameInWindow.origin.y
|
|
331
|
+
updateAccessoryBottomInsetForAccessoryPosition(accessoryTop, screenHeight: screenHeight)
|
|
332
|
+
frameInWindow = accessoryView.convert(accessoryView.bounds, to: window)
|
|
333
|
+
accessoryTop = frameInWindow.origin.y
|
|
334
|
+
|
|
247
335
|
let scrollViewTopInWindow = scrollView.superview?.convert(
|
|
248
336
|
scrollView.frame.origin, to: nil).y ?? scrollView.frame.origin.y
|
|
249
337
|
|
|
250
338
|
let targetFrameHeight = max(100, screenHeight - scrollViewTopInWindow)
|
|
251
|
-
let
|
|
339
|
+
let accessoryTotalHeight = self.accessoryHeight + self.currentAccessoryBottomInset
|
|
340
|
+
let keyboardOverlap = max(accessoryTotalHeight, screenHeight - accessoryTop)
|
|
252
341
|
|
|
253
342
|
let frameChanged = abs(targetFrameHeight - scrollView.frame.size.height) > 1
|
|
254
343
|
let insetChanged = abs(keyboardOverlap - scrollView.contentInset.bottom) > 1
|
|
@@ -287,7 +376,8 @@ public class KeyboardTrackingView: UIView {
|
|
|
287
376
|
let targetFrameHeight = max(100, screenHeight - scrollViewTopInWindow)
|
|
288
377
|
|
|
289
378
|
// contentInset tracks the moving keyboard+accessory area
|
|
290
|
-
let
|
|
379
|
+
let accessoryTotalHeight = self.accessoryHeight + self.currentAccessoryBottomInset
|
|
380
|
+
let keyboardOverlap = max(accessoryTotalHeight, screenHeight - accessoryTop)
|
|
291
381
|
|
|
292
382
|
let frameChanged = abs(targetFrameHeight - scrollView.frame.size.height) > 0.5
|
|
293
383
|
let insetChanged = abs(keyboardOverlap - scrollView.contentInset.bottom) > 0.5
|
|
@@ -334,11 +424,13 @@ public class KeyboardTrackingView: UIView {
|
|
|
334
424
|
|
|
335
425
|
self.accessoryHeight = clampedHeight
|
|
336
426
|
|
|
337
|
-
// Total height includes
|
|
338
|
-
|
|
427
|
+
// Total height includes the currently active bottom inset. This is
|
|
428
|
+
// zero while the keyboard is open and safe-area padding when closed.
|
|
429
|
+
let totalHeight = clampedHeight + self.currentAccessoryBottomInset
|
|
339
430
|
|
|
340
431
|
// Update the container's heights
|
|
341
432
|
accessoryView.contentHeight = clampedHeight
|
|
433
|
+
accessoryView.safeAreaBottomInset = self.currentAccessoryBottomInset
|
|
342
434
|
|
|
343
435
|
// Update via the internal height constraint - the reliable way to resize inputAccessoryViews
|
|
344
436
|
accessoryView.updateHeightConstraint(totalHeight)
|
|
@@ -375,6 +467,32 @@ public class KeyboardTrackingView: UIView {
|
|
|
375
467
|
}
|
|
376
468
|
return nil
|
|
377
469
|
}
|
|
470
|
+
|
|
471
|
+
private func updateAccessoryBottomInset(_ bottomInset: CGFloat) {
|
|
472
|
+
guard abs(self.currentAccessoryBottomInset - bottomInset) > 0.5 else {
|
|
473
|
+
return
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
self.currentAccessoryBottomInset = bottomInset
|
|
477
|
+
|
|
478
|
+
guard let accessoryView = _keyboardAccessoryView else {
|
|
479
|
+
return
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
accessoryView.safeAreaBottomInset = bottomInset
|
|
483
|
+
accessoryView.updateHeightConstraint(self.accessoryHeight + bottomInset)
|
|
484
|
+
|
|
485
|
+
if let contentView = self.contentView {
|
|
486
|
+
contentView.frame = CGRect(x: 0, y: 0, width: accessoryView.bounds.width, height: self.accessoryHeight)
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
private func updateAccessoryBottomInsetForAccessoryPosition(_ accessoryTop: CGFloat, screenHeight: CGFloat) {
|
|
491
|
+
let accessoryOnlyThreshold = self.accessoryHeight + self.safeAreaBottomInset + 10
|
|
492
|
+
let keyboardOverlap = max(0, screenHeight - accessoryTop)
|
|
493
|
+
let isKeyboardShowing = keyboardOverlap > accessoryOnlyThreshold
|
|
494
|
+
updateAccessoryBottomInset(isKeyboardShowing ? 0 : self.safeAreaBottomInset)
|
|
495
|
+
}
|
|
378
496
|
|
|
379
497
|
/**
|
|
380
498
|
* Show the keyboard (make text field first responder)
|
|
@@ -401,6 +519,78 @@ public class KeyboardTrackingView: UIView {
|
|
|
401
519
|
}
|
|
402
520
|
}
|
|
403
521
|
|
|
522
|
+
/**
|
|
523
|
+
* Dismiss the software keyboard while keeping the inputAccessoryView alive.
|
|
524
|
+
* This transfers first responder from the hosted TextView/TextField back to
|
|
525
|
+
* KeyboardTrackingView, producing an accessory-only state instead of fully
|
|
526
|
+
* removing the input bar.
|
|
527
|
+
*/
|
|
528
|
+
public func dismissKeyboard() {
|
|
529
|
+
stopInteractiveTracking()
|
|
530
|
+
setDismissingKeyboard()
|
|
531
|
+
|
|
532
|
+
if !self.isFirstResponder {
|
|
533
|
+
self.becomeFirstResponder()
|
|
534
|
+
} else {
|
|
535
|
+
self.reloadInputViews()
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// The frame notification usually handles this, but finalize on the next
|
|
539
|
+
// turn as a guard against UIKit sending notification(s) before the
|
|
540
|
+
// responder transfer settles.
|
|
541
|
+
DispatchQueue.main.async { [weak self] in
|
|
542
|
+
self?.finalizeScrollViewHeight()
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
@objc private func textInputDidBeginEditing(_ notification: Notification) {
|
|
547
|
+
if let textInputView = notification.object as? UIView {
|
|
548
|
+
suppressTextInputAssistant(for: textInputView)
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
@objc private func textInputDidEndEditing(_ notification: Notification) {
|
|
553
|
+
guard !isCleaningUp,
|
|
554
|
+
let accessoryView = _keyboardAccessoryView,
|
|
555
|
+
accessoryView.window != nil else { return }
|
|
556
|
+
|
|
557
|
+
restoreAccessoryFirstResponderIfNeeded()
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
private func restoreAccessoryFirstResponderIfNeeded() {
|
|
561
|
+
guard !isCleaningUp,
|
|
562
|
+
let accessoryView = _keyboardAccessoryView,
|
|
563
|
+
let window = accessoryView.window else { return }
|
|
564
|
+
|
|
565
|
+
if let activeResponder = findFirstResponder(in: window),
|
|
566
|
+
activeResponder !== self,
|
|
567
|
+
!isView(activeResponder, descendantOf: accessoryView) {
|
|
568
|
+
return
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
guard !self.isFirstResponder else { return }
|
|
572
|
+
|
|
573
|
+
setDismissingKeyboard()
|
|
574
|
+
DispatchQueue.main.async { [weak self] in
|
|
575
|
+
guard let self = self, !self.isCleaningUp else { return }
|
|
576
|
+
if !self.isFirstResponder {
|
|
577
|
+
self.becomeFirstResponder()
|
|
578
|
+
}
|
|
579
|
+
self.finalizeScrollViewHeight()
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
private func isView(_ view: UIView, descendantOf ancestor: UIView) -> Bool {
|
|
584
|
+
var currentView: UIView? = view
|
|
585
|
+
while let candidate = currentView {
|
|
586
|
+
if candidate === ancestor {
|
|
587
|
+
return true
|
|
588
|
+
}
|
|
589
|
+
currentView = candidate.superview
|
|
590
|
+
}
|
|
591
|
+
return false
|
|
592
|
+
}
|
|
593
|
+
|
|
404
594
|
// MARK: - Keyboard Handling
|
|
405
595
|
|
|
406
596
|
@objc private func keyboardWillChangeFrame(_ notification: Notification) {
|
|
@@ -437,14 +627,6 @@ public class KeyboardTrackingView: UIView {
|
|
|
437
627
|
// the translucent accessory AND the keyboard (for blur-through visibility).
|
|
438
628
|
let targetFrameHeight = max(100, screenHeight - scrollViewTopInWindow)
|
|
439
629
|
|
|
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
630
|
// Detect keyboard showing/hiding for scroll behavior.
|
|
449
631
|
// iOS includes the inputAccessoryView in the reported keyboard frame,
|
|
450
632
|
// so when only the accessory is visible (keyboard hidden), endFrame.origin.y
|
|
@@ -454,6 +636,16 @@ public class KeyboardTrackingView: UIView {
|
|
|
454
636
|
let isKeyboardShowing = endFrame.origin.y < screenHeight - accessoryOnlyThreshold
|
|
455
637
|
let wasKeyboardHidden = previousKeyboardY >= screenHeight - accessoryOnlyThreshold
|
|
456
638
|
let keyboardJustAppeared = isKeyboardShowing && wasKeyboardHidden
|
|
639
|
+
let desiredBottomInset: CGFloat = isKeyboardShowing ? 0 : self.safeAreaBottomInset
|
|
640
|
+
updateAccessoryBottomInset(desiredBottomInset)
|
|
641
|
+
|
|
642
|
+
// contentInset covers the full keyboard+accessory area from the screen bottom.
|
|
643
|
+
// Clamp to at least the accessory height — the accessory is always visible
|
|
644
|
+
// (KeyboardTrackingView is always first responder), so the overlap never drops
|
|
645
|
+
// below it. This prevents a transient inset=0 state during first-responder
|
|
646
|
+
// transfers (e.g., UITextView → KeyboardTrackingView) that would cause a scroll jump.
|
|
647
|
+
let accessoryTotalHeight = self.accessoryHeight + self.currentAccessoryBottomInset
|
|
648
|
+
let keyboardOverlap = max(accessoryTotalHeight, screenHeight - endFrame.origin.y)
|
|
457
649
|
|
|
458
650
|
// Store current position for next comparison
|
|
459
651
|
previousKeyboardY = endFrame.origin.y
|
|
@@ -488,12 +680,18 @@ public class KeyboardTrackingView: UIView {
|
|
|
488
680
|
scrollView.contentInset.bottom = keyboardOverlap
|
|
489
681
|
scrollView.verticalScrollIndicatorInsets.bottom = keyboardOverlap
|
|
490
682
|
|
|
491
|
-
//
|
|
683
|
+
// Preserve visual position for short content. Clamping to zero while
|
|
684
|
+
// the keyboard is handing off causes a visible up/down jump, and the
|
|
685
|
+
// chat view will issue its own scroll after adding the sent message.
|
|
492
686
|
let contentHeight = scrollView.contentSize.height
|
|
493
687
|
let visibleHeight = targetFrameHeight - keyboardOverlap
|
|
494
688
|
let maxOffset = max(0, contentHeight - visibleHeight)
|
|
495
|
-
|
|
496
|
-
|
|
689
|
+
if contentHeight > visibleHeight {
|
|
690
|
+
let clampedOffset = max(0, min(currentOffset, maxOffset))
|
|
691
|
+
if abs(clampedOffset - currentOffset) > 0.5 {
|
|
692
|
+
scrollView.contentOffset = CGPoint(x: 0, y: clampedOffset)
|
|
693
|
+
}
|
|
694
|
+
}
|
|
497
695
|
|
|
498
696
|
self.relayoutScrollViewContent()
|
|
499
697
|
return
|
|
@@ -537,11 +735,13 @@ public class KeyboardTrackingView: UIView {
|
|
|
537
735
|
// MARK: - Cleanup
|
|
538
736
|
|
|
539
737
|
public func cleanup() {
|
|
738
|
+
isCleaningUp = true
|
|
540
739
|
stopInteractiveTracking()
|
|
541
740
|
trackedScrollView?.panGestureRecognizer.removeTarget(self, action: #selector(handleScrollViewPan(_:)))
|
|
542
741
|
NotificationCenter.default.removeObserver(self)
|
|
543
742
|
self._keyboardAccessoryView = nil
|
|
544
743
|
self.contentView = nil
|
|
744
|
+
self.textInputView = nil
|
|
545
745
|
self.trackedScrollView = nil
|
|
546
746
|
self.scrollViewRelayoutCallback = nil
|
|
547
747
|
self.resignFirstResponder()
|