@momo-kits/native-kits 0.167.1-duo.18-debug → 0.167.1-duo.19

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 (163) hide show
  1. package/CLAUDE.md +5 -3
  2. package/ios/Application/MaxApi.swift +12 -3
  3. package/ios/Colors+Radius+Spacing/Radius.swift +6 -0
  4. package/ios/Layout/GridContext.swift +67 -3
  5. package/ios/Navigation/BottomSheet.swift +30 -4
  6. package/ios/Navigation/BottomTab/BottomTab.swift +0 -2
  7. package/ios/Navigation/Component/SnackBar.swift +0 -1
  8. package/ios/Navigation/HostBackGestureBridge.swift +27 -6
  9. package/ios/Navigation/ModalScreen.swift +0 -1
  10. package/ios/Navigation/Navigation.swift +14 -0
  11. package/ios/Navigation/NavigationContainer.swift +26 -56
  12. package/ios/Navigation/Navigator.swift +102 -165
  13. package/ios/Navigation/StackScreen.swift +19 -2
  14. package/ios/Navigation/StackTransition.swift +400 -0
  15. package/ios/Navigation/SwipeBackHaptic.swift +33 -0
  16. package/package.json +1 -1
  17. package/build.gradle.kts +0 -11
  18. package/compose/build.gradle.kts +0 -204
  19. package/compose/build.gradle.kts.backup +0 -204
  20. package/compose/compose.podspec +0 -47
  21. package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +0 -19
  22. package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +0 -6
  23. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/BackGesture.android.kt +0 -12
  24. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/OsFontScale.android.kt +0 -7
  25. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +0 -153
  26. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +0 -6
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +0 -52
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +0 -124
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +0 -183
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +0 -207
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +0 -39
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +0 -86
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +0 -76
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +0 -76
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +0 -293
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +0 -33
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +0 -916
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Localize.kt +0 -269
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +0 -110
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +0 -378
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +0 -56
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +0 -69
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Avatar.kt +0 -157
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +0 -81
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +0 -32
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +0 -338
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +0 -175
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +0 -357
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +0 -115
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +0 -89
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +0 -128
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Collapse.kt +0 -209
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +0 -542
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +0 -49
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +0 -86
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +0 -125
  57. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +0 -231
  58. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +0 -189
  59. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +0 -478
  60. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +0 -114
  61. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +0 -265
  62. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +0 -235
  63. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +0 -223
  64. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +0 -265
  65. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +0 -185
  66. package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +0 -343
  67. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Loader.kt +0 -108
  68. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +0 -56
  69. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +0 -41
  70. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +0 -84
  71. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +0 -40
  72. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +0 -361
  73. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +0 -108
  74. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +0 -308
  75. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +0 -59
  76. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Rating.kt +0 -87
  77. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +0 -21
  78. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +0 -90
  79. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +0 -330
  80. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +0 -220
  81. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Steps.kt +0 -473
  82. package/compose/src/commonMain/kotlin/vn/momo/kits/components/SuggestAction.kt +0 -122
  83. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +0 -207
  84. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +0 -92
  85. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +0 -560
  86. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +0 -86
  87. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +0 -125
  88. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +0 -207
  89. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +0 -501
  90. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +0 -176
  91. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Uploader.kt +0 -182
  92. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +0 -193
  93. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +0 -29
  94. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +0 -236
  95. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +0 -180
  96. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +0 -325
  97. package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +0 -20
  98. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitDesignSystem.kt +0 -18
  99. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +0 -22
  100. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +0 -12
  101. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +0 -16
  102. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +0 -194
  103. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +0 -283
  104. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +0 -54
  105. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +0 -50
  106. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +0 -309
  107. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +0 -41
  108. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +0 -52
  109. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +0 -66
  110. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +0 -11
  111. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/DeprecatedModifier.kt +0 -104
  112. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +0 -48
  113. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +0 -51
  114. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +0 -338
  115. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/HostBackGestureBridge.kt +0 -58
  116. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +0 -137
  117. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +0 -106
  118. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +0 -205
  119. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +0 -393
  120. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +0 -8
  121. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +0 -713
  122. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackTransition.kt +0 -148
  123. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +0 -35
  124. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +0 -218
  125. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +0 -231
  126. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +0 -86
  127. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +0 -98
  128. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +0 -312
  129. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +0 -169
  130. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +0 -447
  131. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +0 -103
  132. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +0 -106
  133. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +0 -74
  134. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +0 -379
  135. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +0 -39
  136. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +0 -363
  137. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +0 -192
  138. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +0 -133
  139. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +0 -127
  140. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/BackGesture.kt +0 -12
  141. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +0 -91
  142. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/OsFontScale.kt +0 -9
  143. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +0 -74
  144. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +0 -13
  145. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +0 -1329
  146. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +0 -55
  147. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Tracking.kt +0 -15
  148. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +0 -118
  149. package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +0 -78
  150. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/HostBackGesture.ios.kt +0 -45
  151. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +0 -32
  152. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/BackGesture.ios.kt +0 -38
  153. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +0 -66
  154. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +0 -217
  155. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +0 -94
  156. package/gradle/libs.versions.toml +0 -69
  157. package/gradle/wrapper/gradle-wrapper.jar +0 -0
  158. package/gradle/wrapper/gradle-wrapper.properties +0 -8
  159. package/gradle.properties +0 -26
  160. package/gradlew +0 -252
  161. package/gradlew.bat +0 -94
  162. package/ios-demo/README.md +0 -120
  163. package/settings.gradle.kts +0 -64
