@lodev09/react-native-true-sheet 4.0.0-beta.1 → 4.0.0-beta.10

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.
Files changed (221) hide show
  1. package/README.md +25 -4
  2. package/android/build.gradle +22 -1
  3. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +24 -7
  4. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +211 -182
  5. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentViewManager.kt +5 -1
  6. package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +14 -19
  7. package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayView.kt +106 -0
  8. package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayViewManager.kt +45 -0
  9. package/android/src/main/java/com/lodev09/truesheet/TrueSheetPackage.kt +2 -1
  10. package/android/src/main/java/com/lodev09/truesheet/TrueSheetStateUpdater.kt +18 -0
  11. package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +82 -39
  12. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +275 -88
  13. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +26 -1
  14. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +75 -3
  15. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +56 -34
  16. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetOverlayRootView.kt +137 -0
  17. package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetLifecycleEvents.kt +15 -0
  18. package/android/src/main/java/com/lodev09/truesheet/utils/KeyboardUtils.kt +1 -1
  19. package/android/src/main/java/com/lodev09/truesheet/utils/TouchEventDeduper.kt +31 -0
  20. package/android/src/main/java/com/lodev09/truesheet/utils/ViewUtils.kt +19 -7
  21. package/android/src/main/jni/TrueSheetSpec.h +1 -0
  22. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewComponentDescriptor.h +0 -8
  23. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.cpp +54 -29
  24. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.h +6 -1
  25. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.cpp +1 -1
  26. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.h +6 -4
  27. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.cpp +7 -0
  28. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetLayoutUtils.h +46 -0
  29. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewComponentDescriptor.h +21 -0
  30. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewShadowNode.cpp +17 -0
  31. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewShadowNode.h +36 -0
  32. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.cpp +4 -35
  33. package/ios/TrueSheetContainerView.h +14 -7
  34. package/ios/TrueSheetContainerView.mm +10 -7
  35. package/ios/TrueSheetContentView.h +31 -15
  36. package/ios/TrueSheetContentView.mm +133 -179
  37. package/ios/TrueSheetFooterView.h +6 -0
  38. package/ios/TrueSheetFooterView.mm +48 -2
  39. package/ios/TrueSheetModule.h +2 -7
  40. package/ios/TrueSheetModule.mm +7 -16
  41. package/ios/TrueSheetOverlayView.h +32 -0
  42. package/ios/TrueSheetOverlayView.mm +161 -0
  43. package/ios/TrueSheetView.mm +171 -29
  44. package/ios/TrueSheetViewController.h +10 -0
  45. package/ios/TrueSheetViewController.mm +256 -74
  46. package/ios/core/TrueSheetGrabberView.mm +13 -3
  47. package/ios/core/TrueSheetOverlayContainerView.h +31 -0
  48. package/ios/core/TrueSheetOverlayContainerView.mm +35 -0
  49. package/ios/events/TrueSheetLifecycleEvents.h +2 -0
  50. package/ios/events/TrueSheetLifecycleEvents.mm +8 -0
  51. package/ios/tvos/TrueSheetStubs.mm +12 -0
  52. package/ios/utils/UIView+ScrollEdgeInteraction.h +1 -0
  53. package/ios/utils/UIView+ScrollEdgeInteraction.mm +24 -2
  54. package/lib/module/TrueSheet.js +149 -34
  55. package/lib/module/TrueSheet.js.map +1 -1
  56. package/lib/module/TrueSheet.web.js +122 -53
  57. package/lib/module/TrueSheet.web.js.map +1 -1
  58. package/lib/module/TrueSheetOverlay.js +30 -0
  59. package/lib/module/TrueSheetOverlay.js.map +1 -0
  60. package/lib/module/TrueSheetOverlay.web.js +35 -0
  61. package/lib/module/TrueSheetOverlay.web.js.map +1 -0
  62. package/lib/module/TrueSheetPeek.web.js +3 -3
  63. package/lib/module/TrueSheetPeek.web.js.map +1 -1
  64. package/lib/module/fabric/TrueSheetContentViewNativeComponent.ts +1 -1
  65. package/lib/module/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
  66. package/lib/module/fabric/TrueSheetViewNativeComponent.ts +5 -0
  67. package/lib/module/index.js +1 -0
  68. package/lib/module/index.js.map +1 -1
  69. package/lib/module/mocks/index.js +10 -0
  70. package/lib/module/mocks/index.js.map +1 -1
  71. package/lib/module/mocks/navigation.js +31 -21
  72. package/lib/module/mocks/navigation.js.map +1 -1
  73. package/lib/module/mocks/navigationExpoRouter.js +21 -0
  74. package/lib/module/mocks/navigationExpoRouter.js.map +1 -0
  75. package/lib/module/mocks/reanimated.js +1 -1
  76. package/lib/module/mocks/reanimated.js.map +1 -1
  77. package/lib/module/navigation/{TrueSheetView.js → TrueSheetNavigatorContent.js} +14 -12
  78. package/lib/module/navigation/TrueSheetNavigatorContent.js.map +1 -0
  79. package/lib/module/navigation/actions.js +50 -0
  80. package/lib/module/navigation/actions.js.map +1 -0
  81. package/lib/module/navigation/base-types.js +2 -0
  82. package/lib/module/navigation/base-types.js.map +1 -0
  83. package/lib/module/navigation/{TrueSheetRouter.js → createTrueSheetRouter.js} +11 -24
  84. package/lib/module/navigation/createTrueSheetRouter.js.map +1 -0
  85. package/lib/module/navigation/expo-router/base-types.js +2 -0
  86. package/lib/module/navigation/expo-router/base-types.js.map +1 -0
  87. package/lib/module/navigation/expo-router/index.js +50 -0
  88. package/lib/module/navigation/expo-router/index.js.map +1 -0
  89. package/lib/module/navigation/expo-router/types.js +4 -0
  90. package/lib/module/navigation/expo-router/types.js.map +1 -0
  91. package/lib/module/navigation/index.js +43 -2
  92. package/lib/module/navigation/index.js.map +1 -1
  93. package/lib/module/navigation/navigator.js +10 -0
  94. package/lib/module/navigation/navigator.js.map +1 -0
  95. package/lib/module/navigation/screen/ReanimatedTrueSheetScreen.js +2 -2
  96. package/lib/module/navigation/screen/ReanimatedTrueSheetScreen.js.map +1 -1
  97. package/lib/module/navigation/screen/TrueSheetScreen.js +2 -2
  98. package/lib/module/navigation/screen/TrueSheetScreen.js.map +1 -1
  99. package/lib/module/navigation/screen/useSheetScreenState.js +5 -4
  100. package/lib/module/navigation/screen/useSheetScreenState.js.map +1 -1
  101. package/lib/module/navigation/useTrueSheetNavigation.js +1 -1
  102. package/lib/module/navigation/useTrueSheetNavigation.js.map +1 -1
  103. package/lib/module/web/dom.js +10 -0
  104. package/lib/module/web/dom.js.map +1 -0
  105. package/lib/module/web/vaul/constants.js +4 -0
  106. package/lib/module/web/vaul/constants.js.map +1 -1
  107. package/lib/module/web/vaul/context.js.map +1 -1
  108. package/lib/module/web/vaul/helpers.js +10 -4
  109. package/lib/module/web/vaul/helpers.js.map +1 -1
  110. package/lib/module/web/vaul/index.js +35 -2
  111. package/lib/module/web/vaul/index.js.map +1 -1
  112. package/lib/typescript/src/TrueSheet.d.ts +6 -0
  113. package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
  114. package/lib/typescript/src/TrueSheet.types.d.ts +72 -1
  115. package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
  116. package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
  117. package/lib/typescript/src/TrueSheetOverlay.d.ts +8 -0
  118. package/lib/typescript/src/TrueSheetOverlay.d.ts.map +1 -0
  119. package/lib/typescript/src/TrueSheetOverlay.web.d.ts +8 -0
  120. package/lib/typescript/src/TrueSheetOverlay.web.d.ts.map +1 -0
  121. package/lib/typescript/src/TrueSheetPeek.web.d.ts.map +1 -1
  122. package/lib/typescript/src/fabric/TrueSheetOverlayViewNativeComponent.d.ts +6 -0
  123. package/lib/typescript/src/fabric/TrueSheetOverlayViewNativeComponent.d.ts.map +1 -0
  124. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +4 -0
  125. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
  126. package/lib/typescript/src/index.d.ts +1 -0
  127. package/lib/typescript/src/index.d.ts.map +1 -1
  128. package/lib/typescript/src/mocks/index.d.ts +4 -0
  129. package/lib/typescript/src/mocks/index.d.ts.map +1 -1
  130. package/lib/typescript/src/mocks/navigation.d.ts +5 -1
  131. package/lib/typescript/src/mocks/navigation.d.ts.map +1 -1
  132. package/lib/typescript/src/mocks/navigationExpoRouter.d.ts +22 -0
  133. package/lib/typescript/src/mocks/navigationExpoRouter.d.ts.map +1 -0
  134. package/lib/typescript/src/mocks/reanimated.d.ts.map +1 -1
  135. package/lib/typescript/src/navigation/TrueSheetNavigatorContent.d.ts +5 -0
  136. package/lib/typescript/src/navigation/TrueSheetNavigatorContent.d.ts.map +1 -0
  137. package/lib/typescript/src/navigation/actions.d.ts +71 -0
  138. package/lib/typescript/src/navigation/actions.d.ts.map +1 -0
  139. package/lib/typescript/src/navigation/base-types.d.ts +9 -0
  140. package/lib/typescript/src/navigation/base-types.d.ts.map +1 -0
  141. package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts +3 -0
  142. package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts.map +1 -0
  143. package/lib/typescript/src/navigation/expo-router/base-types.d.ts +8 -0
  144. package/lib/typescript/src/navigation/expo-router/base-types.d.ts.map +1 -0
  145. package/lib/typescript/src/navigation/expo-router/index.d.ts +37 -0
  146. package/lib/typescript/src/navigation/expo-router/index.d.ts.map +1 -0
  147. package/lib/typescript/src/navigation/expo-router/types.d.ts +44 -0
  148. package/lib/typescript/src/navigation/expo-router/types.d.ts.map +1 -0
  149. package/lib/typescript/src/navigation/index.d.ts +42 -3
  150. package/lib/typescript/src/navigation/index.d.ts.map +1 -1
  151. package/lib/typescript/src/navigation/navigator.d.ts +7 -0
  152. package/lib/typescript/src/navigation/navigator.d.ts.map +1 -0
  153. package/lib/typescript/src/navigation/screen/ReanimatedTrueSheetScreen.d.ts +1 -1
  154. package/lib/typescript/src/navigation/screen/ReanimatedTrueSheetScreen.d.ts.map +1 -1
  155. package/lib/typescript/src/navigation/screen/TrueSheetScreen.d.ts +1 -1
  156. package/lib/typescript/src/navigation/screen/TrueSheetScreen.d.ts.map +1 -1
  157. package/lib/typescript/src/navigation/screen/types.d.ts +3 -4
  158. package/lib/typescript/src/navigation/screen/types.d.ts.map +1 -1
  159. package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts +5 -6
  160. package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts.map +1 -1
  161. package/lib/typescript/src/navigation/types.d.ts +65 -5
  162. package/lib/typescript/src/navigation/types.d.ts.map +1 -1
  163. package/lib/typescript/src/navigation/useTrueSheetNavigation.d.ts +1 -1
  164. package/lib/typescript/src/navigation/useTrueSheetNavigation.d.ts.map +1 -1
  165. package/lib/typescript/src/web/dom.d.ts +7 -0
  166. package/lib/typescript/src/web/dom.d.ts.map +1 -0
  167. package/lib/typescript/src/web/vaul/constants.d.ts +1 -0
  168. package/lib/typescript/src/web/vaul/constants.d.ts.map +1 -1
  169. package/lib/typescript/src/web/vaul/context.d.ts +1 -0
  170. package/lib/typescript/src/web/vaul/context.d.ts.map +1 -1
  171. package/lib/typescript/src/web/vaul/helpers.d.ts.map +1 -1
  172. package/lib/typescript/src/web/vaul/index.d.ts +7 -1
  173. package/lib/typescript/src/web/vaul/index.d.ts.map +1 -1
  174. package/package.json +32 -9
  175. package/react-native.config.js +1 -0
  176. package/src/TrueSheet.tsx +184 -46
  177. package/src/TrueSheet.types.ts +77 -1
  178. package/src/TrueSheet.web.tsx +135 -64
  179. package/src/TrueSheetOverlay.tsx +24 -0
  180. package/src/TrueSheetOverlay.web.tsx +33 -0
  181. package/src/TrueSheetPeek.web.tsx +4 -3
  182. package/src/fabric/TrueSheetContentViewNativeComponent.ts +1 -1
  183. package/src/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
  184. package/src/fabric/TrueSheetViewNativeComponent.ts +5 -0
  185. package/src/index.ts +1 -0
  186. package/src/mocks/index.ts +7 -0
  187. package/src/mocks/navigation.ts +32 -25
  188. package/src/mocks/navigationExpoRouter.ts +41 -0
  189. package/src/mocks/reanimated.ts +8 -9
  190. package/src/navigation/{TrueSheetView.tsx → TrueSheetNavigatorContent.tsx} +24 -17
  191. package/src/navigation/actions.ts +78 -0
  192. package/src/navigation/base-types.ts +20 -0
  193. package/src/navigation/createTrueSheetRouter.ts +194 -0
  194. package/src/navigation/expo-router/base-types.ts +18 -0
  195. package/src/navigation/expo-router/index.ts +90 -0
  196. package/src/navigation/expo-router/types.ts +116 -0
  197. package/src/navigation/index.ts +68 -2
  198. package/src/navigation/navigator.ts +18 -0
  199. package/src/navigation/screen/ReanimatedTrueSheetScreen.tsx +2 -2
  200. package/src/navigation/screen/TrueSheetScreen.tsx +2 -2
  201. package/src/navigation/screen/types.ts +4 -6
  202. package/src/navigation/screen/useSheetScreenState.ts +10 -15
  203. package/src/navigation/types.ts +99 -16
  204. package/src/navigation/useTrueSheetNavigation.ts +1 -1
  205. package/src/web/dom.ts +9 -0
  206. package/src/web/vaul/constants.ts +4 -0
  207. package/src/web/vaul/context.ts +1 -0
  208. package/src/web/vaul/helpers.ts +13 -6
  209. package/src/web/vaul/index.tsx +46 -1
  210. package/lib/module/navigation/TrueSheetRouter.js.map +0 -1
  211. package/lib/module/navigation/TrueSheetView.js.map +0 -1
  212. package/lib/module/navigation/createTrueSheetNavigator.js +0 -63
  213. package/lib/module/navigation/createTrueSheetNavigator.js.map +0 -1
  214. package/lib/typescript/src/navigation/TrueSheetRouter.d.ts +0 -57
  215. package/lib/typescript/src/navigation/TrueSheetRouter.d.ts.map +0 -1
  216. package/lib/typescript/src/navigation/TrueSheetView.d.ts +0 -10
  217. package/lib/typescript/src/navigation/TrueSheetView.d.ts.map +0 -1
  218. package/lib/typescript/src/navigation/createTrueSheetNavigator.d.ts +0 -35
  219. package/lib/typescript/src/navigation/createTrueSheetNavigator.d.ts.map +0 -1
  220. package/src/navigation/TrueSheetRouter.ts +0 -234
  221. package/src/navigation/createTrueSheetNavigator.tsx +0 -93
