Skip to content

Commit 0837ab7

Browse files
authored
Add badge support to TabView and refactor icon rendering (#428)
* Add badge support to TabView, move modifier Move BadgeModifier from List.swift into AdditionalViewModifiers.swift and keep its combined(for:) helper. Add badge support across TabView: import Badge/BadgedBox, add a badge property to TabContent, Tab, and TabSection, propagate badges to section children, and render tab icons inside a BadgedBox when a badge is present. Implement TabContent.badge(...) modifier overloads to set badge values instead of being unavailable, and apply the combined BadgeModifier when building tabs. Remove the duplicate BadgeModifier definition from List.swift. * Refactor Tab icon rendering and badge offset Rename iconContent to renderIcon and reformat Box initializer for clarity. Import Modifier.offset and apply a small offset (8.dp, -4.dp) to Badge so the badge is positioned correctly over tab icons. Ensure badge content is rendered with a white foregroundStyle for proper contrast. Minor cleanup of render calls to use renderIcon. * Mark .badge as supported in README Replace the detailed/partial-support row for `.badge` with a simple supported indicator (✅) in README.md. Removes the expandable details noting limited support on `List` and lack of `TabView` support, simplifying the support table entry.
1 parent f571d1e commit 0837ab7

4 files changed

Lines changed: 79 additions & 48 deletions

File tree

README.md

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1463,16 +1463,8 @@ Support levels:
14631463
<td><code>.backgroundStyle</code></td>
14641464
</tr>
14651465
<tr>
1466-
<td>🟡</td>
1467-
<td>
1468-
<details>
1469-
<summary><code>.badge</code></summary>
1470-
<ul>
1471-
<li>Supported on <code>List</code> items</li>
1472-
<li>Not yet supported on <code>TabView</code></li>
1473-
</ul>
1474-
</details>
1475-
</td>
1466+
<td>✅</td>
1467+
<td><code>.badge</code></td>
14761468
</tr>
14771469
<tr>
14781470
<td>🟢</td>

Sources/SkipUI/SkipUI/Containers/List.swift

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1407,30 +1407,6 @@ final class ListItemModifier: RenderModifier {
14071407
return ListItemModifier(background: background, separator: separator)
14081408
}
14091409
}
1410-
1411-
final class BadgeModifier: RenderModifier {
1412-
let badge: Text?
1413-
let prominence: BadgeProminence?
1414-
1415-
init(badge: Text? = nil, prominence: BadgeProminence? = nil) {
1416-
self.badge = badge
1417-
self.prominence = prominence
1418-
super.init()
1419-
}
1420-
1421-
static func combined(for renderable: Renderable) -> BadgeModifier {
1422-
var badge: Text? = nil
1423-
var prominence: BadgeProminence? = nil
1424-
renderable.forEachModifier {
1425-
if let badgeModifier = $0 as? BadgeModifier {
1426-
badge = badge ?? badgeModifier.badge
1427-
prominence = prominence ?? badgeModifier.prominence
1428-
}
1429-
return nil
1430-
}
1431-
return BadgeModifier(badge: badge, prominence: prominence)
1432-
}
1433-
}
14341410
#endif
14351411

14361412
#if false

