Skip to content

Commit 5713a41

Browse files
authored
Merge pull request #455 from theleftbit/new-bottom-action-bar
feat: add cross-platform BottomActionBar and TopActionBar components
2 parents e0891c3 + 087efe1 commit 5713a41

1 file changed

Lines changed: 217 additions & 0 deletions

File tree

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
#if os(Android)
2+
import SkipFuseUI
3+
#else
4+
import SwiftUI
5+
#endif
6+
7+
#if os(Android)
8+
#if SKIP
9+
import androidx.compose.foundation.layout.Box
10+
import androidx.compose.foundation.layout.Column
11+
import androidx.compose.foundation.layout.WindowInsets
12+
import androidx.compose.foundation.layout.fillMaxSize
13+
import androidx.compose.foundation.layout.fillMaxWidth
14+
import androidx.compose.foundation.layout.imePadding
15+
import androidx.compose.foundation.layout.padding
16+
import androidx.compose.material3.Divider
17+
import androidx.compose.material3.MaterialTheme
18+
import androidx.compose.material3.Scaffold
19+
import androidx.compose.material3.Surface
20+
import androidx.compose.runtime.Composable
21+
import androidx.compose.ui.Modifier
22+
#endif
23+
#endif
24+
25+
#if canImport(Darwin)
26+
27+
#Preview("Top Button") {
28+
NavigationStack {
29+
let fruits = [
30+
"Apple", "Banana", "Cherry", "Date", "Elderberry",
31+
"Fig", "Grape", "Honeydew", "Kiwi", "Lemon",
32+
"Mango", "Nectarine", "Orange", "Papaya", "Quince",
33+
"Raspberry", "Strawberry", "Tangerine", "Ugli Fruit", "Watermelon"
34+
]
35+
List(fruits, id: \.self, rowContent: Text.init)
36+
.topActionBar {
37+
AsyncButton(
38+
action: { try await Task.sleep(for: .seconds(1)) },
39+
label: {
40+
HStack { Spacer(); Text("Hello"); Spacer() }
41+
}
42+
)
43+
}
44+
}
45+
}
46+
47+
#Preview("Bottom Button") {
48+
NavigationStack {
49+
let fruits = [
50+
"Apple", "Banana", "Cherry", "Date", "Elderberry",
51+
"Fig", "Grape", "Honeydew", "Kiwi", "Lemon",
52+
"Mango", "Nectarine", "Orange", "Papaya", "Quince",
53+
"Raspberry", "Strawberry", "Tangerine", "Ugli Fruit", "Watermelon"
54+
]
55+
List(fruits, id: \.self, rowContent: Text.init)
56+
.bottomActionBar {
57+
AsyncButton(
58+
action: { try await Task.sleep(for: .seconds(1)) },
59+
label: {
60+
HStack { Spacer(); Text("Hello"); Spacer() }
61+
}
62+
)
63+
}
64+
}
65+
}
66+
#endif
67+
68+
public extension View {
69+
70+
@ViewBuilder
71+
func bottomActionBar<T: View>(
72+
@ViewBuilder button: () -> T
73+
) -> some View {
74+
let actionButton = button()
75+
76+
#if os(Android)
77+
ComposeView {
78+
AndroidActionBarScaffold(
79+
content: self,
80+
actionBar: actionButton,
81+
isTopBar: false
82+
)
83+
}
84+
#else
85+
self.actionBarInset(
86+
actionButton,
87+
edge: .bottom,
88+
dividerAlignment: .top
89+
)
90+
#endif
91+
}
92+
93+
@ViewBuilder
94+
func topActionBar<T: View>(
95+
@ViewBuilder topBar: () -> T
96+
) -> some View {
97+
let actionTopBar = topBar()
98+
99+
#if os(Android)
100+
ComposeView {
101+
AndroidActionBarScaffold(
102+
content: self,
103+
actionBar: actionTopBar,
104+
isTopBar: true
105+
)
106+
}
107+
#else
108+
self.actionBarInset(
109+
actionTopBar,
110+
edge: .top,
111+
dividerAlignment: .bottom
112+
)
113+
#endif
114+
}
115+
}
116+
117+
#if os(iOS) || os(macOS)
118+
private extension View {
119+
120+
@ViewBuilder
121+
func actionBarInset<T: View>(
122+
_ actionBar: T,
123+
edge: VerticalEdge,
124+
dividerAlignment: Alignment
125+
) -> some View {
126+
if #available(iOS 26.0, macOS 26.0, *) {
127+
self.safeAreaBar(edge: edge) {
128+
actionBar
129+
}
130+
} else {
131+
self.safeAreaInset(edge: edge) {
132+
actionBar
133+
.background(.regularMaterial)
134+
.overlay(alignment: dividerAlignment) {
135+
Divider()
136+
}
137+
}
138+
}
139+
}
140+
}
141+
#endif
142+
143+
#if os(Android)
144+
#if SKIP
145+
146+
struct AndroidActionBarScaffold: ContentComposer {
147+
typealias BridgedView = any View
148+
let content: BridgedView
149+
let actionBar: BridgedView
150+
let isTopBar: Bool
151+
152+
@Composable
153+
func Compose(context: ComposeContext) {
154+
ComposeContainer(modifier: context.modifier, fillWidth: true, fillHeight: true) { modifier in
155+
Scaffold(
156+
modifier: modifier.fillMaxSize(),
157+
contentWindowInsets: WindowInsets(0),
158+
topBar: {
159+
if isTopBar {
160+
Surface(color: MaterialTheme.colorScheme.surface) {
161+
Column(
162+
modifier: Modifier
163+
.fillMaxWidth()
164+
) {
165+
actionBar.Compose(
166+
context: context.content(
167+
modifier: Modifier.fillMaxWidth()
168+
)
169+
)
170+
Divider()
171+
}
172+
}
173+
}
174+
},
175+
bottomBar: {
176+
if !isTopBar {
177+
Surface(color: MaterialTheme.colorScheme.surface) {
178+
Box(
179+
modifier: Modifier
180+
.fillMaxWidth()
181+
.imePadding()
182+
) {
183+
Divider()
184+
Box(
185+
modifier: Modifier
186+
.fillMaxWidth()
187+
) {
188+
actionBar.Compose(
189+
context: context.content(
190+
modifier: Modifier.fillMaxWidth()
191+
)
192+
)
193+
}
194+
}
195+
}
196+
}
197+
},
198+
content: { innerPadding in
199+
Box(
200+
modifier: Modifier
201+
.fillMaxSize()
202+
.padding(innerPadding)
203+
) {
204+
content.Compose(
205+
context: context.content(
206+
modifier: Modifier.fillMaxSize()
207+
)
208+
)
209+
}
210+
}
211+
)
212+
}
213+
}
214+
}
215+
216+
#endif
217+
#endif

0 commit comments

Comments
 (0)