Reputation: 341
Is it possible to show TabItem badge with SwiftUI?
It is easy to achieve with UIKit like described here ->
How to set badge value in Tab bar?
I didn't find a way to do this with a SwiftUI. The only possible way is to access to UITabBarController using scene rootViewController and modify its tab bar items directly.
func setBadgeCount(_ count: Int) {
UIApplication.shared.applicationIconBadgeNumber = count
guard let delegate = app.connectedScenes.first?.delegate as? SceneDelegate else {
return
}
if let tabBarController = delegate.window?.rootViewController?.children.first {
tabBarController.viewControllers?.first?.tabBarItem.badgeValue = "\(count)"
}
}
Any ideas how to do this with native SwiftUI approach?
Upvotes: 6
Views: 4911
Reputation: 87804
iOS 15 added support for .badge
modifier, but as I need to support iOS 14, I've created UITabBarController
wrapper:
struct TabBarController<TabContent: View, Tab: Hashable>: UIViewControllerRepresentable {
let tabs: [Tab]
@Binding
var selection: Tab
let tabBarItem: (Tab) -> UITabBarItem
let badgeValue: (Tab) -> String?
@ViewBuilder
let contentView: (Tab) -> TabContent
func makeUIViewController(context: Context) -> UITabBarController {
let controller = UITabBarController()
controller.delegate = context.coordinator
return controller
}
func updateUIViewController(_ uiViewController: UITabBarController, context: Context) {
context.coordinator.viewControllers
.keys
.filterNot(tabs.contains(_:))
.forEach { removedKey in
context.coordinator.viewControllers.removeValue(forKey: removedKey)
}
uiViewController.viewControllers = tabs.map { tab in
let rootView = contentView(tab)
let viewController = context.coordinator.viewControllers[tab] ?? {
let viewController = UIHostingController(rootView: rootView)
viewController.tabBarItem = tabBarItem(tab)
context.coordinator.viewControllers[tab] = viewController
return viewController
}()
viewController.rootView = rootView
viewController.tabBarItem.badgeValue = badgeValue(tab)
return viewController
}
uiViewController.selectedIndex = tabs.firstIndex(of: selection) ?? 0
}
func makeCoordinator() -> Coordinator {
Coordinator(selection: $selection)
}
final class Coordinator: NSObject, UITabBarControllerDelegate {
@Binding
private var selection: Tab
var viewControllers = [Tab: UIHostingController<TabContent>]()
init(selection: Binding<Tab>) {
_selection = selection
}
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
guard let newTab = viewControllers.first(where: { $0.value == viewController })?.key else {
print("tabBarController:didSelect: unexpected")
return
}
selection = newTab
}
}
}
filterNot
:
extension Collection {
@inlinable public func filterNot(_ isNotIncluded: (Element) throws -> Bool) rethrows -> [Element] {
try filter { try !isNotIncluded($0) }
}
}
Usage:
TabBarController(
tabs: [1,2,3],
selection: $selection,
tabBarItem: { tab in
UITabBarItem(title: "tab \(tab)", image: UIImage(systemName: "1.square.fill"), tag: 0)
},
badgeValue: { tab in
"\(tab)"
},
contentView: { tab in
Text("\(tab) screen")
}
).ignoresSafeArea()
Upvotes: 0
Reputation: 11
The above mentioned .introspectTabBarController modifier worked for me. Gave a nice native badge on tabItem. Looks great on both orientations.
.introspectTabBarController { (UITabBarController) in
self.tabBarControl = UITabBarController
if let items = UITabBarController.tabBar.items {
let tabItem = items[2] // in my case it was 3rd item
tabItem.badgeValue = "5" // hardcoded
}
}
Though, when changing tabs, the badge disappears so I saved UITabBarController in @State and when tab changes I set tabItem.badgeValue again.
P.S: iOS 15+ supports .badge modifier. Use it if you're targeting above or iOS 15.
Upvotes: 0
Reputation: 984
Now in SwiftUI 3 they added a .badge()
modifier
Source: HackingWithSwift
TabView {
Text("Your home screen here")
.tabItem {
Label("Home", systemImage: "house")
}
.badge(5)
}
Upvotes: 6
Reputation: 1
struct ContentView: View {
var body: some View {
TabView {
Text("Home")
.tabItem {
Text("Home")
}
Text("Home")
.tabItem {
Text("Home")
}
Text("Home")
.tabItem {
Text("Home")
}
}
.introspectTabBarController { (tabbarController) in
if let items = tabbarController.tabBar.items {
let tabItem = items[2]
tabItem.badgeValue = "1"
}
}
}
}
Upvotes: -1
Reputation: 327
func calculateBadgeXPos(width: CGFloat) -> CGFloat {
let t = (2*CGFloat(self.selectedTab))+1
return CGFloat(t * width/(2*CGFloat(TABS_COUNT)))
}
then use it here:
GeometryReader { geometry in
ZStack(alignment: .bottomLeading) {
// make sure to update TABS_COUNT
TabView(selection: self.$selectedTab) {
...
}
NotificationBadge(...)
.offset(x: self.calculateBadgeXPos(width: geometry.size.width), y: -28)
}
}
Looks sth like this on Preview
Upvotes: 1
Reputation: 839
Currently, SwiftUI don't have badge feature so we must custom.
Reference HERE I create My tabar with badge
struct ContentView: View {
private var badgePosition: CGFloat = 2
private var tabsCount: CGFloat = 2
@State var selectedView = 0
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .bottomLeading) {
TabView {
Text("First View")
.tabItem {
Image(systemName: "list.dash")
Text("First")
}.tag(0)
Text("Second View")
.tabItem {
Image(systemName: "star")
Text("Second")
}.tag(1)
}
ZStack {
Circle()
.foregroundColor(.red)
Text("3")
.foregroundColor(.white)
.font(Font.system(size: 12))
}
.frame(width: 15, height: 15)
.offset(x: ( ( 2 * self.badgePosition) - 0.95 ) * ( geometry.size.width / ( 2 * self.tabsCount ) ) + 2, y: -30)
.opacity(1.0)
}
}
}
}
Upvotes: 7