package/CLAUDE.md CHANGED
@@ -56,11 +56,13 @@ Publishing is automated (`publish.sh` + GitLab CI on `[ci build]` / `main`). Do
56
56
 
57
57
  ## SwiftUI kit (`ios/`)
58
58
 
59
- - **Layout mirrors Compose:** `ios/Navigation/` follows `navigation/` file for file (`NavigationContainer`, `Navigator`, `Navigation` (options + per-screen `Navigation`), `StackScreen` (incl. `NavigationFooter`), `BottomSheet`, `ModalScreen`, `HostBackGestureBridge`, `BottomTab/`, `Component/`, `Tracking/`); `ios/Layout/ScreenLayout.swift` mirrors `layout/`. `ios/Application/` keeps only context (`ApplicationEnvironment`), `Localize*`, `MaxApi` and the deprecated `Screen` / `LegacyHeader` (Compose `application/`). Change a navigation piece on both sides together.
59
+ - **Layout mirrors Compose:** `ios/Navigation/` follows `navigation/` file for file (`NavigationContainer`, `Navigator`, `Navigation` (options + per-screen `Navigation`), `StackScreen` (incl. `NavigationFooter`), `StackTransition`, `SwipeBackHaptic`, `BottomSheet`, `ModalScreen`, `HostBackGestureBridge`, `BottomTab/`, `Component/`, `Tracking/`); `ios/Layout/ScreenLayout.swift` mirrors `layout/`. `ios/Application/` keeps only context (`ApplicationEnvironment`), `Localize*`, `MaxApi` and the deprecated `Screen` / `LegacyHeader` (Compose `application/`). Change a navigation piece on both sides together.
60
60
  - **File basenames must be unique across the pod** — Swift fails with "filename used twice" even in different folders (hence `LegacyHeader.swift` next to `Navigation/Component/Header.swift`). New folders under `ios/` must be added to `source_files` in `ios/native-kits.podspec`.
61
61
  - Screens update their own chrome at runtime through `@EnvironmentObject var navigation: Navigation` → `setOptions { $0.title = … }` (Compose `LocalNavigation`); bottom tabs keep one `Navigation` per tab. The host back bridge is `HostBackGestureBridge.shared` (`NavigatorManager` is a deprecated alias); a bottom tab off its first tab routes back to the first tab (`Navigator.tabBackHandler`, keyed by the hosting screen).
