@momo-kits/native-kits 0.167.1-duo.1-debug → 0.167.1-duo.10-debug

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 (177) hide show
  1. package/CLAUDE.md +32 -0
  2. package/compose/build.gradle.kts +15 -1
  3. package/compose/build.gradle.kts.backup +14 -0
  4. package/compose/compose.podspec +1 -1
  5. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +2 -0
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
  7. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +20 -21
  8. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +2 -1
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +4 -2
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +1 -1
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +1 -1
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +1 -1
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +1 -1
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +9 -6
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +11 -12
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +4 -9
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +2 -2
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +10 -9
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +6 -6
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +53 -26
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +12 -2
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +7 -9
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +1 -1
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +13 -6
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +13 -5
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +3 -2
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +14 -6
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +19 -21
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +2 -2
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +5 -2
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +3 -1
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +7 -15
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +13 -11
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +21 -9
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +232 -0
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +11 -9
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +43 -4
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +8 -0
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +4 -0
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +143 -31
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +100 -66
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +22 -16
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +98 -0
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +312 -0
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +66 -18
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +23 -14
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +3 -3
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +1 -2
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +81 -24
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +1 -2
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +185 -0
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +2 -1
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +3 -0
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +13 -0
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +4 -1
  56. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +4 -0
  57. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +86 -0
  58. package/gradle.properties +1 -1
  59. package/ios/Application/ApplicationEnvironment.swift +17 -0
  60. package/ios/Application/ComposeApi.swift +2 -1
  61. package/ios/Application/{Components.swift → LegacyHeader.swift} +7 -65
  62. package/ios/Application/Screen.swift +3 -2
  63. package/ios/Avatar/Avatar.swift +4 -3
  64. package/ios/Badge/Badge.swift +6 -4
  65. package/ios/Badge/BadgeDot.swift +1 -1
  66. package/ios/Badge/BadgeRibbon.swift +0 -1
  67. package/ios/Button/Button.swift +15 -12
  68. package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +64 -18
  69. package/ios/Carousel/Carousel.swift +29 -12
  70. package/ios/Checkbox/Checkbox.swift +7 -3
  71. package/ios/Chip/Chip.swift +5 -4
  72. package/ios/Collapse/Collapse.swift +70 -19
  73. package/ios/Colors+Radius+Spacing/Colors.swift +10 -1
  74. package/ios/DateTimePicker/DateTimePicker.swift +54 -20
  75. package/ios/DateTimePicker/WheelPicker.swift +103 -105
  76. package/ios/Divider/Divider.swift +37 -4
  77. package/ios/Extensions/Color++.swift +38 -9
  78. package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
  79. package/ios/Extensions/SkeletonUICompat.swift +11 -0
  80. package/ios/Icon/Icon.swift +21 -23
  81. package/ios/Icon/Icons.swift +1329 -0
  82. package/ios/IconButton/IconButton.swift +15 -7
  83. package/ios/Image/Image.swift +7 -23
  84. package/ios/Information/Information.swift +27 -9
  85. package/ios/Input/ErrorView.swift +2 -2
  86. package/ios/Input/Input.swift +160 -147
  87. package/ios/Input/InputPhoneNumber.swift +12 -9
  88. package/ios/Input/InputSearch.swift +102 -75
  89. package/ios/Input/InputTextArea.swift +128 -145
  90. package/ios/InputDropDown/InputDropDown.swift +10 -7
  91. package/ios/InputMoney/InputMoney.swift +33 -11
  92. package/ios/InputOTP/InputOTP.swift +22 -12
  93. package/ios/Layout/Card.swift +50 -0
  94. package/ios/Layout/GridContext.swift +123 -3
  95. package/ios/Layout/Item.swift +5 -4
  96. package/ios/Layout/ScreenLayout.swift +312 -0
  97. package/ios/Layout/Section.swift +4 -100
  98. package/ios/Loader/Loader.swift +5 -9
  99. package/ios/Lottie/LottieView.swift +14 -2
  100. package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +97 -8
  101. package/ios/Navigation/BottomTab/BottomTab.swift +187 -0
  102. package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +15 -7
  103. package/ios/Navigation/BottomTab/DuoBarCompression.swift +105 -0
  104. package/ios/Navigation/BottomTab/DuoTabBar.swift +304 -0
  105. package/ios/{Application → Navigation/Component}/FloatingButton.swift +14 -3
  106. package/ios/Navigation/Component/Header.swift +493 -0
  107. package/ios/Navigation/Component/HeaderBackground.swift +134 -0
  108. package/ios/Navigation/Component/HeaderJourney.swift +73 -0
  109. package/ios/Navigation/Component/HeaderLocation.swift +48 -0
  110. package/ios/Navigation/Component/HeaderRight.swift +386 -0
  111. package/ios/Navigation/Component/HeaderTitle.swift +36 -0
  112. package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +21 -24
  113. package/ios/Navigation/Component/SideBar.swift +176 -0
  114. package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +5 -5
  115. package/ios/Navigation/HostBackGestureBridge.swift +57 -0
  116. package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +5 -1
  117. package/ios/Navigation/Navigation.swift +190 -0
  118. package/ios/{Application/Navigation → Navigation}/NavigationContainer.swift +92 -81
  119. package/ios/{Application/Navigation → Navigation}/Navigator.swift +185 -36
  120. package/ios/Navigation/StackScreen.swift +782 -0
  121. package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
  122. package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
  123. package/ios/Pagination/PaginationScroll.swift +2 -2
  124. package/ios/Popup/PopupDisplay.swift +72 -136
  125. package/ios/Popup/PopupInput.swift +10 -13
  126. package/ios/Popup/PopupLayout.swift +140 -0
  127. package/ios/Popup/PopupNotify.swift +56 -73
  128. package/ios/Popup/PopupPromotion.swift +31 -47
  129. package/ios/PopupView/FullscreenPopup.swift +13 -19
  130. package/ios/PopupView/PopupView.swift +1 -1
  131. package/ios/PopupView/Utils++.swift +0 -8
  132. package/ios/ProgressInfo/ProgressInfo.swift +4 -2
  133. package/ios/Radio/Radio.swift +2 -1
  134. package/ios/Rating/Rating.swift +9 -1
  135. package/ios/Resources/dot_loading.json +558 -0
  136. package/ios/Resources/lottie_circle_loader.json +1 -0
  137. package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
  138. package/ios/Skeleton/Skeleton.swift +22 -27
  139. package/ios/Slider/Slider.swift +16 -18
  140. package/ios/Stepper/Stepper.swift +6 -1
  141. package/ios/Steps/Steps.swift +8 -6
  142. package/ios/SuggestAction/SuggestAction.swift +5 -0
  143. package/ios/Swipeable/SwipeCell.swift +10 -6
  144. package/ios/Switch/Switch.swift +22 -20
  145. package/ios/TabView/TabView.swift +102 -21
  146. package/ios/Tag/Tag.swift +15 -4
  147. package/ios/Template/Logo/Logo.swift +2 -1
  148. package/ios/Template/TrustBanner/TrustBanner.swift +106 -54
  149. package/ios/Title/Title.swift +6 -17
  150. package/ios/Tooltip/Tooltip.swift +29 -14
  151. package/ios/Typography/Text.swift +61 -19
  152. package/ios/Uploader/Uploader.swift +3 -1
  153. package/ios/native-kits.podspec +2 -1
  154. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  155. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
  156. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
  157. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +1 -0
  158. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
  159. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
  160. package/package.json +1 -1
  161. package/scripts/gen-ios-icons.mjs +36 -0
  162. package/.claude/settings.local.json +0 -185
  163. package/ios/Application/HeaderRight.swift +0 -306
  164. package/ios/Application/Navigation/BottomTab/BottomTab.swift +0 -104
  165. package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
  166. package/ios/Application/Navigation/HeaderTitle.swift +0 -57
  167. package/ios/Application/Navigation/NavigationOptions.swift +0 -109
  168. package/ios/Application/Navigation/NavigatorManager.swift +0 -22
  169. package/ios/Application/Navigation/component/Header.swift +0 -178
  170. package/ios/Application/Navigation/component/HeaderBackground.swift +0 -83
  171. package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
  172. package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
  173. package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
  174. package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -196
  175. package/ios/Application/StackScreen.swift +0 -492
  176. package/local.properties +0 -8
  177. /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
