@momo-kits/native-kits 0.167.1-duo.2-debug → 0.167.1-duo.20-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.
- package/CLAUDE.md +36 -8
- package/README.md +2 -2
- package/compose/build.gradle.kts +15 -1
- package/compose/build.gradle.kts.backup +14 -0
- package/compose/compose.podspec +1 -1
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +2 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +168 -52
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +20 -21
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +2 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +4 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +24 -14
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +9 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +11 -12
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +4 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +2 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +10 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +6 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +53 -26
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +12 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +7 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +13 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +13 -5
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +3 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +14 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +19 -21
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +2 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +5 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +3 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +7 -15
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +4 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +10 -4
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +3 -17
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +173 -21
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +43 -4
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +8 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +10 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +153 -48
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +114 -71
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +8 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +98 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +312 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +84 -18
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +33 -13
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +2 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +51 -19
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +4 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +46 -23
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +2 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +3 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +13 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +38 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +4 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +94 -0
- package/gradle.properties +1 -1
- package/ios/Application/ApplicationEnvironment.swift +17 -0
- package/ios/Application/MaxApi.swift +12 -3
- package/ios/Avatar/Avatar.swift +4 -3
- package/ios/Badge/Badge.swift +6 -4
- package/ios/Badge/BadgeDot.swift +1 -1
- package/ios/Badge/BadgeRibbon.swift +0 -1
- package/ios/Button/Button.swift +15 -12
- package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +64 -18
- package/ios/Carousel/Carousel.swift +29 -12
- package/ios/Checkbox/Checkbox.swift +7 -3
- package/ios/Chip/Chip.swift +5 -4
- package/ios/Collapse/Collapse.swift +70 -19
- package/ios/Colors+Radius+Spacing/Colors.swift +10 -1
- package/ios/Colors+Radius+Spacing/Radius.swift +6 -0
- package/ios/DateTimePicker/DateTimePicker.swift +54 -20
- package/ios/DateTimePicker/WheelPicker.swift +103 -105
- package/ios/Divider/Divider.swift +37 -4
- package/ios/Extensions/Color++.swift +38 -9
- package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
- package/ios/Extensions/SkeletonUICompat.swift +11 -0
- package/ios/Icon/Icon.swift +21 -23
- package/ios/Icon/Icons.swift +1329 -0
- package/ios/IconButton/IconButton.swift +15 -7
- package/ios/Image/Image.swift +7 -23
- package/ios/Information/Information.swift +27 -9
- package/ios/Input/ErrorView.swift +2 -2
- package/ios/Input/Input.swift +160 -147
- package/ios/Input/InputPhoneNumber.swift +12 -9
- package/ios/Input/InputSearch.swift +102 -75
- package/ios/Input/InputTextArea.swift +128 -145
- package/ios/InputDropDown/InputDropDown.swift +10 -7
- package/ios/InputMoney/InputMoney.swift +33 -11
- package/ios/InputOTP/InputOTP.swift +22 -12
- package/ios/Layout/Card.swift +50 -0
- package/ios/Layout/GridContext.swift +187 -3
- package/ios/Layout/Item.swift +5 -4
- package/ios/Layout/ScreenLayout.swift +380 -0
- package/ios/Layout/Section.swift +4 -100
- package/ios/Loader/Loader.swift +5 -9
- package/ios/Lottie/LottieView.swift +14 -2
- package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +128 -9
- package/ios/Navigation/BottomTab/BottomTab.swift +185 -0
- package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +15 -7
- package/ios/Navigation/BottomTab/DuoBarCompression.swift +105 -0
- package/ios/Navigation/BottomTab/DuoTabBar.swift +304 -0
- package/ios/{Application → Navigation/Component}/FloatingButton.swift +14 -3
- package/ios/Navigation/Component/Header.swift +531 -0
- package/ios/Navigation/Component/HeaderBackground.swift +133 -0
- package/ios/Navigation/Component/HeaderJourney.swift +73 -0
- package/ios/Navigation/Component/HeaderLocation.swift +48 -0
- package/ios/Navigation/Component/HeaderRight.swift +377 -0
- package/ios/Navigation/Component/HeaderTitle.swift +36 -0
- package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +21 -24
- package/ios/Navigation/Component/SideBar.swift +183 -0
- package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +5 -6
- package/ios/Navigation/HostBackGestureBridge.swift +78 -0
- package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +5 -2
- package/ios/Navigation/Navigation.swift +204 -0
- package/ios/Navigation/NavigationContainer.swift +194 -0
- package/ios/{Application/Navigation → Navigation}/Navigator.swift +177 -91
- package/ios/Navigation/StackScreen.swift +820 -0
- package/ios/Navigation/StackTransition.swift +400 -0
- package/ios/Navigation/SwipeBackHaptic.swift +33 -0
- package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
- package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
- package/ios/Pagination/PaginationScroll.swift +2 -2
- package/ios/Popup/PopupDisplay.swift +72 -136
- package/ios/Popup/PopupInput.swift +10 -13
- package/ios/Popup/PopupLayout.swift +140 -0
- package/ios/Popup/PopupNotify.swift +56 -73
- package/ios/Popup/PopupPromotion.swift +31 -47
- package/ios/PopupView/FullscreenPopup.swift +13 -19
- package/ios/PopupView/PopupView.swift +1 -1
- package/ios/PopupView/Utils++.swift +0 -8
- package/ios/ProgressInfo/ProgressInfo.swift +4 -2
- package/ios/Radio/Radio.swift +2 -1
- package/ios/Rating/Rating.swift +9 -1
- package/ios/Resources/dot_loading.json +558 -0
- package/ios/Resources/lottie_circle_loader.json +1 -0
- package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
- package/ios/Skeleton/Skeleton.swift +22 -27
- package/ios/Slider/Slider.swift +16 -18
- package/ios/Stepper/Stepper.swift +6 -1
- package/ios/Steps/Steps.swift +8 -6
- package/ios/SuggestAction/SuggestAction.swift +5 -0
- package/ios/Swipeable/SwipeCell.swift +10 -6
- package/ios/Switch/Switch.swift +22 -20
- package/ios/TabView/TabView.swift +102 -21
- package/ios/Tag/Tag.swift +15 -4
- package/ios/Template/Logo/Logo.swift +2 -1
- package/ios/Template/TrustBanner/TrustBanner.swift +106 -54
- package/ios/Title/Title.swift +6 -17
- package/ios/Tooltip/Tooltip.swift +29 -14
- package/ios/Typography/Text.swift +61 -19
- package/ios/Uploader/Uploader.swift +3 -1
- package/ios/native-kits.podspec +2 -1
- package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +1 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
- package/package.json +1 -1
- package/scripts/gen-ios-icons.mjs +36 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +0 -52
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +0 -183
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +0 -207
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +0 -39
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +0 -86
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +0 -76
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +0 -76
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +0 -293
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +0 -33
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +0 -110
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +0 -378
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +0 -69
- package/ios/Application/Components.swift +0 -270
- package/ios/Application/ComposeApi.swift +0 -22
- package/ios/Application/HeaderRight.swift +0 -306
- package/ios/Application/Navigation/BottomTab/BottomTab.swift +0 -104
- package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
- package/ios/Application/Navigation/HeaderTitle.swift +0 -57
- package/ios/Application/Navigation/NavigationContainer.swift +0 -213
- package/ios/Application/Navigation/NavigationOptions.swift +0 -109
- package/ios/Application/Navigation/NavigatorManager.swift +0 -22
- package/ios/Application/Navigation/component/Header.swift +0 -178
- package/ios/Application/Navigation/component/HeaderBackground.swift +0 -83
- package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
- package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
- package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
- package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -196
- package/ios/Application/Screen.swift +0 -250
- package/ios/Application/StackScreen.swift +0 -492
- /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// BottomSheet.swift
|
|
3
3
|
// MoMoUIKits
|
|
4
4
|
//
|
|
5
|
-
// SwiftUI mirror of Compose's `
|
|
5
|
+
// SwiftUI mirror of Compose's `navigation/BottomSheet.kt`.
|
|
6
6
|
// A sheet that slides up from the bottom over a dimmed scrim, with a drag
|
|
7
7
|
// handle, a header (title or custom), a divider and the content body.
|
|
8
8
|
// Dragging down past a threshold (or tapping the scrim) dismisses it.
|
|
@@ -35,16 +35,20 @@ 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
|
|
42
41
|
let barrierDismissible: Bool
|
|
42
|
+
var draggable: Bool = true
|
|
43
|
+
/// Keeps the content above the home indicator (Compose `AppNavigationBar` padding).
|
|
44
|
+
var useBottomInset: Bool = true
|
|
45
|
+
var footer: (() -> AnyView)? = nil
|
|
43
46
|
let content: () -> Content
|
|
44
47
|
|
|
45
48
|
@EnvironmentObject private var navigator: Navigator
|
|
46
49
|
@Environment(\.appTheme) private var theme
|
|
47
50
|
@Environment(\.applicationEnvironment) private var appEnv
|
|
51
|
+
@Environment(\.screenLayout) private var layout
|
|
48
52
|
|
|
49
53
|
/// Vertical translation of the sheet; starts off-screen and animates to 0.
|
|
50
54
|
@State private var sheetOffset: CGFloat = UIScreen.main.bounds.height
|
|
@@ -66,8 +70,36 @@ struct BottomSheetView<Content: View>: View {
|
|
|
66
70
|
/// steady-state (fully open) scrim peaks at 0.6, matching ModalScreen's peak.
|
|
67
71
|
private let maxDynamicAlpha: CGFloat = 0.3
|
|
68
72
|
|
|
69
|
-
|
|
70
|
-
private var
|
|
73
|
+
// UIScreen stays on the outer display on iPhone Duo, even when unfolded
|
|
74
|
+
private var screenHeight: CGFloat { isDuo ? layout.height : UIScreen.main.bounds.height }
|
|
75
|
+
|
|
76
|
+
private var isDuo: Bool { layout.isDuoDevice }
|
|
77
|
+
// iPhone Duo inner display: center the sheet instead of spanning the width
|
|
78
|
+
private var isDuoCentered: Bool { isDuo && layout.width >= 600 }
|
|
79
|
+
|
|
80
|
+
/// Keeps the portrait width in landscape, like system sheets.
|
|
81
|
+
private var duoCenteredWidth: CGFloat {
|
|
82
|
+
min(
|
|
83
|
+
min(layout.width, layout.height) - Spacing.L * 2,
|
|
84
|
+
layout.width - max(layout.insets.leading, layout.insets.trailing, Spacing.L) * 2
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// Landscape side buttons span the full height, so the content is inset; in portrait the
|
|
89
|
+
/// side status bar only covers the top corner.
|
|
90
|
+
private var sideInsetLeading: CGFloat {
|
|
91
|
+
isDuo && !isDuoCentered && layout.width > layout.height ? layout.insets.leading : 0
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private var sideInsetTrailing: CGFloat {
|
|
95
|
+
isDuo && !isDuoCentered && layout.width > layout.height ? layout.insets.trailing : 0
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// The keyboard filler is part of the sheet, so capping the sheet shrinks the content by it.
|
|
99
|
+
private var maxHeight: CGFloat {
|
|
100
|
+
isDuo ? layout.height - max(layout.duoTopInset, Spacing.XL) : screenHeight - 90
|
|
101
|
+
}
|
|
102
|
+
private var bottomInset: CGFloat { useBottomInset ? safeAreaBottomInset() : 0 }
|
|
71
103
|
/// While the keyboard is up, it already covers the home-indicator area, so it
|
|
72
104
|
/// replaces (rather than adds to) the usual bottom-inset filler.
|
|
73
105
|
private var effectiveBottomInset: CGFloat { keyboardHeight > 0 ? keyboardHeight : bottomInset }
|
|
@@ -110,7 +142,7 @@ struct BottomSheetView<Content: View>: View {
|
|
|
110
142
|
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillChangeFrameNotification)) { notification in
|
|
111
143
|
guard let endFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
|
|
112
144
|
withAnimation(.easeOut(duration: 0.25)) {
|
|
113
|
-
keyboardHeight =
|
|
145
|
+
keyboardHeight = screenHeight - endFrame.origin.y
|
|
114
146
|
}
|
|
115
147
|
}
|
|
116
148
|
.onAppear {
|
|
@@ -138,23 +170,34 @@ struct BottomSheetView<Content: View>: View {
|
|
|
138
170
|
headerView
|
|
139
171
|
.frame(height: 56)
|
|
140
172
|
.padding(.horizontal, Spacing.M)
|
|
173
|
+
.padding(.leading, sideInsetLeading)
|
|
174
|
+
.padding(.trailing, sideInsetTrailing)
|
|
141
175
|
}
|
|
142
176
|
.frame(maxWidth: .infinity)
|
|
143
177
|
.frame(height: 72)
|
|
144
178
|
.contentShape(Rectangle())
|
|
145
|
-
.gesture(dragGesture)
|
|
179
|
+
.gesture(dragGesture, including: draggable ? .all : .subviews)
|
|
146
180
|
|
|
147
181
|
Rectangle()
|
|
148
182
|
.fill(theme.colors.border.default)
|
|
149
183
|
.frame(height: 1)
|
|
150
184
|
|
|
151
185
|
content()
|
|
186
|
+
.padding(.leading, sideInsetLeading)
|
|
187
|
+
.padding(.trailing, sideInsetTrailing)
|
|
188
|
+
// a tall sheet keeps its header (and footer) and gives the content what is left
|
|
189
|
+
.layoutPriority(-1)
|
|
152
190
|
|
|
153
|
-
|
|
191
|
+
if let footer {
|
|
192
|
+
BottomSheetFooter(content: footer)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
(footer != nil ? theme.colors.background.surface : backgroundColor)
|
|
154
196
|
.frame(height: effectiveBottomInset)
|
|
155
197
|
}
|
|
156
|
-
.frame(maxWidth: .infinity)
|
|
157
|
-
.
|
|
198
|
+
.frame(maxWidth: isDuoCentered ? duoCenteredWidth : .infinity)
|
|
199
|
+
.modifier(CapHeight(maxHeight: maxHeight))
|
|
200
|
+
.clipped()
|
|
158
201
|
.background(
|
|
159
202
|
GeometryReader { geo in
|
|
160
203
|
Color.clear.onAppear { sheetHeight = geo.size.height }
|
|
@@ -247,7 +290,83 @@ struct BottomSheetView<Content: View>: View {
|
|
|
247
290
|
}
|
|
248
291
|
}
|
|
249
292
|
|
|
293
|
+
/// Compose's `heightIn(max:)`. `.frame(maxHeight:)` grows to the cap whenever the proposal is
|
|
294
|
+
/// larger, leaving a gap above the header; this sizes the sheet to its content and only caps it
|
|
295
|
+
/// when taller.
|
|
296
|
+
private struct CapHeight: ViewModifier {
|
|
297
|
+
let maxHeight: CGFloat
|
|
298
|
+
|
|
299
|
+
func body(content: Content) -> some View {
|
|
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 }
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
250
333
|
@available(iOS 16.0, *)
|
|
334
|
+
private struct CapHeightLayout: Layout {
|
|
335
|
+
let maxHeight: CGFloat
|
|
336
|
+
|
|
337
|
+
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
|
|
338
|
+
guard let subview = subviews.first else { return .zero }
|
|
339
|
+
let cap = min(maxHeight, proposal.height ?? .infinity)
|
|
340
|
+
// ideal first: a scroll view would otherwise fill whatever height it is offered
|
|
341
|
+
let ideal = subview.sizeThatFits(ProposedViewSize(width: proposal.width, height: nil))
|
|
342
|
+
return subview.sizeThatFits(ProposedViewSize(width: proposal.width, height: min(ideal.height, cap)))
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
|
|
346
|
+
subviews.first?.place(at: bounds.origin, proposal: ProposedViewSize(bounds.size))
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/// Mirrors Compose's `BottomSheetFooter`: a surface band with a soft shadow cast on the content.
|
|
351
|
+
private struct BottomSheetFooter: View {
|
|
352
|
+
let content: () -> AnyView
|
|
353
|
+
|
|
354
|
+
@Environment(\.appTheme) private var theme
|
|
355
|
+
|
|
356
|
+
var body: some View {
|
|
357
|
+
content()
|
|
358
|
+
.frame(maxWidth: .infinity)
|
|
359
|
+
.padding(.vertical, Spacing.S)
|
|
360
|
+
.background(theme.colors.background.surface)
|
|
361
|
+
.overlay(alignment: .top) {
|
|
362
|
+
LinearGradient(colors: [.clear, Color.black.opacity(0.05)], startPoint: .top, endPoint: .bottom)
|
|
363
|
+
.frame(height: 6)
|
|
364
|
+
.offset(y: -6)
|
|
365
|
+
.allowsHitTesting(false)
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
251
370
|
private func safeAreaBottomInset() -> CGFloat {
|
|
252
371
|
let bottomInset = UIApplication.shared.connectedScenes
|
|
253
372
|
.compactMap { ($0 as? UIWindowScene)?.keyWindow?.safeAreaInsets.bottom }
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
//
|
|
2
|
+
// BottomTab.swift
|
|
3
|
+
// MoMoUIKits
|
|
4
|
+
//
|
|
5
|
+
// SwiftUI mirror of Compose's `navigation/bottomtab/BottomTab.kt`: tabbed screens, each in
|
|
6
|
+
// `StackScreen` chrome built from its own `Navigation`, over a `BottomTabBar` (or the iPhone Duo
|
|
7
|
+
// rail / floating bar). Switching tabs cross-fades + scales the content.
|
|
8
|
+
//
|
|
9
|
+
|
|
10
|
+
import SwiftUI
|
|
11
|
+
|
|
12
|
+
public struct BottomTab: View {
|
|
13
|
+
private let items: [BottomTabItem]
|
|
14
|
+
private let floatingButton: BottomTabFloatingButton?
|
|
15
|
+
private let compressionBehavior: DuoBarCompressionBehavior
|
|
16
|
+
|
|
17
|
+
@EnvironmentObject private var navigator: Navigator
|
|
18
|
+
/// The screen hosting the tabs (Compose's `LocalNavigation` in BottomTab).
|
|
19
|
+
@OptionalEnvironmentObject private var hostNavigation: Navigation?
|
|
20
|
+
@Environment(\.screenLayout) private var layout
|
|
21
|
+
@Environment(\.appTheme) private var theme
|
|
22
|
+
@StateObject private var tabNavigations = TabNavigations()
|
|
23
|
+
@State private var selectedIndex: Int = 0
|
|
24
|
+
@State private var toolbarHeight = DuoBarCompression.defaultToolbarHeight
|
|
25
|
+
@State private var expandedBar: DuoBar?
|
|
26
|
+
@State private var openedBar: DuoBar?
|
|
27
|
+
|
|
28
|
+
/// `compressionBehavior`: on iPhone Duo, when the side bar can't hold both bars, which one
|
|
29
|
+
/// starts compressed; tapping the compressed one swaps them.
|
|
30
|
+
public init(
|
|
31
|
+
items: [BottomTabItem],
|
|
32
|
+
floatingButton: BottomTabFloatingButton? = nil,
|
|
33
|
+
compressionBehavior: DuoBarCompressionBehavior = .toolbar
|
|
34
|
+
) {
|
|
35
|
+
self.items = items
|
|
36
|
+
self.floatingButton = floatingButton
|
|
37
|
+
self.compressionBehavior = compressionBehavior
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private var bottomInset: CGFloat { safeAreaBottomInset() }
|
|
41
|
+
// the Duo bars overlay the content or sit in the side strip, so only the home indicator is kept clear
|
|
42
|
+
private var reservedBottom: CGFloat {
|
|
43
|
+
layout.isIPhoneDuo ? layout.insets.bottom : BOTTOM_TAB_BAR_HEIGHT + bottomInset + Spacing.S
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private var compression: DuoBarCompression {
|
|
47
|
+
DuoBarCompression.resolve(
|
|
48
|
+
layout: layout,
|
|
49
|
+
tabCount: items.count,
|
|
50
|
+
hasQr: floatingButton != nil,
|
|
51
|
+
toolbarHeight: toolbarHeight,
|
|
52
|
+
// 'toolbar' compression keeps the tab bar whole until the toolbar is tapped
|
|
53
|
+
expanded: Binding(
|
|
54
|
+
get: { expandedBar ?? (compressionBehavior == .toolbar ? .tabBar : .toolbar) },
|
|
55
|
+
set: { expandedBar = $0 }
|
|
56
|
+
),
|
|
57
|
+
opened: $openedBar,
|
|
58
|
+
setToolbarHeight: { height in
|
|
59
|
+
if toolbarHeight != height { toolbarHeight = height }
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private func duoInsets(_ compression: DuoBarCompression) -> DuoTabBarInsets {
|
|
65
|
+
layout.duoTabBarInsets(
|
|
66
|
+
itemCount: items.count,
|
|
67
|
+
hasFloatingButton: floatingButton != nil,
|
|
68
|
+
tabBarCompressed: compression.tabBarCompressed
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public var body: some View {
|
|
73
|
+
let compression = compression
|
|
74
|
+
ZStack(alignment: .bottom) {
|
|
75
|
+
// Tab content area, inset above the bar.
|
|
76
|
+
ZStack {
|
|
77
|
+
ForEach(items.indices, id: \.self) { index in
|
|
78
|
+
if index == selectedIndex {
|
|
79
|
+
tabScreen(for: index)
|
|
80
|
+
.transition(.scale(scale: 0.97).combined(with: .opacity))
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
.padding(.bottom, reservedBottom)
|
|
85
|
+
.environment(\.duoTabBarInsets, duoInsets(compression))
|
|
86
|
+
.environment(\.duoBarCompression, compression)
|
|
87
|
+
.animation(.easeInOut(duration: 0.2), value: selectedIndex)
|
|
88
|
+
|
|
89
|
+
if layout.isIPhoneDuo {
|
|
90
|
+
DuoTabBar(
|
|
91
|
+
items: items,
|
|
92
|
+
floatingButton: floatingButton,
|
|
93
|
+
selectedIndex: selectedIndex,
|
|
94
|
+
onTabSelected: selectTab,
|
|
95
|
+
compression: compression
|
|
96
|
+
)
|
|
97
|
+
} else {
|
|
98
|
+
// Tab bar + home-indicator filler.
|
|
99
|
+
VStack(spacing: 0) {
|
|
100
|
+
BottomTabBar(
|
|
101
|
+
items: items,
|
|
102
|
+
floatingButton: floatingButton,
|
|
103
|
+
selectedIndex: selectedIndex,
|
|
104
|
+
onTabSelected: selectTab
|
|
105
|
+
)
|
|
106
|
+
theme.colors.background.surface
|
|
107
|
+
.frame(height: bottomInset + Spacing.S)
|
|
108
|
+
.ignoresSafeArea(edges: .horizontal)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
113
|
+
.ignoresSafeArea(edges: .bottom)
|
|
114
|
+
.preference(key: IsBottomTabRootKey.self, value: true)
|
|
115
|
+
.onAppear {
|
|
116
|
+
hostNavigation?.setOptions {
|
|
117
|
+
$0.headerType = .none
|
|
118
|
+
$0.useAvoidKeyboard = false
|
|
119
|
+
}
|
|
120
|
+
updateTabBack()
|
|
121
|
+
}
|
|
122
|
+
.onChange(of: selectedIndex) { _ in updateTabBack() }
|
|
123
|
+
.onDisappear {
|
|
124
|
+
// both live outside this view
|
|
125
|
+
navigator.tabBackHandler = nil
|
|
126
|
+
HostBackGestureBridge.shared.shouldUseFloatingArrowBack = false
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private func selectTab(_ index: Int) {
|
|
131
|
+
if index != selectedIndex { selectedIndex = index }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/// Back off the first tab returns to it; the host shows its floating arrow there (RN parity).
|
|
135
|
+
private func updateTabBack() {
|
|
136
|
+
let isFirstTab = selectedIndex == 0
|
|
137
|
+
HostBackGestureBridge.shared.shouldUseFloatingArrowBack = !isFirstTab
|
|
138
|
+
navigator.tabBackHandler = isFirstTab ? nil : (hostNavigation?.id ?? -1, { selectedIndex = 0 })
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private func tabScreen(for index: Int) -> some View {
|
|
142
|
+
let item = items[index]
|
|
143
|
+
return TabScreenHost(
|
|
144
|
+
navigation: tabNavigations.navigation(for: index, options: item.options),
|
|
145
|
+
screen: item.screen,
|
|
146
|
+
goBack: { navigator.onBackSafe() }
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/// Keeps each tab's `Navigation`, so options a tab set at runtime survive switching away.
|
|
152
|
+
private final class TabNavigations: ObservableObject {
|
|
153
|
+
private var navigations: [Int: Navigation] = [:]
|
|
154
|
+
|
|
155
|
+
func navigation(for index: Int, options: NavigationOptions?) -> Navigation {
|
|
156
|
+
if let navigation = navigations[index] { return navigation }
|
|
157
|
+
let navigation = Navigation(bottomTabIndex: index, options: options)
|
|
158
|
+
navigations[index] = navigation
|
|
159
|
+
return navigation
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private struct TabScreenHost: View {
|
|
164
|
+
@ObservedObject var navigation: Navigation
|
|
165
|
+
let screen: () -> AnyView
|
|
166
|
+
let goBack: () -> Void
|
|
167
|
+
|
|
168
|
+
var body: some View {
|
|
169
|
+
OptionsStackScreen(
|
|
170
|
+
options: navigation.options,
|
|
171
|
+
screenName: navigation.options.screenName ?? "",
|
|
172
|
+
goBack: goBack,
|
|
173
|
+
content: screen
|
|
174
|
+
)
|
|
175
|
+
.environmentObject(navigation)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/// Bottom safe-area inset (home indicator).
|
|
180
|
+
private func safeAreaBottomInset() -> CGFloat {
|
|
181
|
+
let bottomInset = UIApplication.shared.connectedScenes
|
|
182
|
+
.compactMap { ($0 as? UIWindowScene)?.keyWindow?.safeAreaInsets.bottom }
|
|
183
|
+
.first ?? 0
|
|
184
|
+
return min(bottomInset, 21)
|
|
185
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// BottomTabBar.swift
|
|
3
3
|
// MoMoUIKits
|
|
4
4
|
//
|
|
5
|
-
// SwiftUI mirror of Compose's `
|
|
5
|
+
// SwiftUI mirror of Compose's `navigation/bottomtab/BottomTabBar.kt`.
|
|
6
6
|
// Renders the row of tab items, an animated selection indicator, a top shadow,
|
|
7
7
|
// badges/dots, and an optional center floating button (with the curved notch).
|
|
8
8
|
//
|
|
@@ -65,7 +65,11 @@ struct BottomTabBar: View {
|
|
|
65
65
|
let selectedIndex: Int
|
|
66
66
|
let onTabSelected: (Int) -> Void
|
|
67
67
|
|
|
68
|
-
private var
|
|
68
|
+
@Environment(\.screenLayout) private var layout
|
|
69
|
+
@Environment(\.appTheme) private var theme
|
|
70
|
+
|
|
71
|
+
// the bar sits between the side safe areas
|
|
72
|
+
private var screenWidth: CGFloat { layout.width - layout.insets.leading - layout.insets.trailing }
|
|
69
73
|
private var mid: Int { items.count / 2 }
|
|
70
74
|
|
|
71
75
|
private var tabWidth: CGFloat {
|
|
@@ -95,11 +99,11 @@ struct BottomTabBar: View {
|
|
|
95
99
|
}
|
|
96
100
|
.frame(maxWidth: .infinity)
|
|
97
101
|
.frame(height: BOTTOM_TAB_BAR_HEIGHT)
|
|
98
|
-
.background(
|
|
102
|
+
.background(theme.colors.background.surface.ignoresSafeArea(edges: .horizontal))
|
|
99
103
|
|
|
100
104
|
// Animated selection indicator.
|
|
101
105
|
RoundedRectangle(cornerRadius: 2)
|
|
102
|
-
.fill(
|
|
106
|
+
.fill(theme.colors.primary)
|
|
103
107
|
.frame(width: 44, height: 2)
|
|
104
108
|
.offset(x: indicatorOffset, y: -BOTTOM_TAB_BAR_HEIGHT + 2)
|
|
105
109
|
.animation(.easeInOut(duration: 0.25), value: selectedIndex)
|
|
@@ -140,7 +144,9 @@ private struct TabBarItem: View {
|
|
|
140
144
|
let selected: Bool
|
|
141
145
|
let onClick: () -> Void
|
|
142
146
|
|
|
143
|
-
private var
|
|
147
|
+
@Environment(\.appTheme) private var theme
|
|
148
|
+
|
|
149
|
+
private var tintColor: Color { selected ? theme.colors.primary : theme.colors.text.hint }
|
|
144
150
|
|
|
145
151
|
/// Mirrors Compose's `formatLabel`: hide a badge that is the number "0".
|
|
146
152
|
private var badgeLabel: String? {
|
|
@@ -192,18 +198,20 @@ private struct TabBarItem: View {
|
|
|
192
198
|
private struct CenterFloatingButton: View {
|
|
193
199
|
let data: BottomTabFloatingButton
|
|
194
200
|
|
|
201
|
+
@Environment(\.appTheme) private var theme
|
|
202
|
+
|
|
195
203
|
var body: some View {
|
|
196
204
|
VStack(spacing: 0) {
|
|
197
205
|
CurvedContainer()
|
|
198
206
|
Circle()
|
|
199
|
-
.fill(
|
|
207
|
+
.fill(theme.colors.primary)
|
|
200
208
|
.frame(width: 48, height: 48)
|
|
201
209
|
.overlay(Icon(source: data.icon, size: 28, color: .white))
|
|
202
210
|
Spacer().frame(height: 4)
|
|
203
211
|
MomoText(data.label, typography: .labelXsMedium, color: Colors.black01, textAlign: .center, maxLines: 1)
|
|
204
212
|
.frame(maxWidth: .infinity)
|
|
205
213
|
.frame(height: 14)
|
|
206
|
-
.background(
|
|
214
|
+
.background(theme.colors.primary)
|
|
207
215
|
.clipShape(RoundedRectangle(cornerRadius: Radius.XL))
|
|
208
216
|
}
|
|
209
217
|
.frame(width: bottomTabFloatingButtonWidth)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
//
|
|
2
|
+
// DuoBarCompression.swift
|
|
3
|
+
// MoMoUIKits
|
|
4
|
+
//
|
|
5
|
+
// Port of RN `BottomTab/DuoBarCompression.tsx` (HIG, Designing for iPhone Duo: Vertical controls):
|
|
6
|
+
// keeps the side bar's toolbar and tab bar clear of each other.
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
import SwiftUI
|
|
10
|
+
|
|
11
|
+
/// iPhone Duo: which side bar bar starts compressed when the strip cannot hold both whole.
|
|
12
|
+
public enum DuoBarCompressionBehavior {
|
|
13
|
+
case toolbar, tabBar
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
enum DuoBar {
|
|
17
|
+
case toolbar, tabBar
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
struct DuoBarCompression {
|
|
21
|
+
var toolbarCompressed = false
|
|
22
|
+
var tabBarCompressed = false
|
|
23
|
+
/// The open rail needs the toolbar button slot too: only Back stays.
|
|
24
|
+
var toolbarHidden = false
|
|
25
|
+
var expandToolbar: () -> Void = {}
|
|
26
|
+
var expandTabBar: () -> Void = {}
|
|
27
|
+
/// A rail opened from the compressed state closes once a tab is picked.
|
|
28
|
+
var onTabSelected: () -> Void = {}
|
|
29
|
+
/// Side bar toolkits report their full height so the tab bar knows what is left.
|
|
30
|
+
var setToolbarHeight: (CGFloat) -> Void = { _ in }
|
|
31
|
+
|
|
32
|
+
/// Space kept between the top and the bottom groups (HIG Vertical controls).
|
|
33
|
+
static let groupGap = Spacing.M
|
|
34
|
+
/// Back, then the right toolkit: help + home capsule.
|
|
35
|
+
static let defaultToolbarHeight = DuoMetrics.controlSize + Spacing.M + 2 * DuoMetrics.itemPitch
|
|
36
|
+
/// Back, then the single button the toolbar folds into.
|
|
37
|
+
private static let compressedToolbarHeight = 2 * DuoMetrics.controlSize + Spacing.M
|
|
38
|
+
/// Back alone, while a rail opened from the compressed state takes the toolbar slot.
|
|
39
|
+
private static let backOnlyHeight = DuoMetrics.controlSize
|
|
40
|
+
|
|
41
|
+
/// - both fit: both stay whole
|
|
42
|
+
/// - one fits whole beside the other compressed: one is compressed, tapping it swaps them
|
|
43
|
+
/// - otherwise both rest compressed; tapping one opens it for a moment (the rail closes
|
|
44
|
+
/// again once a tab is picked)
|
|
45
|
+
static func resolve(
|
|
46
|
+
layout: ScreenLayout,
|
|
47
|
+
tabCount: Int,
|
|
48
|
+
hasQr: Bool,
|
|
49
|
+
toolbarHeight: CGFloat,
|
|
50
|
+
expanded: Binding<DuoBar>,
|
|
51
|
+
opened: Binding<DuoBar?>,
|
|
52
|
+
setToolbarHeight: @escaping (CGFloat) -> Void
|
|
53
|
+
) -> DuoBarCompression {
|
|
54
|
+
guard let sideBar = layout.sideBar else { return DuoBarCompression() }
|
|
55
|
+
let room = layout.height - sideBar.bottom - sideBar.top - groupGap
|
|
56
|
+
let tabBar = duoRailHeight(itemCount: tabCount, hasQr: hasQr)
|
|
57
|
+
let compressedTabBar = duoRailHeight(itemCount: 1, hasQr: hasQr, compressed: true)
|
|
58
|
+
func fits(_ toolbar: CGFloat, _ tabs: CGFloat) -> Bool { toolbar + tabs <= room }
|
|
59
|
+
|
|
60
|
+
if fits(toolbarHeight, tabBar) {
|
|
61
|
+
return DuoBarCompression(setToolbarHeight: setToolbarHeight)
|
|
62
|
+
}
|
|
63
|
+
if fits(compressedToolbarHeight, tabBar) && fits(toolbarHeight, compressedTabBar) {
|
|
64
|
+
return DuoBarCompression(
|
|
65
|
+
toolbarCompressed: expanded.wrappedValue == .tabBar,
|
|
66
|
+
tabBarCompressed: expanded.wrappedValue == .toolbar,
|
|
67
|
+
expandToolbar: { expanded.wrappedValue = .toolbar },
|
|
68
|
+
expandTabBar: { expanded.wrappedValue = .tabBar },
|
|
69
|
+
setToolbarHeight: setToolbarHeight
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let expandToolbar = { opened.wrappedValue = .toolbar }
|
|
74
|
+
let expandTabBar = { opened.wrappedValue = .tabBar }
|
|
75
|
+
if opened.wrappedValue == .tabBar && fits(backOnlyHeight, tabBar) {
|
|
76
|
+
return DuoBarCompression(
|
|
77
|
+
toolbarCompressed: true,
|
|
78
|
+
tabBarCompressed: false,
|
|
79
|
+
toolbarHidden: true,
|
|
80
|
+
expandToolbar: expandToolbar,
|
|
81
|
+
expandTabBar: expandTabBar,
|
|
82
|
+
onTabSelected: { opened.wrappedValue = nil },
|
|
83
|
+
setToolbarHeight: setToolbarHeight
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
return DuoBarCompression(
|
|
87
|
+
toolbarCompressed: opened.wrappedValue != .toolbar,
|
|
88
|
+
tabBarCompressed: true,
|
|
89
|
+
expandToolbar: expandToolbar,
|
|
90
|
+
expandTabBar: expandTabBar,
|
|
91
|
+
setToolbarHeight: setToolbarHeight
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private struct DuoBarCompressionKey: EnvironmentKey {
|
|
97
|
+
static let defaultValue = DuoBarCompression()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
extension EnvironmentValues {
|
|
101
|
+
var duoBarCompression: DuoBarCompression {
|
|
102
|
+
get { self[DuoBarCompressionKey.self] }
|
|
103
|
+
set { self[DuoBarCompressionKey.self] = newValue }
|
|
104
|
+
}
|
|
105
|
+
}
|