62
- - The pod's minimum is **iOS 15**, but the navigation shell (`NavigationContainer`, `Navigator`, `HostBackGestureBridge`, `BottomTab`, overplay views) is **iOS 16+** (`@available(iOS 16.0, *)`) because it is built on `NavigationStack`. Keep components outside that shell iOS 15-compatible.
63
- - `present` opens one `fullScreenCover` that hosts its own `NavigationStack` (`Navigator.presentedPath`): pushes and pops act on the presented flow while it is up, and each overplay (modal / bottom sheet / snackbar) renders in the context it was shown over (`OverplayItem.presentedId`).
62
+ - Everything, navigation shell included, supports the pod's minimum **iOS 15** — no `@available` gates on public types. iOS 16+ APIs only behind `#available` with an iOS 15 path (`Layout`: `BottomSheet` `CapHeight`, `GridFlow` → `_VariadicView` flow row).
63
+ - **Usage is `NavigationContainer` + `Navigator` + `Navigation` only**; the kit owns the stack, transitions, overplays and back gesture. There is no `NavigationStack`: `StackTransition.swift` holds `StackHost` (representable) + `StackController`, the NavHost counterpart. Each entry gets its own `UIHostingController`, kept while on the stack and removed from the window while covered (state kept, onDisappear / onAppear like UINavigationController). Motion is a SwiftUI `offset` per card driven by one `coverage` value on a display link (a UIKit transform would move the safe area); it is Compose `StackTransition` 1:1 (RN `forStackTransitionWithOverlay`: linear 300ms slide, 0.3 parallax, `pink09` scrim to 0.6, 8pt edge shadow to 0.3). Card roots get the container environment via `.environment(\.self, …)` with the kit environment re-applied on top, so host environment objects reach screens.
64
+ - One back stack, root first (`Navigator.entries`, Compose's back stack): `present` appends a dialog entry that rises (FastOutSlowIn 300ms) and later pushes stack on top of it; `dismiss` drops the topmost dialog and everything above it (slides down whole); `popToRoot` returns to the active flow's root. Overplays (modal / bottom sheet / snackbar) render inside the screen they were shown over (`OverplayItem.screenId`, Compose `currentRootId`), so a later push slides over them.
65
+ - Host back gesture: `HostBackGestureBridge.started` / `progressed` / `completed` / `cancelled` scrub the pop (`Navigator.backStarted` …) only for a plain pop; tab back, `onBackHandler`, `preventBack`, an open overplay or a presented top wait for the release and go through `onBackSafe`. `SwipeBackHaptic` ticks at 50pt (`MaxApi.triggerEventVibration`, default = system light impact).
64
66
  - Icons are compiled in: `ios/Icon/Icons.swift` is generated from Compose `utils/Icons.kt` by `node scripts/gen-ios-icons.mjs` — don't hand-edit it. A host `icon.json` in the main bundle only adds keys the kit lacks.
65
67
  - Bundled assets (colors, Lottie JSON) live in `ios/Resources` and ship in the `MoMoUIKitsResources` bundle; load them through `MoMoUIKitsResources.bundle`, never `Bundle.main`.
66
68
 
@@ -5,14 +5,23 @@
5
5
  // Swift mirror of the host-app bridge exposed on Kotlin as `vn.momo.maxapi.IMaxApi`
6
6
  // (`LocalMaxApi`). `IMaxApi` itself is an external compiled dependency with no
7
7
  // source in this repo, so this protocol intentionally exposes only the methods
8
- // this kit's Kotlin source actually calls (`track`, `dismiss`, `getLanguage`) —
9
- // not a full guess at IMaxApi's real surface.
8
+ // this kit's Kotlin source actually calls (`track`, `dismiss`, `getLanguage`,
9
+ // `triggerEventVibration`) — not a full guess at IMaxApi's real surface.
10
10
  //
11
11
 
12
- import Foundation
12
+ import UIKit
13
13
 
14
14
  public protocol MaxApi {
15
15
  func track(eventName: String, params: [String: Any?])
16
16
  func dismiss(completion: (() -> Void)?)
17
17
  func getLanguage(callback: @escaping ([String: Any?]?) -> Void)
18
+ /// `type` is the IMaxApi vibration name (`"light"`, …).
19
+ func triggerEventVibration(_ type: String)
20
+ }
21
+
22
+ public extension MaxApi {
23
+ /// Hosts that don't bridge haptics still get the system light tick.
24
+ func triggerEventVibration(_ type: String) {
25
+ UIImpactFeedbackGenerator(style: .light).impactOccurred()
26
+ }
18
27
  }
@@ -15,6 +15,12 @@ public struct RoundedCorner: Shape {
15
15
  var radius: CGFloat = .infinity
16
16
  var corners: UIRectCorner = .allCorners
17
17
 
18
+ /// iOS 15 stand-in for `UnevenRoundedRectangle`.
19
+ public init(radius: CGFloat = .infinity, corners: UIRectCorner = .allCorners) {
20
+ self.radius = radius
21
+ self.corners = corners
22
+ }
23
+
18
24
  public func path(in rect: CGRect) -> Path {
19
25
  let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
20
26
  return Path(path.cgPath)
@@ -73,9 +73,7 @@ struct GridFlow<Content: View>: View {
73
73
  if #available(iOS 16.0, *) {
74
74
  FlowRowLayout(gutter: gutter) { content() }
75
75
  } else {
76
- // No `Layout` protocol before iOS 16: children stack instead of wrapping.
77
- // Full-width items — the common case — look identical.
78
- VStack(alignment: .leading, spacing: gutter) { content() }
76
+ _VariadicView.Tree(FlowRowFallback(gutter: gutter, maxWidth: availableWidth)) { content() }
79
77
  }
80
78
  }
81
79
 
@@ -95,6 +93,72 @@ private struct GridWidthKey: PreferenceKey {
95
93
  }
96
94
  }
97
95
 
96
+ /// iOS 15 has no `Layout`: the children are measured, then pinned to the same rows as
97
+ /// `FlowRowLayout` through alignment guides.
98
+ private struct FlowRowFallback: _VariadicView_UnaryViewRoot {
99
+ let gutter: CGFloat
100
+ let maxWidth: CGFloat
101
+
102
+ func body(children: _VariadicView.Children) -> some View {
103
+ FlowRowPlacement(children: children, gutter: gutter, maxWidth: maxWidth)
104
+ }
105
+ }
106
+
107
+ private struct FlowRowPlacement: View {
108
+ let children: _VariadicView.Children
109
+ let gutter: CGFloat
110
+ let maxWidth: CGFloat
111
+ @State private var sizes: [AnyHashable: CGSize] = [:]
112
+
113
+ var body: some View {
114
+ let placement = place()
115
+ ZStack(alignment: .topLeading) {
116
+ ForEach(children) { child in
117
+ child
118
+ // FlowRowLayout proposes no height
119
+ .fixedSize(horizontal: false, vertical: true)
120
+ .background(
121
+ GeometryReader { proxy in
122
+ Color.clear.preference(key: FlowChildSizesKey.self, value: [child.id: proxy.size])
123
+ }
124
+ )
125
+ .alignmentGuide(.leading) { _ in -(placement.origins[child.id]?.x ?? 0) }
126
+ .alignmentGuide(.top) { _ in -(placement.origins[child.id]?.y ?? 0) }
127
+ }
128
+ }
129
+ .frame(height: placement.height, alignment: .topLeading)
130
+ .onPreferenceChange(FlowChildSizesKey.self) { sizes = $0 }
131
+ }
132
+
133
+ private func place() -> (origins: [AnyHashable: CGPoint], height: CGFloat) {
134
+ var origins: [AnyHashable: CGPoint] = [:]
135
+ var x: CGFloat = 0
136
+ var y: CGFloat = 0
137
+ var rowHeight: CGFloat = 0
138
+ for child in children {
139
+ let size = sizes[child.id] ?? .zero
140
+ // Sub-point tolerance: spans are computed to land exactly on `maxWidth`.
141
+ if x > 0, x + size.width > maxWidth + 0.5 {
142
+ x = 0
143
+ y += rowHeight + gutter
144
+ rowHeight = 0
145
+ }
146
+ origins[child.id] = CGPoint(x: x, y: y)
147
+ x += size.width + gutter
148
+ rowHeight = max(rowHeight, size.height)
149
+ }
150
+ return (origins, y + rowHeight)
151
+ }
152
+ }
153
+
154
+ private struct FlowChildSizesKey: PreferenceKey {
155
+ static var defaultValue: [AnyHashable: CGSize] = [:]
156
+
157
+ static func reduce(value: inout [AnyHashable: CGSize], nextValue: () -> [AnyHashable: CGSize]) {
158
+ value.merge(nextValue()) { $1 }
159
+ }
160
+ }
161
+
98
162
  /// Wrapping row with equal horizontal/vertical spacing — the Compose `FlowRow` equivalent.
99
163
  @available(iOS 16.0, *)
100
164
  private struct FlowRowLayout: Layout {
@@ -35,7 +35,6 @@ public struct BottomSheetHeader {
35
35
 
36
36
  // MARK: - Bottom sheet view
37
37
 
38
- @available(iOS 16.0, *)
39
38
  struct BottomSheetView<Content: View>: View {
40
39
  let header: BottomSheetHeader
41
40
  let isSurface: Bool
@@ -294,12 +293,40 @@ struct BottomSheetView<Content: View>: View {
294
293
  /// Compose's `heightIn(max:)`. `.frame(maxHeight:)` grows to the cap whenever the proposal is
295
294
  /// larger, leaving a gap above the header; this sizes the sheet to its content and only caps it
296
295
  /// when taller.
297
- @available(iOS 16.0, *)
298
296
  private struct CapHeight: ViewModifier {
299
297
  let maxHeight: CGFloat
300
298
 
301
299
  func body(content: Content) -> some View {
302
- CapHeightLayout(maxHeight: maxHeight) { content }
300
+ if #available(iOS 16.0, *) {
301
+ CapHeightLayout(maxHeight: maxHeight) { content }
302
+ } else {
303
+ content.modifier(CapHeightFallback(maxHeight: maxHeight))
304
+ }
305
+ }
306
+ }
307
+
308
+ /// iOS 15 has no `Layout`: the sheet hugs its ideal height until that outgrows the cap, then is
309
+ /// held at the cap (the ideal height can't be read again without a second copy of the content).
310
+ private struct CapHeightFallback: ViewModifier {
311
+ let maxHeight: CGFloat
312
+ @State private var capped = false
313
+
314
+ func body(content: Content) -> some View {
315
+ content
316
+ // both modifiers stay in place whatever `capped` is, so the content keeps its identity
317
+ .fixedSize(horizontal: false, vertical: !capped)
318
+ .background(
319
+ GeometryReader { proxy in
320
+ Color.clear
321
+ .onAppear { cap(proxy.size.height) }
322
+ .onChange(of: proxy.size.height) { cap($0) }
323
+ }
324
+ )
325
+ .frame(height: capped ? maxHeight : nil, alignment: .top)
326
+ }
327
+
328
+ private func cap(_ height: CGFloat) {
329
+ if !capped, height > maxHeight { capped = true }
303
330
  }
304
331
  }
305
332
 
@@ -340,7 +367,6 @@ private struct BottomSheetFooter: View {
340
367
  }
341
368
  }
342
369
 
343
- @available(iOS 16.0, *)
344
370
  private func safeAreaBottomInset() -> CGFloat {
345
371
  let bottomInset = UIApplication.shared.connectedScenes
346
372
  .compactMap { ($0 as? UIWindowScene)?.keyWindow?.safeAreaInsets.bottom }
@@ -9,7 +9,6 @@
9
9
 
10
10
  import SwiftUI
11
11
 
12
- @available(iOS 16.0, *)
13
12
  public struct BottomTab: View {
14
13
  private let items: [BottomTabItem]
15
14
  private let floatingButton: BottomTabFloatingButton?
@@ -178,7 +177,6 @@ private struct TabScreenHost: View {
178
177
  }
179
178
 
180
179
  /// Bottom safe-area inset (home indicator).
181
- @available(iOS 16.0, *)
182
180
  private func safeAreaBottomInset() -> CGFloat {
183
181
  let bottomInset = UIApplication.shared.connectedScenes
184
182
  .compactMap { ($0 as? UIWindowScene)?.keyWindow?.safeAreaInsets.bottom }
@@ -49,7 +49,6 @@ public struct SnackBar {
49
49
 
50
50
  /// Self-animating container. Slides the content up on appear, schedules an
51
51
  /// auto-dismiss after `duration`, and animates out before removing the overplay.
52
- @available(iOS 16.0, *)
53
52
  struct SnackBarView: View {
54
53
  let snackBar: SnackBar
55
54
  /// Distance from the window bottom to the top of the screen's footer, or the navigation bar
@@ -4,18 +4,19 @@
4
4
  //
5
5
  // SwiftUI mirror of Compose's `navigation/HostBackGestureBridge.kt`: the host owns the edge-pan
6
6
  // recogniser and routes a back into the topmost kit stack. NavigationContainer attaches itself.
7
- // The kit stack is a NavigationStack, so there is no seekable transition: the host only pops.
7
+ // `started` / `progressed` / `completed` / `cancelled` scrub the pop transition with the finger;
8
+ // a host that only calls `pop` gets the regular animated back.
8
9
  //
9
10
 
10
- import Foundation
11
+ import UIKit
11
12
 
12
- @available(iOS 16.0, *)
13
13
  public final class HostBackGestureBridge {
14
14
  public static let shared = HostBackGestureBridge()
15
15
  private init() {}
16
16
 
17
- // Weak: a container torn down while its cover is up never reports its disappearance.
17
+ // Weak: a container torn down without disappearing must not keep receiving backs.
18
18
  private var navigators: [WeakNavigator] = []
19
+ private weak var gestureTarget: Navigator?
19
20
 
20
21
  /// Set while a bottom tab other than the first is selected; the host shows its floating arrow.
21
22
  public var shouldUseFloatingArrowBack = false
@@ -28,6 +29,7 @@ public final class HostBackGestureBridge {
28
29
 
29
30
  public func detach(_ navigator: Navigator) {
30
31
  navigators.removeAll { $0.value == nil || $0.value === navigator }
32
+ if gestureTarget === navigator { gestureTarget = nil }
31
33
  }
32
34
 
33
35
  /// Whether a kit stack is on screen at all.
@@ -38,6 +40,27 @@ public final class HostBackGestureBridge {
38
40
 
39
41
  public func pop() { top()?.onBackSafe() }
40
42
 
43
+ public func started() {
44
+ guard let target = top() else { return }
45
+ gestureTarget = target
46
+ target.backStarted()
47
+ }
48
+
49
+ /// `progress` is the touch travel over the window width, 0...1.
50
+ public func progressed(_ progress: CGFloat) {
51
+ gestureTarget?.backProgressed(min(max(progress, 0), 1))
52
+ }
53
+
54
+ public func completed() {
55
+ (gestureTarget ?? top())?.backCompleted()
56
+ gestureTarget = nil
57
+ }
58
+
59
+ public func cancelled() {
60
+ gestureTarget?.backCancelled()
61
+ gestureTarget = nil
62
+ }
63
+
41
64
  @available(*, deprecated, renamed: "attach")
42
65
  public func push(_ navigator: Navigator) { attach(navigator) }
43
66
 
@@ -47,11 +70,9 @@ public final class HostBackGestureBridge {
47
70
  private func top() -> Navigator? { navigators.last { $0.value != nil }?.value }
48
71
  }
49
72
 
50
- @available(iOS 16.0, *)
51
73
  @available(*, deprecated, renamed: "HostBackGestureBridge")
52
74
  public typealias NavigatorManager = HostBackGestureBridge
53
75
 
54
- @available(iOS 16.0, *)
55
76
  private struct WeakNavigator {
56
77
  weak var value: Navigator?
57
78
  }
@@ -9,7 +9,6 @@
9
9
 
10
10
  import SwiftUI
11
11
 
12
- @available(iOS 16.0, *)
13
12
  struct ModalScreen<Content: View>: View {
14
13
  let barrierDismissible: Bool
15
14
  let content: () -> Content
@@ -66,6 +66,11 @@ public struct NavigationOptions {
66
66
  public var fullScreenContent: Bool
67
67
  public var hiddenBack: Bool
68
68
  public var backgroundColor: Color?
69
+ // AI-GENERATED START: custom screen background view (Compose backgroundComponent)
70
+ /// Drawn instead of `backgroundColor` over the whole window: under the header and the status bar,
71
+ /// across the iPhone Duo side strips, and not shrunk by the keyboard. Pair with `headerTransparent`.
72
+ public var background: (() -> AnyView)?
73
+ // AI-GENERATED END: custom screen background view (Compose backgroundComponent)
69
74
  public var tintColor: Color?
70
75
  public var scrollable: Bool
71
76
  /// Compose's `ScrollData.scrollToTopCallback`, fired on a status-bar tap.
@@ -98,6 +103,9 @@ public struct NavigationOptions {
98
103
  fullScreenContent: Bool = false,
99
104
  hiddenBack: Bool = false,
100
105
  backgroundColor: Color? = nil,
106
+ // AI-GENERATED START: custom screen background view (Compose backgroundComponent)
107
+ background: (() -> AnyView)? = nil,
108
+ // AI-GENERATED END: custom screen background view (Compose backgroundComponent)
101
109
  tintColor: Color? = Colors.black17,
102
110
  scrollable: Bool = true,
103
111
  scrollToTopCallback: (() -> Void)? = nil,
@@ -125,6 +133,9 @@ public struct NavigationOptions {
125
133
  self.fullScreenContent = fullScreenContent
126
134
  self.hiddenBack = hiddenBack
127
135
  self.backgroundColor = backgroundColor
136
+ // AI-GENERATED START: custom screen background view (Compose backgroundComponent)
137
+ self.background = background
138
+ // AI-GENERATED END: custom screen background view (Compose backgroundComponent)
128
139
  self.tintColor = tintColor
129
140
  self.scrollable = scrollable
130
141
  self.scrollToTopCallback = scrollToTopCallback
@@ -161,6 +172,9 @@ struct OptionsStackScreen: View {
161
172
  var body: some View {
162
173
  StackScreen(
163
174
  backgroundColor: options.backgroundColor,
175
+ // AI-GENERATED START: custom screen background view (Compose backgroundComponent)
176
+ background: options.background,
177
+ // AI-GENERATED END: custom screen background view (Compose backgroundComponent)
164
178
  headerTransparent: options.headerTransparent,
165
179
  fullScreenContent: options.fullScreenContent,
166
180
  tintColor: options.tintColor ?? Colors.black17,
@@ -2,14 +2,13 @@
2
2
  // NavigationContainer.swift
3
3
  // MoMoUIKits
4
4
  //
5
- // SwiftUI mirror of Compose's `navigation/NavigationContainer.kt`: wraps NavigationStack
6
- // (iOS 16+), injects Navigator, theme, localize and font scale, and renders every registered
7
- // screen inside `StackScreen` chrome built from its options.
5
+ // SwiftUI mirror of Compose's `navigation/NavigationContainer.kt`: hosts the kit stack
6
+ // (`StackHost`, the NavHost counterpart), injects Navigator, theme, localize and font scale, and
7
+ // renders every registered screen inside `StackScreen` chrome built from its options.
8
8
  //
9
9
 
10
10
  import SwiftUI
11
11
 
12
- @available(iOS 16.0, *)
13
12
  public struct NavigationContainer<Initial: View>: View {
14
13
  @StateObject private var navigator: Navigator
15
14
  @StateObject private var applicationEnvironment: ApplicationEnvironment
@@ -71,24 +70,17 @@ public struct NavigationContainer<Initial: View>: View {
71
70
  }
72
71
 
73
72
  public var body: some View {
74
- withKitEnvironment(
75
- stack(path: $navigator.path, root: navigator.rootRoute)
76
- .overlay { overplayLayer(presentedId: nil) }
73
+ StackHost(
74
+ navigator: navigator,
75
+ entries: navigator.entries,
76
+ screen: { [navigator] entry in
77
+ guard let screen = navigator.registry.getScreen(id: entry.id) else { return AnyView(EmptyView()) }
78
+ return AnyView(ScreenHost(screen: screen, navigator: navigator))
79
+ },
80
+ decorate: { AnyView(withKitEnvironment($0)) }
77
81
  )
78
- .fullScreenCover(item: Binding(
79
- get: { navigator.coverRoute },
80
- // A close that bypasses the navigator (e.g. `dismiss()`) still tears the flow down.
81
- // SwiftUI also writes nil after a navigator-driven close; by then a new screen may
82
- // already be queued, so only a cover that is still showing counts.
83
- set: { if $0 == nil, navigator.coverRoute != nil { navigator.dismiss() } }
84
- )) { route in
85
- withKitEnvironment(
86
- stack(path: $navigator.presentedPath, root: DynamicScreenRoute(id: route.id))
87
- .overlay { overplayLayer(presentedId: route.id) }
88
- )
89
- .onAppear { navigator.coverDidAppear() }
90
- .onDisappear { navigator.coverDidDisappear() }
91
- }
82
+ // edge to edge: each screen's hosting controller takes the safe area and keyboard itself
83
+ .ignoresSafeArea()
92
84
  .onAppear {
93
85
  if navigator.composeApi == nil {
94
86
  navigator.composeApi = composeApi
@@ -110,42 +102,10 @@ public struct NavigationContainer<Initial: View>: View {
110
102
  // AI-GENERATED END: demote the MaxApi language pull to a seed of last resort
111
103
  }
112
104
  .onDisappear {
113
- // Our own cover hides the stack without leaving it; host back must still reach it.
114
- guard navigator.presented == nil else { return }
115
105
  HostBackGestureBridge.shared.detach(navigator)
116
106
  }
117
107
  }
118
108
 
119
- private func stack(path: Binding<[DynamicScreenRoute]>, root: DynamicScreenRoute) -> some View {
120
- NavigationStack(path: path) {
121
- screenView(for: root)
122
- // A reset swaps the root in place; without the id it would inherit the old root's state.
123
- .id(root.id)
124
- .navigationDestination(for: DynamicScreenRoute.self) { route in
125
- screenView(for: route)
126
- }
127
- }
128
- }
129
-
130
- @ViewBuilder
131
- private func screenView(for route: DynamicScreenRoute) -> some View {
132
- if let screen = navigator.registry.getScreen(id: route.id) {
133
- ScreenHost(screen: screen, navigator: navigator)
134
- .navigationBarBackButtonHidden(true)
135
- } else {
136
- EmptyView()
137
- }
138
- }
139
-
140
- /// Each context draws only the overplays shown over it, so one never re-mounts elsewhere.
141
- @ViewBuilder
142
- private func overplayLayer(presentedId: Int?) -> some View {
143
- if let item = navigator.overplay, item.presentedId == presentedId {
144
- OverplayHost(item: item)
145
- .momoScreenLayout()
146
- }
147
- }
148
-
149
109
  private func withKitEnvironment<Content: View>(_ content: Content) -> some View {
150
110
  content
151
111
  .environmentObject(navigator)
@@ -159,7 +119,6 @@ public struct NavigationContainer<Initial: View>: View {
159
119
 
160
120
  // MARK: - ScreenHost
161
121
 
162
- @available(iOS 16.0, *)
163
122
  private struct ScreenHost: View {
164
123
  let screen: DynamicScreen
165
124
  let navigator: Navigator
@@ -180,15 +139,26 @@ private struct ScreenHost: View {
180
139
  goBack: { navigator.onBackSafe() },
181
140
  content: screen.content
182
141
  )
142
+ // Compose draws the overplay inside the screen it was shown over, under its LocalNavigation.
143
+ .overlay { OverplayLayer(screenId: screen.id) }
183
144
  .environmentObject(navigation)
184
- // Per screen, not around the NavigationStack: a state-driven reader there stops path pushes rendering.
185
145
  .momoScreenLayout()
186
146
  }
187
147
  }
188
148
 
149
+ private struct OverplayLayer: View {
150
+ let screenId: Int
151
+ @EnvironmentObject private var navigator: Navigator
152
+
153
+ var body: some View {
154
+ if let item = navigator.overplay, item.screenId == screenId {
155
+ OverplayHost(item: item)
156
+ }
157
+ }
158
+ }
159
+
189
160
  // MARK: - Overplay host
190
161
 
191
- @available(iOS 16.0, *)
192
162
  private struct OverplayHost: View {
193
163
  let item: OverplayItem
194
164
  @EnvironmentObject var navigator: Navigator