@@ -0,0 +1,782 @@
1
+ //
2
+ // StackScreen.swift
3
+ // MoMoUIKits
4
+ //
5
+ // SwiftUI mirror of Compose's `navigation/StackScreen.kt`: header layer, scrollable content
6
+ // (`StackScreenContent`), animated search, floating button, footer (`NavigationFooter`) and the
7
+ // iPhone Duo side bar.
8
+ //
9
+
10
+ import SwiftUI
11
+
12
+ public struct StackScreen<Content: View>: View {
13
+ // MARK: - Properties
14
+
15
+ var backgroundColor: Color?
16
+ var headerTransparent: Bool = false
17
+ var fullScreenContent: Bool = false
18
+ var tintColor: Color? = Colors.black17
19
+ var isBack: Bool
20
+ var headerType: HeaderType
21
+ var useAnimated: Bool = false
22
+ var verticalAlignment: VerticalAlignment
23
+ var horizontalAlignment: HorizontalAlignment
24
+ var title: String
25
+ var headerTitle: HeaderTitle?
26
+ var titlePosition: TitlePosition
27
+ var goBack: (() -> Void)?
28
+ var scrollable: Bool
29
+ var scrollToTopCallback: (() -> Void)?
30
+ var onContentLayout: ((CGRect) -> Void)?
31
+ var useAvoidKeyboard: Bool
32
+ var keyboardShouldPersistTaps: Bool
33
+ var footer: (() -> AnyView)?
34
+ var footerTransparent: Bool
35
+ var footerUseMargin: Bool
36
+ var headerRight: (() -> any View)? = { HeaderRight() }
37
+ var animatedHeader: AnimatedHeader? = nil
38
+ var layoutOffset: CGFloat
39
+ var inputSearchProps: InputSearchProps?
40
+ var fabProps: FabProps?
41
+ var screenName: String = ""
42
+ var content: () -> Content
43
+
44
+ public init(
45
+ backgroundColor: Color? = nil,
46
+ headerTransparent: Bool = false,
47
+ fullScreenContent: Bool = false,
48
+ tintColor: Color = Colors.black17,
49
+ isBack: Bool = true,
50
+ headerType: HeaderType = .default,
51
+ useAnimated: Bool = false,
52
+ verticalAlignment: VerticalAlignment = .top,
53
+ horizontalAlignment: HorizontalAlignment = .leading,
54
+ title: String = "Stack",
55
+ headerTitle: HeaderTitle? = nil,
56
+ titlePosition: TitlePosition = .left,
57
+ goBack: (() -> Void)? = nil,
58
+ scrollable: Bool = true,
59
+ scrollToTopCallback: (() -> Void)? = nil,
60
+ onContentLayout: ((CGRect) -> Void)? = nil,
61
+ useAvoidKeyboard: Bool = true,
62
+ keyboardShouldPersistTaps: Bool = false,
63
+ footer: (() -> AnyView)? = nil,
64
+ footerTransparent: Bool = false,
65
+ footerUseMargin: Bool = true,
66
+ headerRight: (() -> any View)? = { HeaderRight() },
67
+ animatedHeader: AnimatedHeader? = nil,
68
+ layoutOffset: CGFloat = 56,
69
+ inputSearchProps: InputSearchProps? = nil,
70
+ fabProps: FabProps? = nil,
71
+ screenName: String = "",
72
+ @ViewBuilder content: @escaping () -> Content
73
+ ) {
74
+ self.backgroundColor = backgroundColor
75
+ self.headerTransparent = headerTransparent
76
+ self.fullScreenContent = fullScreenContent
77
+ self.tintColor = tintColor
78
+ self.isBack = isBack
79
+ self.headerType = headerType
80
+ self.useAnimated = useAnimated
81
+ self.verticalAlignment = verticalAlignment
82
+ self.horizontalAlignment = horizontalAlignment
83
+ self.title = title
84
+ self.headerTitle = headerTitle
85
+ self.titlePosition = titlePosition
86
+ self.goBack = goBack
87
+ self.scrollable = scrollable
88
+ self.scrollToTopCallback = scrollToTopCallback
89
+ self.onContentLayout = onContentLayout
90
+ self.useAvoidKeyboard = useAvoidKeyboard
91
+ self.keyboardShouldPersistTaps = keyboardShouldPersistTaps
92
+ self.footer = footer
93
+ self.footerTransparent = footerTransparent
94
+ self.footerUseMargin = footerUseMargin
95
+ self.headerRight = headerRight
96
+ self.animatedHeader = animatedHeader
97
+ self.layoutOffset = layoutOffset
98
+ self.inputSearchProps = inputSearchProps
99
+ self.fabProps = fabProps
100
+ self.screenName = screenName
101
+ self.content = content
102
+ }
103
+
104
+ @State private var scrollOffset: CGFloat = 0
105
+ @State private var keyboardHeight: CGFloat = 0
106
+ @State private var headerRightWidth: CGFloat = 0
107
+ @State private var isBottomTabRoot: Bool = false
108
+ @State private var headerHide: CGFloat = 0
109
+ @State private var lastScrollOffset: CGFloat = 0
110
+ @State private var headerSettle: Task<Void, Never>?
111
+ @State private var footerHeight: CGFloat = 0
112
+
113
+ @Environment(\.applicationEnvironment) private var appEnv
114
+ @Environment(\.screenLayout) private var layout
115
+ @Environment(\.duoTabBarInsets) private var duoTabBarInsets
116
+ @Environment(\.appTheme) private var theme
117
+ @OptionalEnvironmentObject private var navigation: Navigation?
118
+
119
+ // Auto-tracking state (Compose's `remember { ScreenTrackingState() }` + the once-computed action).
120
+ @State private var trackingState = ScreenTrackingState()
121
+ @State private var trackingAction: String = ScreenTracker.getLastScreenName() != nil ? "push" : "back"
122
+
123
+ /// Compose's `headerOpacity()`: 0 over a banner or transparent header, 1 once the bar is opaque.
124
+ private var opacity: CGFloat {
125
+ headerOpacity(
126
+ scrollOffset: scrollOffset,
127
+ isAnimated: animatedHeader != nil,
128
+ layoutOffset: layoutOffset,
129
+ transparent: headerTransparent
130
+ )
131
+ }
132
+
133
+ private var headerColor: HeaderColor {
134
+ getHeaderColor(
135
+ isAnimated: animatedHeader != nil,
136
+ isSurface: animatedHeader?.isSurface ?? true,
137
+ opacity: opacity,
138
+ tintColor: tintColor
139
+ )
140
+ }
141
+
142
+ // MARK: - Search header (Compose's getInputSearchType / SearchAnimated)
143
+
144
+ private enum InputSearchType { case none, header, animated }
145
+
146
+ private var inputSearchType: InputSearchType {
147
+ guard inputSearchProps != nil, headerType != .none else { return .none }
148
+ return useAnimated ? .animated : .header
149
+ }
150
+
151
+ private var toolkit: (data: HeaderRightData?, tintColor: Color?)? { toolkitHeaderRight(headerRight) }
152
+
153
+ private var showsSideBar: Bool {
154
+ layout.sideBar != nil && headerType != .none && !isBottomTabRoot && (goBack != nil || toolkit != nil)
155
+ }
156
+
157
+ /// iPhone Duo: content scrolls under a glass header instead of starting below it.
158
+ private var useDuoGlass: Bool {
159
+ layout.sideBar != nil && headerType != .none && animatedHeader == nil
160
+ && inputSearchProps == nil && !headerTransparent && !fullScreenContent
161
+ }
162
+
163
+ /// iPhone Duo: the header leaves with the content and slides back in on a reverse scroll
164
+ /// (RN DuoQuickReturnHeader). The side bar is drawn apart from the header, so it stays put.
165
+ private var useQuickReturn: Bool {
166
+ layout.sideBar != nil && scrollable && (useDuoGlass || animatedHeader != nil)
167
+ }
168
+
169
+ private func updateHeaderHide(_ offset: CGFloat) {
170
+ let delta = offset - lastScrollOffset
171
+ lastScrollOffset = offset
172
+ guard useQuickReturn, offset > 0 else {
173
+ if headerHide != 0 { headerHide = 0 }
174
+ return
175
+ }
176
+ headerHide = min(max(headerHide + delta, 0), layout.headerHeight)
177
+ settleHeaderWhenScrollStops()
178
+ }
179
+
180
+ /// A half-shown header settles fully in or out. SwiftUI before iOS 18 reports no scroll
181
+ /// phase, so the scroll counts as ended once the offset stops changing.
182
+ private func settleHeaderWhenScrollStops() {
183
+ headerSettle?.cancel()
184
+ headerSettle = Task { @MainActor in
185
+ try? await Task.sleep(nanoseconds: 150_000_000)
186
+ guard !Task.isCancelled else { return }
187
+ let height = layout.headerHeight
188
+ // within the first header height the header still sits on the content
189
+ guard scrollOffset >= height, headerHide > 0, headerHide < height else { return }
190
+ withAnimation(.easeOut(duration: 0.2)) {
191
+ headerHide = headerHide > height / 2 ? height : 0
192
+ }
193
+ }
194
+ }
195
+
196
+ private var keyboardSize: CGFloat { useAvoidKeyboard ? max(keyboardHeight, 0) : 0 }
197
+
198
+ /// What the footer, tab bar or home indicator takes from the bottom of the window, gap included.
199
+ private var bottomBarsHeight: CGFloat {
200
+ if footer != nil { return footerHeight + NavigationFooter.safeAreaBottom + Spacing.XL }
201
+ if duoTabBarInsets.sideBarReserve > 0 { return duoTabBarInsets.sideBarReserve }
202
+ return NavigationFooter.navigationBarInset + Spacing.XL
203
+ }
204
+
205
+ /// Top of the footer (or the navigation bar) from the window bottom; snack bars sit above it.
206
+ private var bottomObstruction: CGFloat {
207
+ guard footer != nil else { return keyboardSize + NavigationFooter.navigationBarInset }
208
+ return footerHeight + NavigationFooter.safeAreaBottom - NavigationFooter.verticalOffset(keyboardSize: keyboardSize)
209
+ }
210
+
211
+ private func reportBottomObstruction() {
212
+ // bottom tab children leave overplays to the screen hosting the tabs, as in Compose
213
+ guard let navigation, navigation.bottomTabIndex == -1 else { return }
214
+ navigation.bottomObstruction = bottomObstruction
215
+ }
216
+
217
+ /// Keyboard frames are in screen coordinates; UIScreen stays on the outer display on iPhone Duo.
218
+ private var keyboardReferenceHeight: CGFloat {
219
+ layout.isIPhoneDuo ? layout.height : UIScreen.main.bounds.height
220
+ }
221
+
222
+ /// Room for the animated search below the header at scroll 0; shrinks as the bar slides up.
223
+ private var animatedSearchInset: CGFloat {
224
+ guard inputSearchType == .animated else { return 0 }
225
+ let scroll = max(scrollOffset, 0)
226
+ return min(max(HEADER_HEIGHT - scroll, 0), HEADER_HEIGHT)
227
+ }
228
+
229
+ /// Title fades out as the animated search bar rises into the header.
230
+ private var titleOpacity: CGFloat {
231
+ guard inputSearchType == .animated else { return 1 }
232
+ let scroll = max(scrollOffset, 0)
233
+ return 1 - min(max(scroll / HEADER_HEIGHT, 0), 1)
234
+ }
235
+
236
+ // MARK: - Body
237
+
238
+ public var body: some View {
239
+ GeometryReader { _ in
240
+ ZStack(alignment: .top) {
241
+ (backgroundColor ?? theme.colors.background.default)
242
+ .edgesIgnoringSafeArea(.all)
243
+
244
+ VStack(spacing: 0) {
245
+ ZStack(alignment: fabProps?.position == .center ? .bottom : .bottomTrailing) {
246
+ VStack(alignment: horizontalAlignment, spacing: 0) {
247
+ headerAndContent
248
+ }
249
+
250
+ FloatingContent(
251
+ fabProps: fabProps,
252
+ keyboardSize: keyboardSize,
253
+ hasFooter: footer != nil,
254
+ scrollOffset: scrollOffset
255
+ )
256
+ .padding(.bottom, footer == nil ? duoTabBarInsets.overlay : 0)
257
+ }
258
+
259
+ NavigationFooter(
260
+ footerComponent: footer,
261
+ keyboardSize: keyboardSize,
262
+ transparent: footerTransparent,
263
+ useMargin: footerUseMargin
264
+ )
265
+ .padding(.bottom, duoTabBarInsets.overlay)
266
+ .background(
267
+ GeometryReader { proxy in
268
+ Color.clear
269
+ .onAppear { footerHeight = proxy.size.height }
270
+ .onChange(of: proxy.size.height) { footerHeight = $0 }
271
+ }
272
+ )
273
+ }
274
+
275
+ if inputSearchType == .animated, let props = inputSearchProps {
276
+ SearchAnimatedHeader(
277
+ props: props,
278
+ scrollOffset: scrollOffset,
279
+ hiddenBack: !isBack,
280
+ hasHeaderRight: headerRight != nil,
281
+ headerRightWidth: headerRightWidth,
282
+ useSideBar: layout.sideBar != nil,
283
+ hasToolkit: toolkit != nil
284
+ )
285
+ }
286
+
287
+ if showsSideBar, let sideBar = layout.sideBar {
288
+ HeaderSideBar(
289
+ sideBar: sideBar,
290
+ goBack: goBack,
291
+ toolkit: toolkit,
292
+ headerColor: headerColor,
293
+ bottomInset: bottomBarsHeight
294
+ )
295
+ }
296
+ }
297
+ .ignoresSafeArea(.keyboard, edges: .bottom)
298
+ .onChange(of: scrollOffset) { updateHeaderHide($0) }
299
+ .onPreferenceChange(HeaderRightWidthKey.self) { width in
300
+ if headerRightWidth != width { headerRightWidth = width }
301
+ }
302
+ .onPreferenceChange(IsBottomTabRootKey.self) { isRoot in
303
+ if isBottomTabRoot != isRoot { isBottomTabRoot = isRoot }
304
+ }
305
+ .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillChangeFrameNotification)) { notification in
306
+ guard useAvoidKeyboard,
307
+ let endFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
308
+ withAnimation(.easeOut(duration: 0.25)) {
309
+ keyboardHeight = keyboardReferenceHeight - endFrame.origin.y
310
+ }
311
+ }
312
+ // Compose's `hideKeyboardOnTap()`: simultaneous, so taps still reach buttons and fields.
313
+ .modifier(HideKeyboardOnTap(enabled: keyboardShouldPersistTaps))
314
+ }
315
+ .onAppear(perform: reportBottomObstruction)
316
+ .onChange(of: bottomObstruction) { _ in reportBottomObstruction() }
317
+ .task(id: screenName) {
318
+ ScreenTracker.trackScreenNavigated(
319
+ maxApi: appEnv.maxApi,
320
+ context: appEnv.applicationContext,
321
+ screenName: screenName,
322
+ action: trackingAction,
323
+ state: trackingState
324
+ )
325
+ do {
326
+ try await Task.sleep(nanoseconds: 2_000_000_000)
327
+ } catch {
328
+ return
329
+ }
330
+ let loadTime = ScreenTracker.now() - trackingState.time
331
+ ScreenTracker.trackScreenDisplayed(
332
+ maxApi: appEnv.maxApi,
333
+ context: appEnv.applicationContext,
334
+ screenName: screenName,
335
+ duration: loadTime,
336
+ state: trackingState
337
+ )
338
+ let interactionTime = ScreenTracker.now() - trackingState.time
339
+ ScreenTracker.trackScreenInteracted(
340
+ maxApi: appEnv.maxApi,
341
+ context: appEnv.applicationContext,
342
+ screenName: screenName,
343
+ duration: interactionTime - loadTime,
344
+ totalDuration: interactionTime,
345
+ state: trackingState
346
+ )
347
+ }
348
+ }
349
+
350
+ // MARK: - Header + content layer
351
+
352
+ @ViewBuilder
353
+ private var headerAndContent: some View {
354
+ if let animatedHeader = animatedHeader {
355
+ // The banner is the top of the scroll content and the body overlaps its lower part;
356
+ // the bar fades in over it (Compose's ScreenContent for HeaderType.Animated).
357
+ ZStack(alignment: .top) {
358
+ StackScreenContent(
359
+ scrollable: scrollable,
360
+ scrollOffset: $scrollOffset,
361
+ scrollToTopCallback: scrollToTopCallback,
362
+ animatedSearchInset: animatedSearchInset,
363
+ bottomInset: duoTabBarInsets.overlay,
364
+ content: { animatedHeroBody(animatedHeader) }
365
+ )
366
+ .edgesIgnoringSafeArea(.all)
367
+
368
+ header
369
+ .background(alignment: .top) {
370
+ Group {
371
+ if layout.sideBar != nil {
372
+ // glass shows once the banner has scrolled under the header
373
+ Color.clear.modifier(DuoBarMaterial(
374
+ shape: Rectangle(),
375
+ visible: scrollOffset > heroGlassStart(animatedHeader)
376
+ ))
377
+ } else {
378
+ HeaderBarBackground(opacity: opacity)
379
+ }
380
+ }
381
+ .edgesIgnoringSafeArea([.top, .horizontal])
382
+ }
383
+ .offset(y: -headerHide)
384
+ .zIndex(100)
385
+ }
386
+ } else if headerTransparent && fullScreenContent {
387
+ // Compose's HeaderType.Transparent(isFullScreenContent): content starts under a clear bar
388
+ ZStack(alignment: .top) {
389
+ header
390
+ .zIndex(100)
391
+
392
+ StackScreenContent(
393
+ scrollable: scrollable,
394
+ scrollOffset: $scrollOffset,
395
+ scrollToTopCallback: scrollToTopCallback,
396
+ animatedSearchInset: animatedSearchInset,
397
+ bottomInset: duoTabBarInsets.overlay,
398
+ content: { duoInsetContent }
399
+ )
400
+ .edgesIgnoringSafeArea(.all)
401
+ }
402
+ } else if useDuoGlass {
403
+ ZStack(alignment: .top) {
404
+ DuoScrollGradient(
405
+ scrollOffset: scrollOffset,
406
+ headerHeight: layout.headerHeight,
407
+ extended: headerType == .extended,
408
+ followScroll: useQuickReturn
409
+ )
410
+
411
+ StackScreenContent(
412
+ scrollable: scrollable,
413
+ scrollOffset: $scrollOffset,
414
+ scrollToTopCallback: scrollToTopCallback,
415
+ topInset: layout.headerHeight - layout.insets.top,
416
+ bottomInset: duoTabBarInsets.overlay,
417
+ content: content
418
+ )
419
+
420
+ header
421
+ .background(
422
+ Color.clear
423
+ .modifier(DuoBarMaterial(shape: Rectangle(), visible: scrollOffset > 0))
424
+ .edgesIgnoringSafeArea([.top, .horizontal])
425
+ )
426
+ .offset(y: -headerHide)
427
+ .zIndex(100)
428
+ }
429
+ } else {
430
+ header
431
+ // top-aligned: an extended band runs past the bar, under the content
432
+ .background(alignment: .top) {
433
+ ZStack(alignment: .top) {
434
+ NavigationHeaderBackground(
435
+ headerType: headerType,
436
+ scrollState: scrollOffset,
437
+ headerTransparent: headerTransparent,
438
+ fullScreenContent: fullScreenContent,
439
+ backgroundColor: backgroundColor
440
+ )
441
+ HeaderBarBackground(opacity: opacity, transparent: headerTransparent || headerType == .none)
442
+ }
443
+ .edgesIgnoringSafeArea([.top, .horizontal])
444
+ }
445
+
446
+ StackScreenContent(
447
+ scrollable: scrollable,
448
+ scrollOffset: $scrollOffset,
449
+ scrollToTopCallback: scrollToTopCallback,
450
+ animatedSearchInset: animatedSearchInset,
451
+ bottomInset: duoTabBarInsets.overlay,
452
+ content: content
453
+ )
454
+ }
455
+ }
456
+
457
+ /// Edge-to-edge branches lay out outside the safe area; iPhone Duo content still keeps
458
+ /// clear of the side strips, as RN's Screen does.
459
+ private var duoInsetContent: some View {
460
+ content()
461
+ .padding(.leading, layout.insetLeft)
462
+ .padding(.trailing, layout.insetRight)
463
+ }
464
+
465
+ private func heroGlassStart(_ animatedHeader: AnimatedHeader) -> CGFloat {
466
+ max(layout.width / animatedHeader.aspectRatio.value - layout.headerHeight, 0)
467
+ }
468
+
469
+ /// The banner fills the width at its aspect ratio; the body starts `layoutOffset` below the
470
+ /// header band so it overlaps the banner's lower part.
471
+ @ViewBuilder
472
+ private func animatedHeroBody(_ animatedHeader: AnimatedHeader) -> some View {
473
+ let width = layout.width
474
+ let imageHeight = width / animatedHeader.aspectRatio.value
475
+ let contentTopInset = layout.headerHeight + layoutOffset
476
+
477
+ ZStack(alignment: .top) {
478
+ animatedHeader.composable(scrollOffset)
479
+ .frame(width: width, height: imageHeight)
480
+ .clipped()
481
+
482
+ duoInsetContent
483
+ .padding(.top, contentTopInset)
484
+ }
485
+ .frame(maxWidth: .infinity, alignment: .top)
486
+ }
487
+
488
+ private var header: NavigationHeader {
489
+ NavigationHeader(
490
+ headerType: headerType,
491
+ titlePosition: titlePosition,
492
+ title: title,
493
+ headerTitle: headerTitle,
494
+ headerRight: headerRight,
495
+ goBack: goBack,
496
+ opacity: opacity,
497
+ animatedHeader: animatedHeader,
498
+ scrollState: scrollOffset,
499
+ // the animated search draws its own field over the (fading) title
500
+ inputSearchProps: inputSearchType == .header ? inputSearchProps : nil,
501
+ tintColor: tintColor,
502
+ titleOpacity: titleOpacity
503
+ )
504
+ }
505
+ }
506
+
507
+ // MARK: - Bottom-tab root preference
508
+
509
+ /// Set by `BottomTab` so the `StackScreen` hosting it keeps the full width and leaves the side
510
+ /// bar to the tabs (Compose's `Navigation.markAsBottomTabRoot()`).
511
+ public struct IsBottomTabRootKey: PreferenceKey {
512
+ public static var defaultValue: Bool = false
513
+ public static func reduce(value: inout Bool, nextValue: () -> Bool) {
514
+ value = value || nextValue()
515
+ }
516
+ }
517
+
518
+ // MARK: - Screen content (scrollable body)
519
+
520
+ /// Compose's `MainContent` / `ScreenContent`: the body, tracking the scroll offset when scrollable.
521
+ private struct StackScreenContent<Content: View>: View {
522
+ let scrollable: Bool
523
+ @Binding var scrollOffset: CGFloat
524
+ /// Compose's `ScrollData.scrollToTopCallback`, fired with the status-bar tap scroll-to-top.
525
+ let scrollToTopCallback: (() -> Void)?
526
+ /// Room for the animated search below the header (Compose's MainContent spacer).
527
+ var animatedSearchInset: CGFloat = 0
528
+ /// Room for a header the content scrolls under (iPhone Duo glass header).
529
+ var topInset: CGFloat = 0
530
+ /// Room for a bar that overlays the content (iPhone Duo floating tab bar).
531
+ var bottomInset: CGFloat = 0
532
+ let content: () -> Content
533
+
534
+ private let topAnchorID = "stack_screen_top_anchor"
535
+ private let scrollSpace = "stack_screen_scroll_space"
536
+
537
+ var body: some View {
538
+ if scrollable {
539
+ ScrollViewReader { proxy in
540
+ ScrollView(.vertical, showsIndicators: false) {
541
+ VStack(spacing: 0) {
542
+ // Top probe + scroll-to-top anchor, measured in the scroll's own space so the
543
+ // offset is a clean 0-based "points scrolled", independent of the insets below.
544
+ Color.clear
545
+ .frame(height: 0)
546
+ .id(topAnchorID)
547
+ .background(GeometryReader { geo in
548
+ Color.clear
549
+ .onAppear {
550
+ scrollOffset = -geo.frame(in: .named(scrollSpace)).minY
551
+ }
552
+ .onChange(of: geo.frame(in: .named(scrollSpace)).minY) { minY in
553
+ scrollOffset = -minY
554
+ }
555
+ })
556
+
557
+ if animatedSearchInset + topInset > 0 {
558
+ Spacer().frame(height: animatedSearchInset + topInset)
559
+ }
560
+
561
+ VStack {
562
+ content()
563
+ }
564
+
565
+ if bottomInset > 0 {
566
+ Spacer().frame(height: bottomInset)
567
+ }
568
+ }
569
+ }
570
+ .coordinateSpace(name: scrollSpace)
571
+ // Status-bar tap (MoMoStatusBarTap swizzle) scrolls to top and notifies the consumer.
572
+ .onReceive(NotificationCenter.default.publisher(for: MoMoStatusBarTap.notificationName)) { _ in
573
+ withAnimation {
574
+ proxy.scrollTo(topAnchorID, anchor: .top)
575
+ }
576
+ scrollToTopCallback?()
577
+ }
578
+ }
579
+ } else {
580
+ content()
581
+ .padding(.top, topInset)
582
+ .padding(.bottom, bottomInset)
583
+ }
584
+ }
585
+ }
586
+
587
+ // MARK: - Animated search header
588
+
589
+ /// Compose's `SearchAnimated`: the search slides from below the header (scroll 0) up into the bar
590
+ /// while its insets grow to clear the back button and header right.
591
+ private struct SearchAnimatedHeader: View {
592
+ let props: InputSearchProps
593
+ let scrollOffset: CGFloat
594
+ let hiddenBack: Bool
595
+ let hasHeaderRight: Bool
596
+ let headerRightWidth: CGFloat
597
+ var useSideBar: Bool = false
598
+ var hasToolkit: Bool = false
599
+
600
+ @Environment(\.screenLayout) private var layout
601
+ @Environment(\.appTheme) private var theme
602
+
603
+ var body: some View {
604
+ let scroll = max(scrollOffset, 0)
605
+
606
+ // Insets are measured from the safe-area top; collapsed, the search is centered in the bar row.
607
+ let rowTop = layout.headerStatusBarHeight - layout.insets.top
608
+ let minTop = rowTop + (layout.headerBarHeight - HEADER_HEIGHT) / 2
609
+ let maxTop = layout.headerHeight - layout.insets.top
610
+ let minSide = Spacing.M
611
+ let maxStart: CGFloat = hiddenBack || useSideBar ? Spacing.M : 48
612
+ let maxEnd: CGFloat = useSideBar && hasToolkit
613
+ ? Spacing.M
614
+ : headerRightWidth + (hasHeaderRight ? Spacing.M * 2 : Spacing.M)
615
+
616
+ let top = min(max(maxTop - scroll, minTop), maxTop)
617
+ let start = min(max(minSide + scroll, minSide), maxStart)
618
+ let end = min(max(minSide + scroll, minSide), maxEnd)
619
+
620
+ VStack(spacing: 0) {
621
+ Spacer().frame(height: top)
622
+ InputSearch(props: headerSearchProps(props, scroll: scroll, layout: layout, theme: theme))
623
+ .frame(height: HEADER_HEIGHT)
624
+ .padding(.leading, start)
625
+ .padding(.trailing, end)
626
+ Spacer(minLength: 0)
627
+ }
628
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
629
+ }
630
+ }
631
+
632
+ // MARK: - Floating content
633
+
634
+ /// Compose's `FloatingContent`: no FAB props means nothing renders.
635
+ private struct FloatingContent: View {
636
+ let fabProps: FabProps?
637
+ let keyboardSize: CGFloat
638
+ /// With a footer the FAB is anchored to the footer's *layout* top, which does not move
639
+ /// when the band offsets itself over the keyboard — so it rides the same offset.
640
+ let hasFooter: Bool
641
+ let scrollOffset: CGFloat
642
+
643
+ /// Compose keeps the FAB `Spacing.M + AppNavigationBar` above the bottom of the screen
644
+ /// and lifts it with `imePadding()`; here it sits `Spacing.M` above the safe area and
645
+ /// nothing lifts it, so the difference is applied as an offset (see NavigationFooter).
646
+ private var verticalOffset: CGFloat {
647
+ if hasFooter { return NavigationFooter.verticalOffset(keyboardSize: keyboardSize) }
648
+ return NavigationFooter.safeAreaBottom - NavigationFooter.navigationBarInset - keyboardSize
649
+ }
650
+
651
+ var body: some View {
652
+ if let fabProps = fabProps {
653
+ FloatingButton(
654
+ props: fabProps,
655
+ keyboardOffset: 0,
656
+ scrollOffset: scrollOffset
657
+ )
658
+ .padding(.horizontal, Spacing.M)
659
+ .offset(y: verticalOffset)
660
+ .ignoresSafeArea(.keyboard, edges: .bottom)
661
+ }
662
+ }
663
+ }
664
+
665
+ // MARK: - Footer
666
+
667
+ /// Compose's `Footer`: the band pinned under the content — surface background, S/M padding,
668
+ /// the navigation-bar reserve, and a 6pt shadow cast up onto the content.
669
+ /// Named `NavigationFooter` to coexist with the deprecated `Screen`'s footer.
670
+ public struct NavigationFooter: View {
671
+ /// Nothing renders when nil.
672
+ var footerComponent: (() -> AnyView)?
673
+ /// Current keyboard height, measured from the bottom of the screen.
674
+ var keyboardSize: CGFloat
675
+ var transparent: Bool
676
+ var useMargin: Bool
677
+
678
+ public init(
679
+ footerComponent: (() -> AnyView)?,
680
+ keyboardSize: CGFloat = 0,
681
+ transparent: Bool = false,
682
+ useMargin: Bool = true
683
+ ) {
684
+ self.footerComponent = footerComponent
685
+ self.keyboardSize = keyboardSize
686
+ self.transparent = transparent
687
+ self.useMargin = useMargin
688
+ }
689
+
690
+ @Environment(\.appTheme) private var theme
691
+
692
+ /// Read from the window, not from an enclosing `GeometryProxy`: the proxy's bottom
693
+ /// inset grows to the keyboard height while the keyboard is up.
694
+ static var safeAreaBottom: CGFloat {
695
+ UIApplication.shared.connectedScenes
696
+ .compactMap { ($0 as? UIWindowScene)?.keyWindow?.safeAreaInsets.bottom }
697
+ .first ?? 0
698
+ }
699
+
700
+ /// Compose's `AppNavigationBar.current` (`getNavigationBarHeight` clamps iOS to 21pt).
701
+ static var navigationBarInset: CGFloat { min(safeAreaBottom, 21) }
702
+
703
+ /// Compose's `bottomPadding + Spacing.S` with the keyboard down. The footer is laid
704
+ /// out inside the safe area, which already covers part of that gap.
705
+ static var bottomPadding: CGFloat {
706
+ max(navigationBarInset + Spacing.S - safeAreaBottom, 0)
707
+ }
708
+
709
+ /// Deliberately an offset and not padding: growing the footer feeds back into SwiftUI's
710
+ /// keyboard avoidance, so a padding-based lift never settles at the right place.
711
+ /// Negative lifts the band over the keyboard; positive shaves the few points by which
712
+ /// the safe area over-reserves compared to Compose. `FloatingContent` rides the same
713
+ /// amount so the FAB keeps its distance above the band.
714
+ ///
715
+ /// Natural position is `safeAreaBottom + bottomPadding` above the bottom of the screen;
716
+ /// Compose puts it at `(AppNavigationBar - keyboard).coerceAtLeast(0) + Spacing.S` on
717
+ /// top of the `imePadding()` lift — i.e. `max(navigationBar, keyboard) + Spacing.S`.
718
+ static func verticalOffset(keyboardSize: CGFloat) -> CGFloat {
719
+ (safeAreaBottom + bottomPadding) - (max(navigationBarInset, keyboardSize) + Spacing.S)
720
+ }
721
+
722
+ public var body: some View {
723
+ if let footerComponent = footerComponent {
724
+ let margin: CGFloat = useMargin ? Spacing.S : 0
725
+ VStack(alignment: .leading, spacing: 0) {
726
+ footerComponent()
727
+ }
728
+ .frame(maxWidth: .infinity, alignment: .leading)
729
+ .padding(.top, margin)
730
+ .padding(.horizontal, useMargin ? Spacing.M : 0)
731
+ // without the margin the Spacing.S under the content goes too: negative, so the
732
+ // content sinks into the reserve while the frame keeps its bottom edge
733
+ .padding(.bottom, Self.bottomPadding - (Spacing.S - margin))
734
+ // Compose paints the surface all the way to the bottom of the screen; the
735
+ // footer's own frame stops at the safe area, so only the fill bleeds down.
736
+ .background(
737
+ (transparent ? Color.clear : theme.colors.background.surface)
738
+ .ignoresSafeArea(.container, edges: [.bottom, .horizontal])
739
+ )
740
+ .overlay(alignment: .top) {
741
+ if !transparent { shadow }
742
+ }
743
+ .offset(y: Self.verticalOffset(keyboardSize: keyboardSize))
744
+ }
745
+ }
746
+
747
+ /// Compose offsets the shadow box by -6dp, so it lands on the content above the
748
+ /// footer rather than on the footer surface itself.
749
+ private var shadow: some View {
750
+ LinearGradient(
751
+ colors: [Color.clear, Color.black.opacity(0.05)],
752
+ startPoint: .top,
753
+ endPoint: .bottom
754
+ )
755
+ .frame(height: 6)
756
+ .offset(y: -6)
757
+ .allowsHitTesting(false)
758
+ }
759
+ }
760
+
761
+ // MARK: - Hide keyboard on tap
762
+
763
+ /// Compose's `Modifier.hideKeyboardOnTap()`. Uses `simultaneousGesture` so the tap is not
764
+ /// consumed — buttons and text fields keep working.
765
+ private struct HideKeyboardOnTap: ViewModifier {
766
+ let enabled: Bool
767
+
768
+ func body(content: Content) -> some View {
769
+ if enabled {
770
+ content.simultaneousGesture(
771
+ TapGesture().onEnded {
772
+ UIApplication.shared.sendAction(
773
+ #selector(UIResponder.resignFirstResponder),
774
+ to: nil, from: nil, for: nil
775
+ )
776
+ }
777
+ )
778
+ } else {
779
+ content
780
+ }
781
+ }
782
+ }