aix 0.6.2 → 0.7.1-alpha.1
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/Aix.podspec +3 -0
- package/README.md +154 -270
- package/ios/AixKeyboardManager.swift +118 -0
- package/ios/HybridAix.swift +106 -462
- package/ios/HybridAixComposer.swift +1 -1
- package/nitrogen/generated/android/c++/JAixScrollOnFooterSizeUpdate.hpp +5 -5
- package/nitrogen/generated/android/c++/JAixStickToKeyboard.hpp +5 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/aix/AixScrollOnFooterSizeUpdate.kt +2 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/aix/AixStickToKeyboard.kt +2 -2
- package/nitrogen/generated/ios/Aix-Swift-Cxx-Bridge.hpp +14 -14
- package/nitrogen/generated/ios/swift/AixScrollOnFooterSizeUpdate.swift +24 -5
- package/nitrogen/generated/ios/swift/AixStickToKeyboard.swift +24 -5
- package/nitrogen/generated/shared/c++/AixScrollOnFooterSizeUpdate.hpp +5 -5
- package/nitrogen/generated/shared/c++/AixStickToKeyboard.hpp +6 -6
- package/package.json +1 -1
- package/src/dropzone.tsx +2 -0
- package/src/views/aix.nitro.ts +2 -2
package/ios/HybridAix.swift
CHANGED
|
@@ -38,7 +38,6 @@ protocol AixContext: AnyObject {
|
|
|
38
38
|
func reportComposerHeightChange(height: CGFloat)
|
|
39
39
|
|
|
40
40
|
// MARK: - Keyboard State (for composer sticky behavior)
|
|
41
|
-
|
|
42
41
|
/// Current keyboard height
|
|
43
42
|
var keyboardHeight: CGFloat { get }
|
|
44
43
|
|
|
@@ -107,11 +106,9 @@ extension UIView {
|
|
|
107
106
|
}
|
|
108
107
|
|
|
109
108
|
// MARK: - HybridAix (Root Context)
|
|
110
|
-
|
|
111
109
|
class HybridAix: HybridAixSpec, AixContext, KeyboardNotificationsDelegate {
|
|
112
110
|
|
|
113
111
|
var penultimateCellIndex: Double?
|
|
114
|
-
|
|
115
112
|
var shouldApplyContentInsets: Bool? = nil
|
|
116
113
|
var applyContentInsetDelay: Double? = nil
|
|
117
114
|
var onWillApplyContentInsets: ((_ insets: AixContentInsets) -> Void)? = nil
|
|
@@ -126,15 +123,17 @@ class HybridAix: HybridAixSpec, AixContext, KeyboardNotificationsDelegate {
|
|
|
126
123
|
}
|
|
127
124
|
}
|
|
128
125
|
|
|
129
|
-
var scrollOnComposerSizeUpdate: AixScrollOnFooterSizeUpdate?
|
|
130
|
-
|
|
131
126
|
var mainScrollViewID: String? {
|
|
132
127
|
didSet {
|
|
133
128
|
guard mainScrollViewID != oldValue else { return }
|
|
134
|
-
|
|
129
|
+
// Reset all scroll view and cell state when ID changes
|
|
135
130
|
removeScrollViewObservers()
|
|
136
131
|
cachedScrollView = nil
|
|
137
|
-
|
|
132
|
+
didScrollToEndInitially = false
|
|
133
|
+
prevIsScrolledNearEnd = nil
|
|
134
|
+
blankView = nil
|
|
135
|
+
cells.removeAllObjects()
|
|
136
|
+
lastReportedBlankViewSize = (size: .zero, index: 0)
|
|
138
137
|
}
|
|
139
138
|
}
|
|
140
139
|
|
|
@@ -156,7 +155,6 @@ class HybridAix: HybridAixSpec, AixContext, KeyboardNotificationsDelegate {
|
|
|
156
155
|
self.startEvent = KeyboardStartEvent(
|
|
157
156
|
scrollY: event.scrollY,
|
|
158
157
|
isOpening: event.isOpening,
|
|
159
|
-
isInteractive: event.isInteractive,
|
|
160
158
|
interpolateContentOffsetY: nil
|
|
161
159
|
)
|
|
162
160
|
}
|
|
@@ -165,11 +163,7 @@ class HybridAix: HybridAixSpec, AixContext, KeyboardNotificationsDelegate {
|
|
|
165
163
|
}
|
|
166
164
|
}
|
|
167
165
|
|
|
168
|
-
private var
|
|
169
|
-
|
|
170
|
-
private var didScrollToEndInitially: Bool {
|
|
171
|
-
return didScrollToEndInitiallyForId == (mainScrollViewID ?? "")
|
|
172
|
-
}
|
|
166
|
+
private var didScrollToEndInitially: Bool = false
|
|
173
167
|
|
|
174
168
|
// MARK: - Inner View
|
|
175
169
|
|
|
@@ -217,7 +211,6 @@ class HybridAix: HybridAixSpec, AixContext, KeyboardNotificationsDelegate {
|
|
|
217
211
|
}
|
|
218
212
|
|
|
219
213
|
// MARK: - Private State
|
|
220
|
-
|
|
221
214
|
/// Queued scroll operation waiting for blank view to update
|
|
222
215
|
private var queuedScrollToEnd: QueuedScrollToEnd? = nil
|
|
223
216
|
|
|
@@ -228,22 +221,15 @@ class HybridAix: HybridAixSpec, AixContext, KeyboardNotificationsDelegate {
|
|
|
228
221
|
/// Cached scroll view reference (weak to avoid retain cycles)
|
|
229
222
|
private weak var cachedScrollView: UIScrollView?
|
|
230
223
|
|
|
231
|
-
/// Flag to track if we've set up the pan gesture observer
|
|
232
|
-
private var didSetupPanGestureObserver = false
|
|
233
|
-
|
|
234
|
-
/// Flag to track if we're currently in an interactive keyboard dismiss
|
|
235
|
-
private var isInInteractiveDismiss = false
|
|
236
|
-
|
|
237
224
|
/// Previous "scrolled near end" state for change detection
|
|
238
225
|
private var prevIsScrolledNearEnd: Bool? = nil
|
|
239
|
-
|
|
226
|
+
|
|
240
227
|
/// KVO observation tokens for scroll view
|
|
241
228
|
private var contentOffsetObservation: NSKeyValueObservation?
|
|
242
229
|
private var contentSizeObservation: NSKeyValueObservation?
|
|
243
230
|
private var boundsObservation: NSKeyValueObservation?
|
|
244
231
|
|
|
245
232
|
// MARK: - Context References (weak to avoid retain cycles)
|
|
246
|
-
|
|
247
233
|
weak var blankView: HybridAixCellView? = nil {
|
|
248
234
|
didSet {
|
|
249
235
|
// Could add observers or callbacks here when blank view changes
|
|
@@ -253,7 +239,6 @@ class HybridAix: HybridAixSpec, AixContext, KeyboardNotificationsDelegate {
|
|
|
253
239
|
weak var composerView: HybridAixComposer? = nil
|
|
254
240
|
|
|
255
241
|
// MARK: - Computed Properties
|
|
256
|
-
|
|
257
242
|
/// Find the scroll view within our view hierarchy
|
|
258
243
|
/// We search from the superview (HybridAixComponent) since the scroll view
|
|
259
244
|
/// is a sibling of our inner view, not a child
|
|
@@ -268,25 +253,18 @@ class HybridAix: HybridAixSpec, AixContext, KeyboardNotificationsDelegate {
|
|
|
268
253
|
var sv: UIScrollView? = nil
|
|
269
254
|
if let scrollViewID = mainScrollViewID, !scrollViewID.isEmpty {
|
|
270
255
|
sv = searchRoot.findScrollView(withIdentifier: scrollViewID)
|
|
271
|
-
if sv != nil {
|
|
272
|
-
print("[Aix] scrollView found by ID '\(scrollViewID)': \(String(describing: sv))")
|
|
273
|
-
}
|
|
274
256
|
}
|
|
275
257
|
|
|
276
258
|
// Fallback to default subview iteration if not found by ID
|
|
277
259
|
if sv == nil {
|
|
278
260
|
sv = searchRoot.findScrollView()
|
|
279
|
-
print("[Aix] scrollView found by iteration: \(String(describing: sv))")
|
|
280
261
|
}
|
|
281
262
|
|
|
282
263
|
cachedScrollView = sv
|
|
283
264
|
|
|
284
|
-
// Set up
|
|
285
|
-
if let scrollView = sv
|
|
286
|
-
// Disable automatic scroll indicator inset adjustment so we can control it manually
|
|
265
|
+
// Set up observers when we find the scroll view
|
|
266
|
+
if let scrollView = sv {
|
|
287
267
|
scrollView.automaticallyAdjustsScrollIndicatorInsets = false
|
|
288
|
-
|
|
289
|
-
setupPanGestureObserver()
|
|
290
268
|
setupScrollViewObservers(scrollView)
|
|
291
269
|
applyScrollIndicatorInsets()
|
|
292
270
|
}
|
|
@@ -322,89 +300,6 @@ class HybridAix: HybridAixSpec, AixContext, KeyboardNotificationsDelegate {
|
|
|
322
300
|
boundsObservation = nil
|
|
323
301
|
}
|
|
324
302
|
|
|
325
|
-
/// Set up observer on scroll view's pan gesture to detect interactive keyboard dismiss
|
|
326
|
-
private func setupPanGestureObserver() {
|
|
327
|
-
guard let scrollView = cachedScrollView else { return }
|
|
328
|
-
didSetupPanGestureObserver = true
|
|
329
|
-
|
|
330
|
-
scrollView.panGestureRecognizer.addTarget(self, action: #selector(handlePanGesture(_:)))
|
|
331
|
-
print("[Aix] Pan gesture observer set up")
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/// Clean up pan gesture observer to avoid retain cycles
|
|
335
|
-
private func removePanGestureObserver() {
|
|
336
|
-
guard didSetupPanGestureObserver, let scrollView = cachedScrollView else { return }
|
|
337
|
-
scrollView.panGestureRecognizer.removeTarget(self, action: #selector(handlePanGesture(_:)))
|
|
338
|
-
didSetupPanGestureObserver = false
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/// Handle pan gesture state changes to detect interactive keyboard dismiss
|
|
342
|
-
@objc private func handlePanGesture(_ gesture: UIPanGestureRecognizer) {
|
|
343
|
-
guard let scrollView = cachedScrollView,
|
|
344
|
-
scrollView.keyboardDismissMode == .interactive,
|
|
345
|
-
keyboardHeight > 0 else { return }
|
|
346
|
-
|
|
347
|
-
switch gesture.state {
|
|
348
|
-
case .began, .changed:
|
|
349
|
-
// Check if finger has reached the top of composer (or keyboard if no composer)
|
|
350
|
-
if !isInInteractiveDismiss && isFingerAtComposerTop(gesture: gesture) {
|
|
351
|
-
startInteractiveKeyboardDismiss()
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
case .ended, .cancelled, .failed:
|
|
355
|
-
if isInInteractiveDismiss {
|
|
356
|
-
// The keyboard manager will handle the end via notifications
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
default:
|
|
360
|
-
break
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/// Check if the finger position has reached the top of the composer view
|
|
365
|
-
private func isFingerAtComposerTop(gesture: UIPanGestureRecognizer) -> Bool {
|
|
366
|
-
guard let window = view.window else { return false }
|
|
367
|
-
|
|
368
|
-
// Get finger location in window coordinates
|
|
369
|
-
let fingerLocationInWindow = gesture.location(in: window)
|
|
370
|
-
|
|
371
|
-
// Get the threshold Y position (top of composer, or top of keyboard if no composer)
|
|
372
|
-
let thresholdY: CGFloat
|
|
373
|
-
if let composerView = composerView?.view, let composerWindow = composerView.window {
|
|
374
|
-
// Convert composer's frame to window coordinates
|
|
375
|
-
let composerFrameInWindow = composerView.convert(composerView.bounds, to: composerWindow)
|
|
376
|
-
thresholdY = composerFrameInWindow.minY
|
|
377
|
-
} else {
|
|
378
|
-
// Fallback: use keyboard top position
|
|
379
|
-
let screenHeight = UIScreen.main.bounds.height
|
|
380
|
-
thresholdY = screenHeight - keyboardHeight
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// Finger has reached the composer top when its Y >= threshold
|
|
384
|
-
return fingerLocationInWindow.y >= thresholdY
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
/// Start tracking an interactive keyboard dismiss
|
|
388
|
-
private func startInteractiveKeyboardDismiss() {
|
|
389
|
-
return // this is totally broken rn, full of false positives
|
|
390
|
-
guard !isInInteractiveDismiss else { return }
|
|
391
|
-
isInInteractiveDismiss = true
|
|
392
|
-
|
|
393
|
-
let scrollY = scrollView?.contentOffset.y ?? 0
|
|
394
|
-
|
|
395
|
-
print("[Aix] Starting interactive keyboard dismiss from height=\(keyboardHeight), scrollY=\(scrollY)")
|
|
396
|
-
|
|
397
|
-
// Calculate proper interpolation values (same as non-interactive close)
|
|
398
|
-
let interpolation = getContentOffsetYWhenClosing(scrollY: scrollY)
|
|
399
|
-
|
|
400
|
-
startEvent = KeyboardStartEvent(
|
|
401
|
-
scrollY: scrollY,
|
|
402
|
-
isOpening: false,
|
|
403
|
-
isInteractive: true,
|
|
404
|
-
interpolateContentOffsetY: interpolation,
|
|
405
|
-
)
|
|
406
|
-
}
|
|
407
|
-
|
|
408
303
|
/// Height of the composer view
|
|
409
304
|
private var composerHeight: CGFloat {
|
|
410
305
|
let h = composerView?.view.bounds.height ?? 0
|
|
@@ -432,14 +327,29 @@ class HybridAix: HybridAixSpec, AixContext, KeyboardNotificationsDelegate {
|
|
|
432
327
|
private func calculateBlankSize(keyboardHeight: CGFloat, additionalContentInsetBottom: CGFloat) -> CGFloat {
|
|
433
328
|
guard let scrollView, let blankView else { return 0 }
|
|
434
329
|
|
|
435
|
-
let
|
|
436
|
-
let
|
|
330
|
+
let startIndex: Int
|
|
331
|
+
let endIndex = Int(blankView.index) - 1
|
|
332
|
+
if let penultimateCellIndex {
|
|
333
|
+
startIndex = Int(penultimateCellIndex)
|
|
334
|
+
} else {
|
|
335
|
+
startIndex = endIndex
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
var cellsBeforeBlankViewHeight: CGFloat = 0
|
|
339
|
+
if startIndex <= endIndex {
|
|
340
|
+
for i in startIndex...endIndex {
|
|
341
|
+
if let cell = getCell(index: i) {
|
|
342
|
+
cellsBeforeBlankViewHeight += cell.view.frame.height
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
437
347
|
let blankViewHeight = blankView.view.frame.height
|
|
438
348
|
|
|
439
349
|
// Calculate visible area above all bottom chrome (keyboard, composer, additional insets)
|
|
440
350
|
// The blank size fills the remaining space so the last message can scroll to the top
|
|
441
351
|
let visibleAreaHeight = scrollView.bounds.height - keyboardHeight - composerHeight - additionalContentInsetBottom
|
|
442
|
-
let inset = visibleAreaHeight - blankViewHeight -
|
|
352
|
+
let inset = visibleAreaHeight - blankViewHeight - cellsBeforeBlankViewHeight
|
|
443
353
|
return max(0, inset)
|
|
444
354
|
}
|
|
445
355
|
|
|
@@ -460,12 +370,12 @@ class HybridAix: HybridAixSpec, AixContext, KeyboardNotificationsDelegate {
|
|
|
460
370
|
|
|
461
371
|
/// Apply the current content inset to the scroll view
|
|
462
372
|
func applyContentInset(contentInsetBottom overrideContentInsetBottom: CGFloat? = nil) {
|
|
463
|
-
guard let scrollView else { return }
|
|
373
|
+
guard let _ = scrollView else { return }
|
|
464
374
|
|
|
465
375
|
let targetTop = additionalContentInsetTop
|
|
466
376
|
let targetBottom = overrideContentInsetBottom ?? self.contentInsetBottom
|
|
467
377
|
|
|
468
|
-
// Create insets struct for callback
|
|
378
|
+
// Create insets struct for callback
|
|
469
379
|
let insets = AixContentInsets(
|
|
470
380
|
top: Double(targetTop),
|
|
471
381
|
left: nil,
|
|
@@ -473,10 +383,8 @@ class HybridAix: HybridAixSpec, AixContext, KeyboardNotificationsDelegate {
|
|
|
473
383
|
right: nil
|
|
474
384
|
)
|
|
475
385
|
|
|
476
|
-
print("[aix] applyContentInset \(targetBottom)")
|
|
477
386
|
onWillApplyContentInsets?(insets)
|
|
478
387
|
|
|
479
|
-
// If shouldApplyContentInsets is explicitly false, call callback and return
|
|
480
388
|
if shouldApplyContentInsets == false {
|
|
481
389
|
return
|
|
482
390
|
}
|
|
@@ -503,7 +411,6 @@ class HybridAix: HybridAixSpec, AixContext, KeyboardNotificationsDelegate {
|
|
|
503
411
|
}
|
|
504
412
|
|
|
505
413
|
/// Centralized function to check and fire onScrolledNearEndChange callback
|
|
506
|
-
/// Called from KVO observers and after content inset changes
|
|
507
414
|
private func updateScrolledNearEndState() {
|
|
508
415
|
guard didScrollToEndInitially, scrollView != nil else { return }
|
|
509
416
|
let isNearEnd = getIsScrolledNearEnd(distFromEnd: distFromEnd)
|
|
@@ -545,9 +452,15 @@ class HybridAix: HybridAixSpec, AixContext, KeyboardNotificationsDelegate {
|
|
|
545
452
|
}
|
|
546
453
|
}
|
|
547
454
|
|
|
455
|
+
/// Apply both content insets and scroll indicator insets
|
|
456
|
+
private func applyAllInsets() {
|
|
457
|
+
applyContentInset()
|
|
458
|
+
applyScrollIndicatorInsets()
|
|
459
|
+
}
|
|
460
|
+
|
|
548
461
|
private func scrollToEndInternal(animated: Bool?) {
|
|
549
462
|
guard let scrollView else { return }
|
|
550
|
-
|
|
463
|
+
|
|
551
464
|
// Calculate the offset to show the bottom of content
|
|
552
465
|
let bottomOffset = CGPoint(
|
|
553
466
|
x: 0,
|
|
@@ -557,8 +470,7 @@ class HybridAix: HybridAixSpec, AixContext, KeyboardNotificationsDelegate {
|
|
|
557
470
|
}
|
|
558
471
|
|
|
559
472
|
|
|
560
|
-
// MARK: - Keyboard
|
|
561
|
-
|
|
473
|
+
// MARK: - Keyboard Management
|
|
562
474
|
private lazy var keyboardNotifications: KeyboardNotifications = {
|
|
563
475
|
return KeyboardNotifications(notifications: [.willShow, .willHide, .didShow, .didHide, .willChangeFrame], delegate: self)
|
|
564
476
|
}()
|
|
@@ -567,7 +479,6 @@ class HybridAix: HybridAixSpec, AixContext, KeyboardNotificationsDelegate {
|
|
|
567
479
|
private struct KeyboardStartEvent {
|
|
568
480
|
let scrollY: CGFloat
|
|
569
481
|
let isOpening: Bool
|
|
570
|
-
var isInteractive: Bool
|
|
571
482
|
let interpolateContentOffsetY: (CGFloat, CGFloat)?
|
|
572
483
|
}
|
|
573
484
|
|
|
@@ -581,11 +492,6 @@ class HybridAix: HybridAixSpec, AixContext, KeyboardNotificationsDelegate {
|
|
|
581
492
|
keyboardHeightWhenOpen = targetHeight
|
|
582
493
|
}
|
|
583
494
|
|
|
584
|
-
// If we're already in an interactive dismiss, don't overwrite
|
|
585
|
-
if isInInteractiveDismiss {
|
|
586
|
-
return
|
|
587
|
-
}
|
|
588
|
-
|
|
589
495
|
let scrollY = scrollView?.contentOffset.y ?? 0
|
|
590
496
|
|
|
591
497
|
var interpolateContentOffsetY: (CGFloat, CGFloat)? = {
|
|
@@ -597,102 +503,50 @@ class HybridAix: HybridAixSpec, AixContext, KeyboardNotificationsDelegate {
|
|
|
597
503
|
}()
|
|
598
504
|
|
|
599
505
|
if queuedScrollToEnd != nil {
|
|
600
|
-
// don't interpolate the keyboard if we're planning to scroll to end
|
|
601
506
|
interpolateContentOffsetY = nil
|
|
602
507
|
}
|
|
603
508
|
|
|
604
|
-
print("[Aix] handleKeyboardWillMove: isOpening=\(isOpening), interpolate=\(String(describing: interpolateContentOffsetY))")
|
|
605
|
-
|
|
606
509
|
startEvent = KeyboardStartEvent(
|
|
607
510
|
scrollY: scrollY,
|
|
608
511
|
isOpening: isOpening,
|
|
609
|
-
isInteractive: false,
|
|
610
512
|
interpolateContentOffsetY: interpolateContentOffsetY
|
|
611
513
|
)
|
|
612
514
|
}
|
|
613
515
|
|
|
614
|
-
/// Handle keyboard frame updates during animation
|
|
615
|
-
private func handleKeyboardMove(height: CGFloat, progress: CGFloat) {
|
|
616
|
-
if keyboardHeightWhenOpen > 0 {
|
|
617
|
-
keyboardProgress = height / keyboardHeightWhenOpen
|
|
618
|
-
}
|
|
619
|
-
keyboardHeight = height
|
|
620
|
-
|
|
621
|
-
guard let startEvent else { return }
|
|
622
|
-
|
|
623
|
-
applyContentInset()
|
|
624
|
-
applyScrollIndicatorInsets()
|
|
625
|
-
|
|
626
|
-
if let (startY, endY) = startEvent.interpolateContentOffsetY {
|
|
627
|
-
// Normalize progress to always go from 0 to 1 (start to end)
|
|
628
|
-
// For opening: progress goes 0→1, so use as-is
|
|
629
|
-
// For closing: progress goes 1→0, so invert it
|
|
630
|
-
let normalizedProgress = startEvent.isOpening ? progress : (1 - progress)
|
|
631
|
-
let newScrollY = startY + (endY - startY) * normalizedProgress
|
|
632
|
-
scrollView?.setContentOffset(CGPoint(x: 0, y: newScrollY), animated: false)
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
|
|
636
516
|
/// Handle keyboard animation end
|
|
637
517
|
private func handleKeyboardDidMove(height: CGFloat, progress: CGFloat) {
|
|
638
|
-
// Ensure final height is applied
|
|
639
518
|
keyboardHeight = height
|
|
640
519
|
if keyboardHeightWhenOpen > 0 {
|
|
641
520
|
keyboardProgress = height / keyboardHeightWhenOpen
|
|
642
521
|
}
|
|
643
522
|
|
|
644
|
-
|
|
645
|
-
applyScrollIndicatorInsets()
|
|
523
|
+
applyAllInsets()
|
|
646
524
|
|
|
647
525
|
startEvent = nil
|
|
648
|
-
isInInteractiveDismiss = false
|
|
649
526
|
|
|
650
527
|
if queuedScrollToEnd?.waitForKeyboardToEnd == true {
|
|
651
528
|
flushQueuedScrollToEnd(force: true)
|
|
652
529
|
}
|
|
653
530
|
}
|
|
654
531
|
|
|
655
|
-
/// Handle interactive keyboard dismissal
|
|
656
|
-
private func handleKeyboardMoveInteractive(height: CGFloat, progress: CGFloat) {
|
|
657
|
-
// Mark that we're in an interactive dismiss if not already
|
|
658
|
-
if !isInInteractiveDismiss && startEvent != nil {
|
|
659
|
-
isInInteractiveDismiss = true
|
|
660
|
-
if var event = startEvent {
|
|
661
|
-
event.isInteractive = true
|
|
662
|
-
startEvent = event
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
// Update keyboard state
|
|
667
|
-
handleKeyboardMove(height: height, progress: progress)
|
|
668
|
-
|
|
669
|
-
// Update composer transform
|
|
670
|
-
composerView?.applyKeyboardTransform(height: height, heightWhenOpen: keyboardHeightWhenOpen, animated: false)
|
|
671
|
-
}
|
|
672
|
-
|
|
673
532
|
// MARK: - Initialization
|
|
674
|
-
|
|
675
533
|
override init() {
|
|
676
534
|
let inner = InnerView()
|
|
677
535
|
self.view = inner
|
|
678
536
|
super.init()
|
|
679
537
|
inner.owner = self
|
|
680
|
-
print("[Aix] HybridAix initialized, attaching context to view")
|
|
681
538
|
// Attach this context to our inner view
|
|
682
539
|
view.aixContext = self
|
|
683
540
|
}
|
|
684
541
|
|
|
685
542
|
deinit {
|
|
686
|
-
removePanGestureObserver()
|
|
687
543
|
removeScrollViewObservers()
|
|
688
544
|
}
|
|
689
545
|
|
|
690
546
|
// MARK: - Lifecycle
|
|
691
|
-
|
|
692
547
|
/// Called when our view is added to or removed from the HybridAixComponent
|
|
693
548
|
private func handleDidMoveToSuperview() {
|
|
694
549
|
if let superview = view.superview {
|
|
695
|
-
print("[Aix] View added to superview: \(type(of: superview)), attaching context")
|
|
696
550
|
// Attach context to the superview (HybridAixComponent) so children can find it
|
|
697
551
|
superview.aixContext = self
|
|
698
552
|
|
|
@@ -738,7 +592,6 @@ class HybridAix: HybridAixSpec, AixContext, KeyboardNotificationsDelegate {
|
|
|
738
592
|
}
|
|
739
593
|
|
|
740
594
|
// MARK: - AixContext Protocol
|
|
741
|
-
|
|
742
595
|
private var lastReportedBlankViewSize = (size: CGSize.zero, index: 0)
|
|
743
596
|
|
|
744
597
|
func reportBlankViewSizeChange(size: CGSize, index: Int) {
|
|
@@ -749,22 +602,28 @@ class HybridAix: HybridAixSpec, AixContext, KeyboardNotificationsDelegate {
|
|
|
749
602
|
|
|
750
603
|
lastReportedBlankViewSize = (size: size, index: index)
|
|
751
604
|
|
|
752
|
-
//
|
|
605
|
+
// Initial mount setup (tied to mainScrollViewID)
|
|
753
606
|
if !didScrollToEndInitially {
|
|
754
607
|
UIView.performWithoutAnimation {
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
608
|
+
applyAllInsets()
|
|
609
|
+
|
|
610
|
+
if shouldStartAtEnd {
|
|
611
|
+
scrollToEndInternal(animated: false)
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Set initial state based on actual scroll position after layout
|
|
758
615
|
prevIsScrolledNearEnd = getIsScrolledNearEnd(distFromEnd: distFromEnd)
|
|
759
616
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
617
|
+
|
|
618
|
+
// Enable callbacks only after all setup is complete
|
|
619
|
+
didScrollToEndInitially = true
|
|
620
|
+
return
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
applyAllInsets()
|
|
764
624
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
}
|
|
625
|
+
if let queued = queuedScrollToEnd, index == queued.index {
|
|
626
|
+
flushQueuedScrollToEnd()
|
|
768
627
|
}
|
|
769
628
|
}
|
|
770
629
|
|
|
@@ -774,6 +633,11 @@ class HybridAix: HybridAixSpec, AixContext, KeyboardNotificationsDelegate {
|
|
|
774
633
|
// If this cell is marked as last, update our blank view reference
|
|
775
634
|
if cell.isLast {
|
|
776
635
|
blankView = cell
|
|
636
|
+
|
|
637
|
+
// Trigger initial setup - handleLayoutChange may have already fired before
|
|
638
|
+
// the cell was in window, so we need to report here to ensure setup happens
|
|
639
|
+
let currentSize = cell.view.bounds.size
|
|
640
|
+
reportBlankViewSizeChange(size: currentSize, index: Int(cell.index))
|
|
777
641
|
}
|
|
778
642
|
}
|
|
779
643
|
|
|
@@ -799,72 +663,33 @@ class HybridAix: HybridAixSpec, AixContext, KeyboardNotificationsDelegate {
|
|
|
799
663
|
private var lastReportedComposerHeight: CGFloat = 0
|
|
800
664
|
|
|
801
665
|
func reportComposerHeightChange(height: CGFloat) {
|
|
802
|
-
|
|
803
|
-
return
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
let previousHeight = lastReportedComposerHeight
|
|
807
|
-
let isShrinking = height < previousHeight
|
|
808
|
-
|
|
666
|
+
guard height != lastReportedComposerHeight else { return }
|
|
809
667
|
lastReportedComposerHeight = height
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
}
|
|
814
|
-
|
|
668
|
+
|
|
669
|
+
guard didScrollToEndInitially else { return }
|
|
670
|
+
|
|
815
671
|
let shouldScroll = shouldScrollOnFooterSizeUpdate()
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
if shouldScroll
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
applyScrollIndicatorInsets()
|
|
822
|
-
return
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
let newContentInsetBottom = self.contentInsetBottom
|
|
826
|
-
let bottomOffset = CGPoint(
|
|
827
|
-
x: 0,
|
|
828
|
-
y: max(0, scrollView.contentSize.height - scrollView.bounds.height + newContentInsetBottom)
|
|
829
|
-
)
|
|
830
|
-
|
|
831
|
-
UIView.animate(withDuration: 0.25, delay: 0, options: [.curveEaseOut]) {
|
|
832
|
-
scrollView.contentInset.bottom = newContentInsetBottom
|
|
833
|
-
scrollView.contentOffset = bottomOffset
|
|
834
|
-
}
|
|
835
|
-
applyScrollIndicatorInsets()
|
|
836
|
-
} else {
|
|
837
|
-
applyContentInset()
|
|
838
|
-
applyScrollIndicatorInsets()
|
|
839
|
-
|
|
840
|
-
if shouldScroll {
|
|
841
|
-
scrollToEndInternal(animated: animated)
|
|
842
|
-
}
|
|
672
|
+
applyAllInsets()
|
|
673
|
+
|
|
674
|
+
if shouldScroll {
|
|
675
|
+
let animated = scrollOnFooterSizeUpdate?.animated ?? false
|
|
676
|
+
scrollToEndInternal(animated: animated)
|
|
843
677
|
}
|
|
844
678
|
}
|
|
845
679
|
|
|
846
680
|
private func shouldScrollOnFooterSizeUpdate() -> Bool {
|
|
847
|
-
guard let settings = scrollOnFooterSizeUpdate, settings.enabled else {
|
|
681
|
+
guard let settings = scrollOnFooterSizeUpdate, settings.enabled ?? true else {
|
|
848
682
|
return false
|
|
849
683
|
}
|
|
850
|
-
guard
|
|
684
|
+
guard scrollView != nil else {
|
|
851
685
|
return false
|
|
852
686
|
}
|
|
853
687
|
|
|
854
|
-
let
|
|
855
|
-
|
|
856
|
-
let currentOffsetY = scrollView.contentOffset.y
|
|
857
|
-
let bottomInset = scrollView.contentInset.bottom
|
|
858
|
-
|
|
859
|
-
let maxOffsetY = max(0, contentHeight - scrollViewHeight + bottomInset)
|
|
860
|
-
let distanceFromEnd = maxOffsetY - currentOffsetY
|
|
861
|
-
|
|
862
|
-
let threshold = settings.scrolledToEndThreshold ?? 0
|
|
863
|
-
return distanceFromEnd <= CGFloat(threshold)
|
|
688
|
+
let threshold = settings.scrolledToEndThreshold ?? 100
|
|
689
|
+
return distFromEnd <= CGFloat(threshold)
|
|
864
690
|
}
|
|
865
691
|
|
|
866
692
|
// MARK: - Cell Access
|
|
867
|
-
|
|
868
693
|
/// Get a cell by its index
|
|
869
694
|
func getCell(index: Int) -> HybridAixCellView? {
|
|
870
695
|
return cells.object(forKey: NSNumber(value: index))
|
|
@@ -872,7 +697,6 @@ class HybridAix: HybridAixSpec, AixContext, KeyboardNotificationsDelegate {
|
|
|
872
697
|
|
|
873
698
|
|
|
874
699
|
// MARK: - Scrolling
|
|
875
|
-
|
|
876
700
|
func getIsQueuedScrollToEndReady(queuedScrollToEnd: QueuedScrollToEnd) -> Bool {
|
|
877
701
|
guard let blankView else { return false }
|
|
878
702
|
if queuedScrollToEnd.waitForKeyboardToEnd == true && startEvent != nil {
|
|
@@ -890,7 +714,6 @@ class HybridAix: HybridAixSpec, AixContext, KeyboardNotificationsDelegate {
|
|
|
890
714
|
}
|
|
891
715
|
|
|
892
716
|
// MARK: - Keyboard Notification Handlers
|
|
893
|
-
|
|
894
717
|
func keyboardWillShow(notification: NSNotification) {
|
|
895
718
|
guard let userInfo = notification.userInfo,
|
|
896
719
|
let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect,
|
|
@@ -898,7 +721,6 @@ class HybridAix: HybridAixSpec, AixContext, KeyboardNotificationsDelegate {
|
|
|
898
721
|
let curveValue = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt else { return }
|
|
899
722
|
|
|
900
723
|
let targetHeight = keyboardFrame.height
|
|
901
|
-
print("[Aix] keyboardWillShow: targetHeight=\(targetHeight), duration=\(duration)")
|
|
902
724
|
|
|
903
725
|
guard duration > 0 else { return }
|
|
904
726
|
|
|
@@ -909,8 +731,7 @@ class HybridAix: HybridAixSpec, AixContext, KeyboardNotificationsDelegate {
|
|
|
909
731
|
if !didScrollToEndInitially {
|
|
910
732
|
keyboardHeight = targetHeight
|
|
911
733
|
keyboardProgress = 1.0
|
|
912
|
-
|
|
913
|
-
applyScrollIndicatorInsets()
|
|
734
|
+
applyAllInsets()
|
|
914
735
|
composerView?.applyKeyboardTransform(height: targetHeight, heightWhenOpen: keyboardHeightWhenOpen, animated: false)
|
|
915
736
|
return
|
|
916
737
|
}
|
|
@@ -919,17 +740,16 @@ class HybridAix: HybridAixSpec, AixContext, KeyboardNotificationsDelegate {
|
|
|
919
740
|
|
|
920
741
|
let options = UIView.AnimationOptions(rawValue: curveValue << 16)
|
|
921
742
|
UIView.animate(withDuration: duration, delay: 0, options: options, animations: { [weak self] in
|
|
922
|
-
guard let self
|
|
923
|
-
|
|
924
|
-
if
|
|
925
|
-
|
|
743
|
+
guard let self else { return }
|
|
744
|
+
keyboardHeight = targetHeight
|
|
745
|
+
if keyboardHeightWhenOpen > 0 {
|
|
746
|
+
keyboardProgress = targetHeight / keyboardHeightWhenOpen
|
|
926
747
|
}
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
self.composerView?.applyKeyboardTransform(height: targetHeight, heightWhenOpen: self.keyboardHeightWhenOpen, animated: false)
|
|
748
|
+
applyAllInsets()
|
|
749
|
+
composerView?.applyKeyboardTransform(height: targetHeight, heightWhenOpen: keyboardHeightWhenOpen, animated: false)
|
|
930
750
|
|
|
931
|
-
if let (
|
|
932
|
-
|
|
751
|
+
if let (_, endY) = startEvent?.interpolateContentOffsetY {
|
|
752
|
+
scrollView?.setContentOffset(CGPoint(x: 0, y: endY), animated: false)
|
|
933
753
|
}
|
|
934
754
|
}, completion: { [weak self] _ in
|
|
935
755
|
self?.handleKeyboardDidMove(height: targetHeight, progress: 1.0)
|
|
@@ -941,249 +761,73 @@ class HybridAix: HybridAixSpec, AixContext, KeyboardNotificationsDelegate {
|
|
|
941
761
|
let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double,
|
|
942
762
|
let curveValue = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt else { return }
|
|
943
763
|
|
|
944
|
-
print("[Aix] keyboardWillHide: duration=\(duration)")
|
|
945
|
-
|
|
946
|
-
// Don't interpolate scroll position when closing, the inset change will handle the visual transition
|
|
947
764
|
startEvent = nil
|
|
948
765
|
|
|
949
766
|
let options = UIView.AnimationOptions(rawValue: curveValue << 16)
|
|
950
767
|
UIView.animate(withDuration: duration, delay: 0, options: options, animations: { [weak self] in
|
|
951
|
-
guard let self
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
self.applyScrollIndicatorInsets()
|
|
957
|
-
self.composerView?.applyKeyboardTransform(height: 0, heightWhenOpen: self.keyboardHeightWhenOpen, animated: false)
|
|
768
|
+
guard let self else { return }
|
|
769
|
+
keyboardHeight = 0
|
|
770
|
+
keyboardProgress = 0
|
|
771
|
+
applyAllInsets()
|
|
772
|
+
composerView?.applyKeyboardTransform(height: 0, heightWhenOpen: keyboardHeightWhenOpen, animated: false)
|
|
958
773
|
}, completion: { [weak self] _ in
|
|
959
774
|
self?.handleKeyboardDidMove(height: 0, progress: 0)
|
|
960
775
|
})
|
|
961
776
|
}
|
|
962
777
|
|
|
963
778
|
func keyboardDidShow(notification: NSNotification) {
|
|
964
|
-
print("[Aix] keyboardDidShow")
|
|
965
779
|
}
|
|
966
780
|
|
|
967
781
|
func keyboardDidHide(notification: NSNotification) {
|
|
968
|
-
print("[Aix] keyboardDidHide")
|
|
969
782
|
keyboardHeightWhenOpen = 0
|
|
970
783
|
composerView?.applyKeyboardTransform(height: 0, heightWhenOpen: 0, animated: false)
|
|
971
784
|
}
|
|
972
785
|
|
|
973
786
|
func keyboardWillChangeFrame(notification: NSNotification) {
|
|
974
|
-
guard let userInfo = notification.userInfo,
|
|
975
|
-
let keyboardFrameEnd = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
|
|
976
|
-
|
|
977
|
-
let screenHeight = UIScreen.main.bounds.height
|
|
978
|
-
let keyboardTop = keyboardFrameEnd.origin.y
|
|
979
|
-
let newHeight = max(0, screenHeight - keyboardTop)
|
|
980
|
-
|
|
981
|
-
if startEvent != nil && !isInInteractiveDismiss {
|
|
982
|
-
return
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
if isInInteractiveDismiss && newHeight != keyboardHeight {
|
|
986
|
-
let progress = keyboardHeightWhenOpen > 0 ? newHeight / keyboardHeightWhenOpen : 0
|
|
987
|
-
handleKeyboardMoveInteractive(height: newHeight, progress: progress)
|
|
988
|
-
}
|
|
989
787
|
}
|
|
990
788
|
}
|
|
991
789
|
|
|
992
790
|
// MARK: - Scroll Position Helpers
|
|
993
|
-
|
|
994
791
|
extension HybridAix {
|
|
995
|
-
/// Check if an interactive keyboard dismiss is in progress by examining scroll view state
|
|
996
|
-
private func isInteractiveDismissInProgress() -> Bool {
|
|
997
|
-
guard let scrollView = scrollView else { return false }
|
|
998
|
-
|
|
999
|
-
// Check if scroll view has interactive keyboard dismiss mode
|
|
1000
|
-
guard scrollView.keyboardDismissMode == .interactive else { return false }
|
|
1001
|
-
|
|
1002
|
-
// Check if pan gesture is active (user is scrolling)
|
|
1003
|
-
let panGesture = scrollView.panGestureRecognizer
|
|
1004
|
-
let gestureState = panGesture.state
|
|
1005
|
-
|
|
1006
|
-
// Pan gesture states: .began = 1, .changed = 2
|
|
1007
|
-
return gestureState == .began || gestureState == .changed
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
792
|
/// Distance from current scroll position to the maximum scroll position (end)
|
|
1011
793
|
var distFromEnd: CGFloat {
|
|
1012
|
-
guard let scrollView
|
|
1013
|
-
|
|
1014
|
-
return maxScrollY - scrollView.contentOffset.y
|
|
794
|
+
guard let scrollView else { return 0 }
|
|
795
|
+
return scrollView.contentSize.height - scrollView.bounds.height + contentInsetBottom - scrollView.contentOffset.y
|
|
1015
796
|
}
|
|
1016
797
|
|
|
1017
798
|
func getIsScrolledNearEnd(distFromEnd: CGFloat) -> Bool {
|
|
799
|
+
guard scrollView != nil else { return false }
|
|
1018
800
|
return distFromEnd <= (scrollEndReachedThreshold ?? max(200, blankSize))
|
|
1019
801
|
}
|
|
1020
|
-
|
|
802
|
+
|
|
1021
803
|
func getContentOffsetYWhenOpening(scrollY: CGFloat) -> (CGFloat, CGFloat)? {
|
|
1022
|
-
guard let scrollView
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
// Use the target additionalContentInsetBottom when keyboard is fully open
|
|
1027
|
-
let targetAdditionalInset = CGFloat(self.additionalContentInsets?.bottom?.whenKeyboardOpen ?? 0)
|
|
804
|
+
guard let scrollView, getIsScrolledNearEnd(distFromEnd: distFromEnd), blankSize <= keyboardHeightWhenOpen else {
|
|
805
|
+
return nil
|
|
806
|
+
}
|
|
1028
807
|
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
// When blankSize is 0: contentInset = keyboard + composer + additionalInset
|
|
1032
|
-
let shiftContentUpToY = scrollView.contentSize.height - scrollView.bounds.height + keyboardHeightWhenOpen + composerHeight + targetAdditionalInset
|
|
808
|
+
let targetAdditionalInset = CGFloat(additionalContentInsets?.bottom?.whenKeyboardOpen ?? 0)
|
|
809
|
+
let targetY = scrollView.contentSize.height - scrollView.bounds.height + keyboardHeightWhenOpen + composerHeight + targetAdditionalInset
|
|
1033
810
|
|
|
1034
|
-
|
|
1035
|
-
return (scrollY, shiftContentUpToY)
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
let hasBlankSizeLessThanOpenKeyboardHeight = blankSize > 0 && blankSize <= keyboardHeightWhenOpen
|
|
1039
|
-
|
|
1040
|
-
if hasBlankSizeLessThanOpenKeyboardHeight && isScrolledNearEnd {
|
|
1041
|
-
return (scrollY, shiftContentUpToY)
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
return nil
|
|
811
|
+
return (scrollY, targetY)
|
|
1045
812
|
}
|
|
1046
813
|
|
|
1047
814
|
func getContentOffsetYWhenClosing(scrollY: CGFloat) -> (CGFloat, CGFloat)? {
|
|
1048
|
-
guard keyboardHeightWhenOpen > 0 else {
|
|
1049
|
-
let isScrolledNearEnd = getIsScrolledNearEnd(distFromEnd: distFromEnd)
|
|
1050
|
-
|
|
1051
|
-
if !isScrolledNearEnd {
|
|
815
|
+
guard scrollView != nil, keyboardHeightWhenOpen > 0, getIsScrolledNearEnd(distFromEnd: distFromEnd) else {
|
|
1052
816
|
return nil
|
|
1053
817
|
}
|
|
1054
818
|
|
|
1055
|
-
let
|
|
1056
|
-
let
|
|
819
|
+
let additionalInsetOpen = CGFloat(additionalContentInsets?.bottom?.whenKeyboardOpen ?? 0)
|
|
820
|
+
let additionalInsetClosed = CGFloat(additionalContentInsets?.bottom?.whenKeyboardClosed ?? 0)
|
|
1057
821
|
|
|
1058
|
-
|
|
1059
|
-
let
|
|
1060
|
-
let blankSizeWithoutKeyboard = calculateBlankSize(keyboardHeight: 0, additionalContentInsetBottom: additionalContentInsetBottomWithoutKeyboard)
|
|
822
|
+
let blankSizeOpen = calculateBlankSize(keyboardHeight: keyboardHeightWhenOpen, additionalContentInsetBottom: additionalInsetOpen)
|
|
823
|
+
let blankSizeClosed = calculateBlankSize(keyboardHeight: 0, additionalContentInsetBottom: additionalInsetClosed)
|
|
1061
824
|
|
|
1062
|
-
|
|
1063
|
-
let
|
|
1064
|
-
let insetWithoutKeyboard = calculateContentInsetBottom(keyboardHeight: 0, blankSize: blankSizeWithoutKeyboard,
|
|
1065
|
-
additionalContentInsetBottom: additionalContentInsetBottomWithoutKeyboard
|
|
1066
|
-
)
|
|
1067
|
-
let insetDecrease = insetWithKeyboard - insetWithoutKeyboard
|
|
825
|
+
let insetOpen = calculateContentInsetBottom(keyboardHeight: keyboardHeightWhenOpen, blankSize: blankSizeOpen, additionalContentInsetBottom: additionalInsetOpen)
|
|
826
|
+
let insetClosed = calculateContentInsetBottom(keyboardHeight: 0, blankSize: blankSizeClosed, additionalContentInsetBottom: additionalInsetClosed)
|
|
1068
827
|
|
|
1069
|
-
|
|
1070
|
-
// by the same amount the inset decreases
|
|
1071
|
-
let targetScrollY = max(0, scrollY - insetDecrease)
|
|
828
|
+
let targetScrollY = max(0, scrollY - (insetOpen - insetClosed))
|
|
1072
829
|
|
|
1073
|
-
// Only interpolate if there's actually movement needed
|
|
1074
830
|
guard abs(scrollY - targetScrollY) > 1 else { return nil }
|
|
1075
|
-
|
|
1076
831
|
return (scrollY, targetScrollY)
|
|
1077
832
|
}
|
|
1078
833
|
}
|
|
1079
|
-
|
|
1080
|
-
// Source - https://stackoverflow.com/a
|
|
1081
|
-
// Posted by Vasily Bodnarchuk, modified by community. See post 'Timeline' for change history
|
|
1082
|
-
// Retrieved 2026-01-07, License - CC BY-SA 4.0
|
|
1083
|
-
|
|
1084
|
-
protocol KeyboardNotificationsDelegate: AnyObject {
|
|
1085
|
-
func keyboardWillShow(notification: NSNotification)
|
|
1086
|
-
func keyboardWillHide(notification: NSNotification)
|
|
1087
|
-
func keyboardDidShow(notification: NSNotification)
|
|
1088
|
-
func keyboardDidHide(notification: NSNotification)
|
|
1089
|
-
func keyboardWillChangeFrame(notification: NSNotification)
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
extension KeyboardNotificationsDelegate {
|
|
1093
|
-
func keyboardWillShow(notification: NSNotification) {}
|
|
1094
|
-
func keyboardWillHide(notification: NSNotification) {}
|
|
1095
|
-
func keyboardDidShow(notification: NSNotification) {}
|
|
1096
|
-
func keyboardDidHide(notification: NSNotification) {}
|
|
1097
|
-
func keyboardWillChangeFrame(notification: NSNotification) {}
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
class KeyboardNotifications {
|
|
1101
|
-
fileprivate var _isEnabled: Bool
|
|
1102
|
-
fileprivate var notifications: [KeyboardNotificationsType]
|
|
1103
|
-
fileprivate weak var delegate: KeyboardNotificationsDelegate?
|
|
1104
|
-
|
|
1105
|
-
init(notifications: [KeyboardNotificationsType], delegate: KeyboardNotificationsDelegate) {
|
|
1106
|
-
_isEnabled = false
|
|
1107
|
-
self.notifications = notifications
|
|
1108
|
-
self.delegate = delegate
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
deinit { if isEnabled { isEnabled = false } }
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
// MARK: - enums
|
|
1115
|
-
|
|
1116
|
-
extension KeyboardNotifications {
|
|
1117
|
-
|
|
1118
|
-
enum KeyboardNotificationsType {
|
|
1119
|
-
case willShow, willHide, didShow, didHide, willChangeFrame
|
|
1120
|
-
|
|
1121
|
-
var selector: Selector {
|
|
1122
|
-
switch self {
|
|
1123
|
-
case .willShow: return #selector(keyboardWillShow(notification:))
|
|
1124
|
-
case .willHide: return #selector(keyboardWillHide(notification:))
|
|
1125
|
-
case .didShow: return #selector(keyboardDidShow(notification:))
|
|
1126
|
-
case .didHide: return #selector(keyboardDidHide(notification:))
|
|
1127
|
-
case .willChangeFrame: return #selector(keyboardWillChangeFrame(notification:))
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
var notificationName: NSNotification.Name {
|
|
1132
|
-
switch self {
|
|
1133
|
-
case .willShow: return UIResponder.keyboardWillShowNotification
|
|
1134
|
-
case .willHide: return UIResponder.keyboardWillHideNotification
|
|
1135
|
-
case .didShow: return UIResponder.keyboardDidShowNotification
|
|
1136
|
-
case .didHide: return UIResponder.keyboardDidHideNotification
|
|
1137
|
-
case .willChangeFrame: return UIResponder.keyboardWillChangeFrameNotification
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
}
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
// MARK: - isEnabled
|
|
1144
|
-
|
|
1145
|
-
extension KeyboardNotifications {
|
|
1146
|
-
|
|
1147
|
-
private func addObserver(type: KeyboardNotificationsType) {
|
|
1148
|
-
NotificationCenter.default.addObserver(self, selector: type.selector, name: type.notificationName, object: nil)
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
var isEnabled: Bool {
|
|
1152
|
-
set {
|
|
1153
|
-
if newValue {
|
|
1154
|
-
for notificaton in notifications { addObserver(type: notificaton) }
|
|
1155
|
-
} else {
|
|
1156
|
-
NotificationCenter.default.removeObserver(self)
|
|
1157
|
-
}
|
|
1158
|
-
_isEnabled = newValue
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
get { return _isEnabled }
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
// MARK: - Notification functions
|
|
1167
|
-
|
|
1168
|
-
extension KeyboardNotifications {
|
|
1169
|
-
|
|
1170
|
-
@objc func keyboardWillShow(notification: NSNotification) {
|
|
1171
|
-
delegate?.keyboardWillShow(notification: notification)
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
@objc func keyboardWillHide(notification: NSNotification) {
|
|
1175
|
-
delegate?.keyboardWillHide(notification: notification)
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
@objc func keyboardDidShow(notification: NSNotification) {
|
|
1179
|
-
delegate?.keyboardDidShow(notification: notification)
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
@objc func keyboardDidHide(notification: NSNotification) {
|
|
1183
|
-
delegate?.keyboardDidHide(notification: notification)
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
@objc func keyboardWillChangeFrame(notification: NSNotification) {
|
|
1187
|
-
delegate?.keyboardWillChangeFrame(notification: notification)
|
|
1188
|
-
}
|
|
1189
|
-
}
|