@@ -17,25 +17,20 @@
17
17
  #import <react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.h>
18
18
  #import "TrueSheetContainerView.h"
19
19
  #import "TrueSheetFooterView.h"
20
- #import "TrueSheetView.h"
21
20
  #import "TrueSheetViewController.h"
22
21
  #import "utils/PlatformUtil.h"
23
22
  #import "utils/UIView+FirstResponder.h"
24
23
 
25
24
  using namespace facebook::react;
26
25
 
27
- static void *TrueSheetContentSizeContext = &TrueSheetContentSizeContext;
28
-
29
26
  @implementation TrueSheetContentView {
30
27
  TrueSheetContentViewShadowNode::ConcreteState::Shared _state;
31
- RCTScrollViewComponentView *_pinnedScrollView;
32
- UIScrollView *_observedScrollView;
33
- CGSize _lastSize;
34
- CGFloat _bottomInset;
35
- CGFloat _originalIndicatorBottomInset;
28
+ RCTScrollViewComponentView *_detectedScrollView;
29
+ CGFloat _lastReportedNaturalHeight;
30
+ CGFloat _appliedKeyboardOffset;
36
31
  BOOL _observingTextChanges;
37
- BOOL _scrollableBounded;
38
- BOOL _isReportPending;
32
+ UIScrollViewContentInsetAdjustmentBehavior _originalInsetAdjustmentBehavior;
33
+ BOOL _appliedContentInsetAdjustment;
39
34
  }
