Julian D.
Julian D.

Reputation: 341

Set TabBar Item badge count with SwiftUI

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

Answers (6)

Phil Dukhov
Phil Dukhov

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

Hikaru
Hikaru

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

Luca
Luca

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

PRAHALAD KUMAWAT
PRAHALAD KUMAWAT

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

Stephen Lee
Stephen Lee

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

enter image description here

Upvotes: 1

Trai Nguyen
Trai Nguyen

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

Related Questions