From 3a0f7a0d9b5abc2fde62ebefd8424dd049b21d8f Mon Sep 17 00:00:00 2001 From: marchidalgo Date: Sun, 22 Feb 2026 20:15:46 +0100 Subject: [PATCH 1/3] feat: add cross-platform BottomActionBar and TopActionBar components Introduce cross-platform `bottomActionBar` and `topActionBar` extensions for SwiftUI View. These components allow you to easily add action bars at the top or bottom of a UI view, supporting both iOS and Android platforms with platform-specific implementations. For iOS, utilize SwiftUI's view modifiers to overlay buttons with safe area insets. For Android, employ Compose UI components to integrate the action bars at appropriate positions. Add previews for `Top Button` and `Bottom Button` configurations to demonstrate the usage of these new action bars in SwiftUI previews. This enhancement improves UI consistency and interoperability across platforms. --- .../SwiftUI/Extensions/BottomActionBar.swift | 227 ++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 Sources/BSWInterfaceKit/SwiftUI/Extensions/BottomActionBar.swift diff --git a/Sources/BSWInterfaceKit/SwiftUI/Extensions/BottomActionBar.swift b/Sources/BSWInterfaceKit/SwiftUI/Extensions/BottomActionBar.swift new file mode 100644 index 00000000..67ecf7d7 --- /dev/null +++ b/Sources/BSWInterfaceKit/SwiftUI/Extensions/BottomActionBar.swift @@ -0,0 +1,227 @@ +#if os(Android) +import SkipFuseUI +#else +import SwiftUI +#endif + +#if os(Android) +#if SKIP +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Divider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +#endif +#endif + +#if canImport(Darwin) + +#Preview("Top Button") { + NavigationStack { + let fruits = [ + "Apple", "Banana", "Cherry", "Date", "Elderberry", + "Fig", "Grape", "Honeydew", "Kiwi", "Lemon", + "Mango", "Nectarine", "Orange", "Papaya", "Quince", + "Raspberry", "Strawberry", "Tangerine", "Ugli Fruit", "Watermelon" + ] + List(fruits, id: \.self, rowContent: Text.init) + .topActionBar { + AsyncButton( + action: { try await Task.sleep(for: .seconds(1)) }, + label: { + HStack { Spacer(); Text("Hello"); Spacer() } + } + ) + } + } +} + +#Preview("Bottom Button") { + NavigationStack { + let fruits = [ + "Apple", "Banana", "Cherry", "Date", "Elderberry", + "Fig", "Grape", "Honeydew", "Kiwi", "Lemon", + "Mango", "Nectarine", "Orange", "Papaya", "Quince", + "Raspberry", "Strawberry", "Tangerine", "Ugli Fruit", "Watermelon" + ] + List(fruits, id: \.self, rowContent: Text.init) + .bottomActionBar { + AsyncButton( + action: { try await Task.sleep(for: .seconds(1)) }, + label: { + HStack { Spacer(); Text("Hello"); Spacer() } + } + ) + } + } +} +#endif + +public extension View { + + @ViewBuilder + func bottomActionBar( + @ViewBuilder button: () -> T + ) -> some View { + let actionButton = button() + + #if os(Android) + ComposeView { + AndroidActionBarScaffold( + content: self, + actionBar: actionButton, + isTopBar: false + ) + } + #else + self.bottomActionBarInset(actionButton) + #endif + } + + @ViewBuilder + func topActionBar( + @ViewBuilder topBar: () -> T + ) -> some View { + let actionTopBar = topBar() + + #if os(Android) + ComposeView { + AndroidActionBarScaffold( + content: self, + actionBar: actionTopBar, + isTopBar: true + ) + } + #else + self.topActionBarInset(actionTopBar) + #endif + } +} + +#if os(iOS) +private extension View { + + @ViewBuilder + func bottomActionBarInset(_ actionButton: T) -> some View { + self.actionBarInset( + actionButton, + edge: .bottom, + dividerAlignment: .top + ) + } + + @ViewBuilder + func topActionBarInset(_ actionTopBar: T) -> some View { + self.actionBarInset( + actionTopBar, + edge: .top, + dividerAlignment: .bottom + ) + } + + @ViewBuilder + func actionBarInset( + _ actionBar: T, + edge: VerticalEdge, + dividerAlignment: Alignment + ) -> some View { + if #available(iOS 26.0, macOS 26.0, *) { + self.safeAreaBar(edge: edge) { + actionBar + } + } else { + self.safeAreaInset(edge: edge) { + actionBar + .background(.regularMaterial) + .overlay(alignment: dividerAlignment) { + Divider() + } + } + } + } +} +#endif + +#if os(Android) +#if SKIP + +struct AndroidActionBarScaffold: ContentComposer { + typealias BridgedView = any View + let content: BridgedView + let actionBar: BridgedView + let isTopBar: Bool + + @Composable + func Compose(context: ComposeContext) { + ComposeContainer(modifier: context.modifier, fillWidth: true, fillHeight: true) { modifier in + Scaffold( + modifier: modifier.fillMaxSize(), + contentWindowInsets: WindowInsets(0), + topBar: { + if isTopBar { + Surface(color: MaterialTheme.colorScheme.surface) { + Column( + modifier: Modifier + .fillMaxWidth() + ) { + actionBar.Compose( + context: context.content( + modifier: Modifier.fillMaxWidth() + ) + ) + Divider() + } + } + } + }, + bottomBar: { + if !isTopBar { + Surface(color: MaterialTheme.colorScheme.surface) { + Box( + modifier: Modifier + .fillMaxWidth() + .imePadding() + ) { + Divider() + Box( + modifier: Modifier + .fillMaxWidth() + ) { + actionBar.Compose( + context: context.content( + modifier: Modifier.fillMaxWidth() + ) + ) + } + } + } + } + }, + content: { innerPadding in + Box( + modifier: Modifier + .fillMaxSize() + .padding(innerPadding) + ) { + content.Compose( + context: context.content( + modifier: Modifier.fillMaxSize() + ) + ) + } + } + ) + } + } +} + +#endif +#endif From ee36b09ad01385ce82762a80a7e66120e294225d Mon Sep 17 00:00:00 2001 From: marchidalgo Date: Sun, 22 Feb 2026 20:22:46 +0100 Subject: [PATCH 2/3] refactor: rename BottomActionBar to ActionBar and consolidate code Rename `BottomActionBar.swift` to `ActionBar.swift` and move it from the `Extensions` directory to `ViewModifiers` to better reflect its purpose and usage. Remove duplicate code by unifying `bottomActionBar` and `topActionBar` implementations into a single flexible method `actionBarInset`. This change improves code maintainability and readability by eliminating redundant extensions and consolidating platform-specific logic. --- .../ActionBar.swift} | 30 ++++++------------- 1 file changed, 9 insertions(+), 21 deletions(-) rename Sources/BSWInterfaceKit/SwiftUI/{Extensions/BottomActionBar.swift => ViewModifiers/ActionBar.swift} (95%) diff --git a/Sources/BSWInterfaceKit/SwiftUI/Extensions/BottomActionBar.swift b/Sources/BSWInterfaceKit/SwiftUI/ViewModifiers/ActionBar.swift similarity index 95% rename from Sources/BSWInterfaceKit/SwiftUI/Extensions/BottomActionBar.swift rename to Sources/BSWInterfaceKit/SwiftUI/ViewModifiers/ActionBar.swift index 67ecf7d7..d3c486c5 100644 --- a/Sources/BSWInterfaceKit/SwiftUI/Extensions/BottomActionBar.swift +++ b/Sources/BSWInterfaceKit/SwiftUI/ViewModifiers/ActionBar.swift @@ -82,7 +82,11 @@ public extension View { ) } #else - self.bottomActionBarInset(actionButton) + self.actionBarInset( + actionButton, + edge: .bottom, + dividerAlignment: .top + ) #endif } @@ -101,31 +105,16 @@ public extension View { ) } #else - self.topActionBarInset(actionTopBar) - #endif - } -} - -#if os(iOS) -private extension View { - - @ViewBuilder - func bottomActionBarInset(_ actionButton: T) -> some View { - self.actionBarInset( - actionButton, - edge: .bottom, - dividerAlignment: .top - ) - } - - @ViewBuilder - func topActionBarInset(_ actionTopBar: T) -> some View { self.actionBarInset( actionTopBar, edge: .top, dividerAlignment: .bottom ) + #endif } +} + +private extension View { @ViewBuilder func actionBarInset( @@ -148,7 +137,6 @@ private extension View { } } } -#endif #if os(Android) #if SKIP From 087efe17830722d51d632cd3353aa6d333c96288 Mon Sep 17 00:00:00 2001 From: marchidalgo Date: Sun, 22 Feb 2026 20:26:43 +0100 Subject: [PATCH 3/3] refactor: isolate iOS/macOS specific code with preprocessor directive Wrap the `actionBarInset` method with an `#if os(iOS) || os(macOS)` directive to ensure this private function is only included on iOS and macOS platforms. This change keeps the codebase platform-specific, making it maintainable and preventing unnecessary compilation on unsupported platforms like Android. It also aligns with the pattern used for the `AndroidActionBarScaffold`, improving consistency. --- Sources/BSWInterfaceKit/SwiftUI/ViewModifiers/ActionBar.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/BSWInterfaceKit/SwiftUI/ViewModifiers/ActionBar.swift b/Sources/BSWInterfaceKit/SwiftUI/ViewModifiers/ActionBar.swift index d3c486c5..c5fc7e06 100644 --- a/Sources/BSWInterfaceKit/SwiftUI/ViewModifiers/ActionBar.swift +++ b/Sources/BSWInterfaceKit/SwiftUI/ViewModifiers/ActionBar.swift @@ -114,6 +114,7 @@ public extension View { } } +#if os(iOS) || os(macOS) private extension View { @ViewBuilder @@ -137,6 +138,7 @@ private extension View { } } } +#endif #if os(Android) #if SKIP