40
35
 
41
36
  + (ComponentDescriptorProvider)componentDescriptorProvider {
@@ -52,37 +47,51 @@ static void *TrueSheetContentSizeContext = &TrueSheetContentSizeContext;
52
47
 
53
48
  - (void)dealloc {
54
49
  [self stopObservingTextChanges];
55
- [self unobserveScrollViewContentSize];
56
50
  }
57
51
 
58
52
  - (void)updateState:(const State::Shared &)state oldState:(const State::Shared &)oldState {
59
53
  _state = std::static_pointer_cast<TrueSheetContentViewShadowNode::ConcreteState const>(state);
54
+
55
+ CGFloat naturalHeight = _state->getData().naturalHeight;
56
+ if (naturalHeight != _lastReportedNaturalHeight) {
57
+ _lastReportedNaturalHeight = naturalHeight;
58
+ [self.delegate contentViewDidChangeSize:CGSizeMake(self.frame.size.width, naturalHeight)];
59
+ }
60
60
  }
61
61
 
62
- // Tells the shadow node to fill the container (flexGrow/flexShrink) so the
63
- // pinned ScrollView's viewport is bounded to the visible space. Only applied
64
- // for auto detents — otherwise content lays out naturally like a regular view.
65
- - (void)setScrollableBounded:(BOOL)bounded {
66
- if (_scrollableBounded == bounded) {
62
+ - (void)setScrollableHandle:(NSInteger)scrollableHandle {
63
+ if (_scrollableHandle == scrollableHandle) {
67
64
  return;
68
65
  }
69
- _scrollableBounded = bounded;
66
+ _scrollableHandle = scrollableHandle;
70
67
 
71
- if (_state) {
72
- TrueSheetContentViewState newState;
73
- newState.scrollableBounded = bounded;
74
- _state->updateState(std::move(newState));
68
+ // Release the previously resolved ScrollView — setupScrollable re-resolves
69
+ [self clearScrollable];
70
+ }
71
+
72
+ - (void)setContentInsetAdjustment:(BOOL)contentInsetAdjustment {
73
+ if (_contentInsetAdjustment == contentInsetAdjustment) {
74
+ return;
75
75
  }
76
+ _contentInsetAdjustment = contentInsetAdjustment;
77
+ [self applyContentInsetAdjustment];
76
78
  }
77
79
 
78
- - (void)setHasAutoDetent:(BOOL)hasAutoDetent {
79
- if (_hasAutoDetent == hasAutoDetent) {
80
+ // Hands the bottom safe-area inset to UIKit — `automatic` only insets while
81
+ // the content can scroll, which the Android counterpart mirrors manually.
82
+ - (void)applyContentInsetAdjustment {
83
+ if (!_detectedScrollView) {
80
84
  return;
81
85
  }
82
- _hasAutoDetent = hasAutoDetent;
83
86
 
84
- if (_pinnedScrollView) {
85
- [self setScrollableBounded:hasAutoDetent];
87
+ UIScrollView *scrollView = _detectedScrollView.scrollView;
88
+ if (_contentInsetAdjustment && !_appliedContentInsetAdjustment) {
89
+ _originalInsetAdjustmentBehavior = scrollView.contentInsetAdjustmentBehavior;
90
+ scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentAutomatic;
91
+ _appliedContentInsetAdjustment = YES;
92
+ } else if (!_contentInsetAdjustment && _appliedContentInsetAdjustment) {
93
+ scrollView.contentInsetAdjustmentBehavior = _originalInsetAdjustmentBehavior;
94
+ _appliedContentInsetAdjustment = NO;
86
95
  }
87
96
  }
88
97
 
@@ -111,58 +120,25 @@ static void *TrueSheetContentSizeContext = &TrueSheetContentSizeContext;
111
120
  #pragma mark - Layout
112
121
 
113
122
  - (CGFloat)naturalHeight {
114
- CGFloat height = self.frame.size.height;
115
- if (_pinnedScrollView) {
116
- height += _pinnedScrollView.scrollView.contentSize.height - _pinnedScrollView.frame.size.height;
117
- }
118
- return MAX(0, height);
119
- }
120
-
121
- - (void)reportSizeIfChanged {
122
- // A deep ScrollView unmount doesn't pass through this view's mount hooks —
123
- // detect the stale pin here so it (and its contentSize observation) is
124
- // released instead of lingering until the next explicit setup or recycle.
125
- if (_pinnedScrollView && ![_pinnedScrollView isDescendantOfView:self]) {
126
- [self.delegate contentViewScrollViewDidChange];
127
- }
128
-
129
- // naturalHeight mixes frames from different views; mid-transaction they're
130
- // momentarily inconsistent (parent updates before the ScrollView), which
131
- // feeds back into the auto detent and oscillates. Coalesce to the next
132
- // main-queue tick so frames are settled before measuring.
133
- if (_pinnedScrollView) {
134
- if (_isReportPending) {
135
- return;
123
+ if (_state) {
124
+ CGFloat height = _state->getData().naturalHeight;
125
+ if (height > 0) {
126
+ return height;
136
127
  }
137
- _isReportPending = YES;
138
-
139
- __weak __typeof(self) weakSelf = self;
140
- dispatch_async(dispatch_get_main_queue(), ^{
141
- __typeof(self) strongSelf = weakSelf;
142
- if (!strongSelf) {
143
- return;
144
- }
145
- strongSelf->_isReportPending = NO;
146
- [strongSelf reportSizeNow];
147
- });
148
- return;
149
- }
150
-
151
- [self reportSizeNow];
152
- }
153
-
154
- - (void)reportSizeNow {
155
- CGSize newSize = CGSizeMake(self.frame.size.width, self.naturalHeight);
156
- if (!CGSizeEqualToSize(newSize, _lastSize)) {
157
- _lastSize = newSize;
158
- [self.delegate contentViewDidChangeSize:newSize];
159
128
  }
129
+ return self.frame.size.height;
160
130
  }
161
131
 
162
132
  - (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics
163
133
  oldLayoutMetrics:(const LayoutMetrics &)oldLayoutMetrics {
164
134
  [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
165
- [self reportSizeIfChanged];
135
+
136
+ // A deep ScrollView unmount doesn't pass through this view's mount hooks —
137
+ // detect the stale reference here so it's released instead of lingering until the
138
+ // next explicit setup or recycle.
139
+ if (_detectedScrollView && ![_detectedScrollView isDescendantOfView:self]) {
140
+ [self.delegate contentViewScrollViewDidChange];
141
+ }
166
142
  }
167
143
 
168
144
  #pragma mark - Child Mounting
@@ -178,46 +154,52 @@ static void *TrueSheetContentSizeContext = &TrueSheetContentSizeContext;
178
154
  }
179
155
 
180
156
  - (void)checkScrollViewChanged {
181
- if (!_pinnedScrollView || ![_pinnedScrollView isDescendantOfView:self]) {
157
+ if (!_detectedScrollView || ![_detectedScrollView isDescendantOfView:self]) {
158
+ [self.delegate contentViewScrollViewDidChange];
159
+ }
160
+ }
161
+
162
+ // A handle that commits in the same transaction as the ScrollView's insertion
163
+ // resolves to nothing — prop updates process before mount instructions, and a
164
+ // deep insertion doesn't pass through this view's mount hooks. Retry once the
165
+ // whole transaction has mounted.
166
+ - (void)mountingTransactionDidMount:(const MountingTransaction &)transaction
167
+ withSurfaceTelemetry:(const SurfaceTelemetry &)surfaceTelemetry {
168
+ if (_scrollableHandle > 0 && !_detectedScrollView) {
182
169
  [self.delegate contentViewScrollViewDidChange];
183
170
  }
184
171
  }
185
172
 
186
173
  #pragma mark - Scrollable
187
174
 
188
- - (void)setScrollViewContentInset:(CGFloat)contentBottom indicatorInset:(CGFloat)indicatorBottom {
189
- if (!_pinnedScrollView)
175
+ // The scroll indicator follows automatically — UIKit derives its insets from
176
+ // the content inset while `automaticallyAdjustsScrollIndicatorInsets` is set.
177
+ - (void)setKeyboardInset:(CGFloat)inset {
178
+ if (!_detectedScrollView)
190
179
  return;
191
180
 
192
- UIEdgeInsets contentInset = _pinnedScrollView.scrollView.contentInset;
193
- contentInset.bottom = contentBottom;
194
- _pinnedScrollView.scrollView.contentInset = contentInset;
195
-
196
- UIEdgeInsets indicatorInsets = _pinnedScrollView.scrollView.verticalScrollIndicatorInsets;
197
- indicatorInsets.bottom = indicatorBottom;
198
- _pinnedScrollView.scrollView.verticalScrollIndicatorInsets = indicatorInsets;
181
+ UIEdgeInsets contentInset = _detectedScrollView.scrollView.contentInset;
182
+ contentInset.bottom = inset;
183
+ _detectedScrollView.scrollView.contentInset = contentInset;
199
184
  }
200
185
 
201
186
  - (void)clearScrollable {
202
- if (_pinnedScrollView) {
203
- [self setScrollViewContentInset:0 indicatorInset:_originalIndicatorBottomInset];
204
- }
205
- [self unobserveScrollViewContentSize];
206
- [self setScrollableBounded:NO];
207
- _pinnedScrollView = nil;
208
- _bottomInset = 0;
209
- _originalIndicatorBottomInset = 0;
210
- [self reportSizeIfChanged];
187
+ [self setKeyboardInset:0];
188
+ _appliedKeyboardOffset = 0;
189
+ if (_appliedContentInsetAdjustment) {
190
+ _detectedScrollView.scrollView.contentInsetAdjustmentBehavior = _originalInsetAdjustmentBehavior;
191
+ _appliedContentInsetAdjustment = NO;
192
+ }
193
+ _detectedScrollView = nil;
211
194
  }
212
195
 
213
- - (void)setupScrollableWithBottomInset:(CGFloat)bottomInset {
214
- // Check if pinned scroll view is still valid (still in view hierarchy)
215
- if (_pinnedScrollView && ![_pinnedScrollView isDescendantOfView:self]) {
196
+ - (void)setupScrollable {
197
+ // Check if the detected scroll view is still valid (still in view hierarchy)
198
+ if (_detectedScrollView && ![_detectedScrollView isDescendantOfView:self]) {
216
199
  [self clearScrollable];
217
200
  }
218
201
 
219
- // Already set up with same inset and valid scroll view
220
- if (_pinnedScrollView && _bottomInset == bottomInset) {
202
+ if (_detectedScrollView) {
221
203
  return;
222
204
  }
223
205
 
@@ -226,104 +208,74 @@ static void *TrueSheetContentSizeContext = &TrueSheetContentSizeContext;
226
208
  return;
227
209
  }
228
210
 
229
- // Only capture originals on first pin
230
- if (!_pinnedScrollView) {
231
- _originalIndicatorBottomInset = scrollView.scrollView.verticalScrollIndicatorInsets.bottom;
232
- _pinnedScrollView = scrollView;
233
-
234
- [self observeScrollViewContentSize];
235
- [self setScrollableBounded:_hasAutoDetent];
236
- [self reportSizeIfChanged];
237
- }
238
-
239
- _bottomInset = bottomInset;
211
+ _detectedScrollView = scrollView;
240
212
 
241
- [self setScrollViewContentInset:_bottomInset indicatorInset:_originalIndicatorBottomInset];
213
+ [self applyContentInsetAdjustment];
242
214
 
243
215
  // If keyboard is currently showing, re-apply the keyboard inset to the new ScrollView
244
216
  CGFloat keyboardHeight = _keyboardObserver ? _keyboardObserver.currentHeight : 0;
245
217
  if (keyboardHeight > 0) {
246
- CGFloat inset = [self keyboardInsetWithHeight:keyboardHeight];
247
- [self setScrollViewContentInset:inset indicatorInset:_originalIndicatorBottomInset + inset];
218
+ [self setKeyboardInset:[self keyboardInsetWithHeight:keyboardHeight]];
248
219
  }
249
220
  }
250
221
 
251
- // An absolute footer rises above the keyboard and covers the content's bottom
252
- // edge — include it so the caret clears the footer, not just the keyboard.
253
222
  // A relative footer stays behind the keyboard below the content, so its
254
- // height shields that much of the keyboard's overlap.
223
+ // height shields that much of the keyboard's overlap. An absolute footer
224
+ // floats within the viewport — its clearance is the content padding's job
225
+ // (the caret reveal accounts for it, see footerOcclusion).
255
226
  - (CGFloat)keyboardInsetWithHeight:(CGFloat)height {
256
- if (!self.footerView) {
257
- return height;
258
- }
259
- if (_keyboardObserver.viewController.absoluteFooter) {
260
- return height + [self.footerView keyboardOcclusionHeight];
227
+ CGFloat inset = height;
228
+ if (self.footerView && !_keyboardObserver.viewController.absoluteFooter) {
229
+ inset = MAX(0, height - self.footerView.frame.size.height);
261
230
  }
262
- return MAX(0, height - self.footerView.frame.size.height);
263
- }
264
231
 
265
- // Content growth is invisible to layout once the viewport is bounded, so track
266
- // the scroll content size directly to keep the auto detent height in sync.
267
- - (void)observeScrollViewContentSize {
268
- UIScrollView *scrollView = _pinnedScrollView.scrollView;
269
- if (_observedScrollView == scrollView) {
270
- return;
232
+ // UIKit stacks the safe area on top of contentInset while the adjustment
233
+ // behavior is automatic take it out so the total lands on the keyboard edge.
234
+ if (_appliedContentInsetAdjustment) {
235
+ inset = MAX(0, inset - _detectedScrollView.scrollView.safeAreaInsets.bottom);
271
236
  }
272
- [self unobserveScrollViewContentSize];
273
- _observedScrollView = scrollView;
274
- [scrollView addObserver:self
275
- forKeyPath:@"contentSize"
276
- options:NSKeyValueObservingOptionNew
277
- context:TrueSheetContentSizeContext];
278
- }
279
237
 
280
- - (void)unobserveScrollViewContentSize {
281
- if (_observedScrollView) {
282
- [_observedScrollView removeObserver:self forKeyPath:@"contentSize" context:TrueSheetContentSizeContext];
283
- _observedScrollView = nil;
238
+ // Track how much of keyboardOffset actually lands in the inset so the caret
239
+ // reveal can compensate — the offset shifts the inset, not the keyboard edge.
240
+ CGFloat adjustedInset = MAX(0, inset + self.keyboardOffset);
241
+ _appliedKeyboardOffset = adjustedInset - inset;
242
+
243
+ // Content that already fits above the keyboard has nothing to reveal — the
244
+ // inset would only open blank scroll range below it (huge gap at the bottom).
245
+ UIScrollView *scrollView = _detectedScrollView.scrollView;
246
+ if (scrollView.contentSize.height <= scrollView.bounds.size.height - adjustedInset) {
247
+ _appliedKeyboardOffset = 0;
248
+ return 0;
284
249
  }
250
+
251
+ return adjustedInset;
285
252
  }
286
253
 
287
- - (void)observeValueForKeyPath:(NSString *)keyPath
288
- ofObject:(id)object
289
- change:(NSDictionary *)change
290
- context:(void *)context {
291
- if (context == TrueSheetContentSizeContext) {
292
- [self reportSizeIfChanged];
293
- return;
254
+ // An absolute footer floats over the viewport's bottom edge — extend the
255
+ // caret target so it clears the footer, not just the keyboard.
256
+ - (CGFloat)footerOcclusion {
257
+ if (self.footerView && _keyboardObserver.viewController.absoluteFooter) {
258
+ return [self.footerView keyboardOcclusionHeight];
294
259
  }
295
- [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
260
+ return 0;
296
261
  }
297
262
 
263
+ // Resolves the user-provided `scrollableHandle` within the content subtree —
264
+ // Fabric component views carry their React tag as `UIView.tag`.
298
265
  - (RCTScrollViewComponentView *)findScrollView {
299
- if (_pinnedScrollView) {
300
- return _pinnedScrollView;
266
+ if (_detectedScrollView) {
267
+ return _detectedScrollView;
301
268
  }
302
269
 
303
- if (self.subviews.count == 0) {
270
+ if (_scrollableHandle <= 0) {
304
271
  return nil;
305
272
  }
306
273
 
307
- RCTScrollViewComponentView *scrollView = [self findScrollViewInSubviews:self.subviews];
308
-
309
- if (!scrollView) {
310
- for (UIView *subview in self.subviews) {
311
- scrollView = [self findScrollViewInSubviews:subview.subviews];
312
- if (scrollView) {
313
- break;
314
- }
315
- }
274
+ UIView *view = [self viewWithTag:_scrollableHandle];
275
+ if ([view isKindOfClass:RCTScrollViewComponentView.class]) {
276
+ return (RCTScrollViewComponentView *)view;
316
277
  }
317
278
 
318
- return scrollView;
319
- }
320
-
321
- - (RCTScrollViewComponentView *)findScrollViewInSubviews:(NSArray<UIView *> *)subviews {
322
- for (UIView *subview in subviews) {
323
- if ([subview isKindOfClass:RCTScrollViewComponentView.class] && ![subview isKindOfClass:TrueSheetView.class]) {
324
- return (RCTScrollViewComponentView *)subview;
325
- }
326
- }
327
279
  return nil;
328
280
  }
329
281
 
@@ -331,11 +283,11 @@ static void *TrueSheetContentSizeContext = &TrueSheetContentSizeContext;
331
283
 
332
284
  - (void)applyScrollEdgeEffects:(nullable ScrollableOptions *)options {
333
285
  #if RNTS_IPHONE_OS_VERSION_AVAILABLE(26_0)
334
- if (!_pinnedScrollView)
286
+ if (!_detectedScrollView)
335
287
  return;
336
288
 
337
289
  if (@available(iOS 26.0, *)) {
338
- UIScrollView *scrollView = _pinnedScrollView.scrollView;
290
+ UIScrollView *scrollView = _detectedScrollView.scrollView;
339
291
  auto topEffect = options ? options.topScrollEdgeEffect : TrueSheetViewTopScrollEdgeEffect::Hidden;
340
292
  auto bottomEffect = options ? options.bottomScrollEdgeEffect : TrueSheetViewBottomScrollEdgeEffect::Hidden;
341
293
 
@@ -371,7 +323,7 @@ static void *TrueSheetContentSizeContext = &TrueSheetContentSizeContext;
371
323
  #pragma mark - TrueSheetKeyboardObserverDelegate
372
324
 
373
325
  - (void)keyboardWillShow:(CGFloat)height duration:(NSTimeInterval)duration curve:(UIViewAnimationOptions)curve {
374
- if (!_pinnedScrollView) {
326
+ if (!_detectedScrollView) {
375
327
  return;
376
328
  }
377
329
 
@@ -385,7 +337,7 @@ static void *TrueSheetContentSizeContext = &TrueSheetContentSizeContext;
385
337
  delay:0
386
338
  options:curve | UIViewAnimationOptionBeginFromCurrentState
387
339
  animations:^{
388
- [self setScrollViewContentInset:inset indicatorInset:self->_originalIndicatorBottomInset + inset];
340
+ [self setKeyboardInset:inset];
389
341
  }
390
342
  completion:nil];
391
343
 
@@ -398,7 +350,7 @@ static void *TrueSheetContentSizeContext = &TrueSheetContentSizeContext;
398
350
  }
399
351
 
400
352
  - (void)focusedInputTextDidChange:(NSNotification *)notification {
401
- if (!_pinnedScrollView || !_keyboardObserver || _keyboardObserver.currentHeight <= 0) {
353
+ if (!_detectedScrollView || !_keyboardObserver || _keyboardObserver.currentHeight <= 0) {
402
354
  return;
403
355
  }
404
356
 
@@ -409,12 +361,15 @@ static void *TrueSheetContentSizeContext = &TrueSheetContentSizeContext;
409
361
  }
410
362
 
411
363
  dispatch_async(dispatch_get_main_queue(), ^{
364
+ // Typing can grow the content (e.g. a multiline input) past the fits-above-
365
+ // the-keyboard threshold — keep the inset in sync before revealing the caret.
366
+ [self setKeyboardInset:[self keyboardInsetWithHeight:self->_keyboardObserver.currentHeight]];
412
367
  [self scrollToFocusedCaretAnimated:YES];
413
368
  });
414
369
  }
415
370
 
416
371
  - (void)scrollToFocusedCaretAnimated:(BOOL)animated {
417
- if (!_pinnedScrollView) {
372
+ if (!_detectedScrollView) {
418
373
  return;
419
374
  }
420
375
 
@@ -424,7 +379,7 @@ static void *TrueSheetContentSizeContext = &TrueSheetContentSizeContext;
424
379
  return;
425
380
  }
426
381
 
427
- UIScrollView *scrollView = _pinnedScrollView.scrollView;
382
+ UIScrollView *scrollView = _detectedScrollView.scrollView;
428
383
  CGRect targetRect = [firstResponder convertRect:firstResponder.bounds toView:scrollView];
429
384
 
430
385
  if ([firstResponder conformsToProtocol:@protocol(UITextInput)]) {
@@ -442,14 +397,14 @@ static void *TrueSheetContentSizeContext = &TrueSheetContentSizeContext;
442
397
  }
443
398
  }
444
399
 
445
- targetRect.size.height += self.keyboardScrollOffset;
400
+ targetRect.size.height += self.keyboardScrollOffset + [self footerOcclusion] - _appliedKeyboardOffset;
446
401
  [scrollView scrollRectToVisible:targetRect animated:animated];
447
402
  }
448
403
 
449
404
  - (void)keyboardWillHide:(NSTimeInterval)duration curve:(UIViewAnimationOptions)curve {
450
405
  [self stopObservingTextChanges];
451
406
 
452
- if (!_pinnedScrollView) {
407
+ if (!_detectedScrollView) {
453
408
  return;
454
409
  }
455
410
 
@@ -457,8 +412,7 @@ static void *TrueSheetContentSizeContext = &TrueSheetContentSizeContext;
457
412
  delay:0
458
413
  options:curve | UIViewAnimationOptionBeginFromCurrentState
459
414
  animations:^{
460
- [self setScrollViewContentInset:self->_bottomInset
461
- indicatorInset:self->_originalIndicatorBottomInset];
415
+ [self setKeyboardInset:0];
462
416
  }
463
417
  completion:nil];
464
418
  }
@@ -470,9 +424,9 @@ static void *TrueSheetContentSizeContext = &TrueSheetContentSizeContext;
470
424
  [self stopObservingTextChanges];
471
425
  [self clearScrollable];
472
426
  _state.reset();
473
- _scrollableBounded = NO;
474
- _hasAutoDetent = NO;
475
- _lastSize = CGSizeZero;
427
+ _scrollableHandle = 0;
428
+ _contentInsetAdjustment = NO;
429
+ _lastReportedNaturalHeight = 0;
476
430
  }
477
431
 
478
432
  @end
@@ -27,6 +27,12 @@ NS_ASSUME_NONNULL_BEGIN
27
27
  @property (nonatomic, weak, nullable) TrueSheetKeyboardObserver *keyboardObserver;
28
28
  @property (nonatomic, weak, nullable) id<TrueSheetFooterViewDelegate> delegate;
29
29
 
30
+ /**
31
+ * Bottom inset currently baked into the footer's layout height — pairs with
32
+ * the measured frame so detent math can subtract exactly what's baked in.
33
+ */
34
+ @property (nonatomic, readonly) CGFloat appliedBottomInset;
35
+
30
36
  /**
31
37
  * Re-applies the footer's keyboard slide using the current keyboard height.
32
38
  * No-op when the keyboard is hidden. Used to reflect live `keyboardOffset` changes.
@@ -25,6 +25,8 @@ using namespace facebook::react;
25
25
  CGFloat _currentKeyboardOffset;
26
26
  CGFloat _bottomInset;
27
27
  BOOL _keyboardVisible;
28
+ BOOL _didPushBottomInset;
29
+ CGFloat _appliedBottomInset;
28
30
  }
29
31
 
30
32
  + (ComponentDescriptorProvider)componentDescriptorProvider {
@@ -43,6 +45,8 @@ using namespace facebook::react;
43
45
  _currentKeyboardOffset = 0;
44
46
  _bottomInset = 0;
45
47
  _keyboardVisible = NO;
48
+ _didPushBottomInset = NO;
49
+ _appliedBottomInset = 0;
46
50
  }
47
51
  return self;
48
52
  }
@@ -55,7 +59,11 @@ using namespace facebook::react;
55
59
  // bottom safe-area inset — the footer owns the sheet's bottom edge, so it
56
60
  // absorbs the inset and its background fills it.
57
61
  - (void)setBottomInset:(CGFloat)bottomInset {
58
- if (_bottomInset == bottomInset) {
62
+ // The first push must go through even when the value matches the ivar
63
+ // default (0) — it marks the state initialized so the shadow node stops
64
+ // seeding from the app-wide precalculated inset, which may belong to
65
+ // another sheet (e.g. a floating sheet whose real inset is 0).
66
+ if (_didPushBottomInset && _bottomInset == bottomInset) {
59
67
  return;
60
68
  }
61
69
  _bottomInset = bottomInset;
@@ -85,6 +93,14 @@ using namespace facebook::react;
85
93
  // them an inset stale (same reason Android bridges through
86
94
  // TrueSheetStateUpdater).
87
95
  _state->updateState(std::move(newState), EventQueue::UpdateMode::unstable_Immediate);
96
+ _didPushBottomInset = YES;
97
+ }
98
+
99
+ // The inset baked into the current layout by the shadow node (pushed or
100
+ // seeded — see TrueSheetFooterViewShadowNode). Refreshed in
101
+ // updateLayoutMetrics so it always pairs with the measured height.
102
+ - (CGFloat)appliedBottomInset {
103
+ return _appliedBottomInset;
88
104
  }
89
105
 
90
106
  // Height the footer occupies above the keyboard — its layout height minus the
@@ -123,8 +139,19 @@ using namespace facebook::react;
123
139
  oldLayoutMetrics:(const facebook::react::LayoutMetrics &)oldLayoutMetrics {
124
140
  [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
125
141
 
142
+ // State lands before layout metrics within the same mount transaction, so
143
+ // this reflects the inset the shadow node baked into this frame — seeded
144
+ // values are recorded back into the state at layout time (see
145
+ // TrueSheetFooterViewShadowNode).
146
+ CGFloat previousAppliedBottomInset = _appliedBottomInset;
147
+ if (_state) {
148
+ _appliedBottomInset = _state->getData().bottomInset;
149
+ }
150
+
126
151
  CGFloat height = layoutMetrics.frame.size.height;
127
- if (height != _lastHeight) {
152
+ // An inset change alone (fixed-height footer: padding moves, frame doesn't)
153
+ // still changes what's baked in — report so the controller re-pairs.
154
+ if (height != _lastHeight || _appliedBottomInset != previousAppliedBottomInset) {
128
155
  _lastHeight = height;
129
156
  [self.delegate footerViewDidChangeSize:CGSizeMake(layoutMetrics.frame.size.width, height)];
130
157
  }
@@ -143,6 +170,8 @@ using namespace facebook::react;
143
170
  _currentKeyboardOffset = 0;
144
171
  _bottomInset = 0;
145
172
  _keyboardVisible = NO;
173
+ _didPushBottomInset = NO;
174
+ _appliedBottomInset = 0;
146
175
  }
147
176
 
148
177
  #pragma mark - TrueSheetKeyboardObserverDelegate
@@ -167,6 +196,7 @@ using namespace facebook::react;
167
196
  options:curve | UIViewAnimationOptionBeginFromCurrentState
168
197
  animations:^{
169
198
  self.transform = CGAffineTransformMakeTranslation(0, -slide);
199
+ [self syncEdgeEffect];
170
200
  }
171
201
  completion:nil];
172
202
  }
@@ -180,10 +210,26 @@ using namespace facebook::react;
180
210
  options:curve | UIViewAnimationOptionBeginFromCurrentState
181
211
  animations:^{
182
212
  self.transform = CGAffineTransformIdentity;
213
+ [self syncEdgeEffect];
183
214
  }
184
215
  completion:nil];
185
216
  }
186
217
 
218
+ // UIKit derives the edge effect region from model geometry at commit time,
219
+ // outside our animation context — the blur snaps to the footer's final
220
+ // position while the footer is still animating. Re-evaluating the scroll
221
+ // view's layout inside the animation block makes the region change inherit
222
+ // the keyboard curve.
223
+ - (void)syncEdgeEffect {
224
+ if (@available(iOS 26.0, *)) {
225
+ UIScrollView *scrollView = [self edgeInteractionScrollView];
226
+ if (scrollView) {
227
+ [scrollView setNeedsLayout];
228
+ [scrollView layoutIfNeeded];
229
+ }
230
+ }
231
+ }
232
+
187
233
  - (void)applyKeyboardOffset {
188
234
  CGFloat height = self.keyboardObserver.currentHeight;
189
235
  if (height <= 0 || !self.keyboardObserver.viewController.absoluteFooter) {
@@ -27,16 +27,11 @@ NS_ASSUME_NONNULL_BEGIN
27
27
 
28
28
  /**
29
29
  * Register a sheet component view with its React tag
30
- * Called automatically by TrueSheetView during initialization
30
+ * Called automatically by TrueSheetView when it attaches to a window.
31
+ * The registry holds views weakly, so no unregistration is needed.
31
32
  */
32
33
  + (void)registerView:(TrueSheetView *)view withTag:(NSNumber *)tag;
33
34
 
34
- /**
35
- * Unregister a sheet component view
36
- * Called automatically by TrueSheetView during dealloc
37
- */
38
- + (void)unregisterViewWithTag:(NSNumber *)tag;
39
-
40
35
  @end
41
36
 
42
37
  NS_ASSUME_NONNULL_END