Sources/SkipUI/SkipUI/Containers/TabView.swift

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
2020
import androidx.compose.foundation.layout.height
2121
import androidx.compose.foundation.layout.wrapContentWidth
2222
import androidx.compose.foundation.layout.ime
23+
import androidx.compose.foundation.layout.offset
2324
import androidx.compose.foundation.layout.padding
2425
import androidx.compose.foundation.layout.Spacer
2526
import androidx.compose.foundation.layout.safeDrawing
@@ -39,6 +40,8 @@ import androidx.compose.material3.NavigationBarItemDefaults
3940
import androidx.compose.material3.NavigationRail
4041
import androidx.compose.material3.NavigationRailItem
4142
import androidx.compose.material3.NavigationRailItemDefaults
43+
import androidx.compose.material3.Badge
44+
import androidx.compose.material3.BadgedBox
4245
import androidx.compose.material3.contentColorFor
4346
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
4447
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldDefaults
@@ -228,10 +231,16 @@ public struct TabView : View, Renderable {
228231
let tabRenderables = EvaluateContent(context: tabContext)
229232
let tabs: kotlin.collections.List<Tab?> = tabRenderables.map {
230233
let renderable = $0 as Renderable // Let transpiler understand type
231-
if let tab = renderable.strip() as? Tab {
234+
let badgeModifier = BadgeModifier.combined(for: renderable)
235+
if var tab = renderable.strip() as? Tab {
236+
if let badge = badgeModifier.badge {
237+
tab.badge = badge
238+
}
232239
return tab
233240
} else if let tabItemModifier = renderable.forEachModifier(perform: { $0 as? TabItemModifier }) as? TabItemModifier {
234-
return Tab(content: { renderable.asView() }, label: { tabItemModifier.label })
241+
var tab = Tab(content: { renderable.asView() }, label: { tabItemModifier.label })
242+
tab.badge = badgeModifier.badge
243+
return tab
235244
} else {
236245
return nil
237246
}
@@ -743,6 +752,7 @@ public enum AdaptableTabBarPlacement : Hashable {
743752
public protocol TabContent : View {
744753
var isHidden: Bool { get set }
745754
var isDisabled: Bool { get set }
755+
var badge: Text? { get set }
746756
}
747757

748758
// SKIP @bridge
@@ -850,6 +860,7 @@ public struct Tab : TabContent, Renderable {
850860

851861
public var isHidden = false
852862
public var isDisabled = false
863+
public var badge: Text? = nil
853864

854865
#if SKIP
855866
@Composable override func Render(context: ComposeContext) {
@@ -888,8 +899,11 @@ public struct Tab : TabContent, Renderable {
888899
renderable.Render(context: context)
889900
}
890901
}
891-
Box(modifier: outerModifier, contentAlignment: androidx.compose.ui.Alignment.Center) {
892-
Box(modifier: Modifier.graphicsLayer(scaleX: Float(1.5), scaleY: Float(1.5)), contentAlignment: androidx.compose.ui.Alignment.Center) {
902+
let renderIcon: (@Composable () -> ()) = {
903+
Box(
904+
modifier: Modifier.graphicsLayer(scaleX: Float(1.5), scaleY: Float(1.5)),
905+
contentAlignment: androidx.compose.ui.Alignment.Center
906+
) {
893907
// Default to a lighter symbol weight so tab icons approximate SwiftUI sizing
894908
if EnvironmentValues.shared._textEnvironment.fontWeight == nil {
895909
EnvironmentValues.shared.setValues {
@@ -905,6 +919,22 @@ public struct Tab : TabContent, Renderable {
905919
}
906920
}
907921
}
922+
923+
Box(modifier: outerModifier, contentAlignment: androidx.compose.ui.Alignment.Center) {
924+
if let badge {
925+
BadgedBox(
926+
badge: {
927+
Badge(modifier: Modifier.offset(x: 8.dp, y: -4.dp)) {
928+
badge.foregroundStyle(Color.white).Render(context: context)
929+
}
930+
}
931+
) {
932+
renderIcon()
933+
}
934+
} else {
935+
renderIcon()
936+
}
937+
}
908938
}
909939
#else
910940
public var body: some View {
@@ -960,6 +990,7 @@ public struct TabSection : TabContent, Renderable {
960990

961991
public var isHidden = false
962992
public var isDisabled = false
993+
public var badge: Text? = nil
963994

964995
#if SKIP
965996
@Composable override func Evaluate(context: ComposeContext, options: Int) -> kotlin.collections.List<Renderable> {
@@ -971,6 +1002,9 @@ public struct TabSection : TabContent, Renderable {
9711002
if isDisabled {
9721003
(renderable as? TabContent)?.isDisabled = true
9731004
}
1005+
if let badge {
1006+
(renderable as? TabContent)?.badge = badge
1007+
}
9741008
}
9751009
return renderables
9761010
}
@@ -1078,29 +1112,34 @@ extension TabContent {
10781112
return self
10791113
}
10801114

1081-
@available(*, unavailable)
10821115
public func badge(_ count: Int) -> some TabContent {
1083-
return self
1116+
var tabContent = self
1117+
tabContent.badge = count > 0 ? Text(String(count)) : nil
1118+
return tabContent
10841119
}
10851120

1086-
@available(*, unavailable)
10871121
public func badge(_ label: Text?) -> some TabContent {
1088-
return self
1122+
var tabContent = self
1123+
tabContent.badge = label
1124+
return tabContent
10891125
}
10901126

1091-
@available(*, unavailable)
10921127
public func badge(_ key: LocalizedStringKey) -> some TabContent {
1093-
return self
1128+
var tabContent = self
1129+
tabContent.badge = Text(key)
1130+
return tabContent
10941131
}
10951132

1096-
@available(*, unavailable)
10971133
public func badge(_ resource: LocalizedStringResource) -> some TabContent {
1098-
return self
1134+
var tabContent = self
1135+
tabContent.badge = Text(resource)
1136+
return tabContent
10991137
}
11001138

1101-
@available(*, unavailable)
11021139
public func badge(_ label: String) -> some TabContent {
1103-
return self
1140+
var tabContent = self
1141+
tabContent.badge = Text(label)
1142+
return tabContent
11041143
}
11051144

11061145
@available(*, unavailable)

Sources/SkipUI/SkipUI/View/AdditionalViewModifiers.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1487,6 +1487,30 @@ final class AspectRatioModifier: RenderModifier {
14871487
}
14881488
}
14891489

1490+
final class BadgeModifier: RenderModifier {
1491+
let badge: Text?
1492+
let prominence: BadgeProminence?
1493+
1494+
init(badge: Text? = nil, prominence: BadgeProminence? = nil) {
1495+
self.badge = badge
1496+
self.prominence = prominence
1497+
super.init()
1498+
}
1499+
1500+
static func combined(for renderable: Renderable) -> BadgeModifier {
1501+
var badge: Text? = nil
1502+
var prominence: BadgeProminence? = nil
1503+
renderable.forEachModifier {
1504+
if let badgeModifier = $0 as? BadgeModifier {
1505+
badge = badge ?? badgeModifier.badge
1506+
prominence = prominence ?? badgeModifier.prominence
1507+
}
1508+
return nil
1509+
}
1510+
return BadgeModifier(badge: badge, prominence: prominence)
1511+
}
1512+
}
1513+
14901514
final class DisabledModifier: EnvironmentModifier {
14911515
let disabled: Bool
14921516

0 commit comments

Comments
 (0)