0

Having more than 5 tabs in a TabView adds a More tab, which contains the tabs that could not be fit on the tab bar. In UIKit, UITabBarControllers also have this functionality and additionally have a built-in edit button as the right nav bar item, which would allow the user to change order. But it looks like SwiftUI's TabView does not have this. How do I enable it?

Here's some basic code to work with.

struct TabBarView: View {
    @State var tabs = Tabs.allCases
    
    var body: some View {
        TabView {
            ForEach(tabs) { tab in
                NavigationView {
                    tab.body.navigationTitle(tab.rawValue)
                }.tabItem {
                    Label(tab.rawValue, systemImage: tab.systemImage)
                }
            }
        }
    }
}

For reference, this is the Tabs enum I'm working with.

enum Tabs: String, Identifiable, CaseIterable {
    case songs = "Songs"
    case albums = "Albums"
    case artists = "Artists"
    case playlists = "Playlists"
    case genres = "Genres"
    case compilations = "Compilations"
    case composers = "Composers"
    
    var systemImage: String {
        switch self {
        case .songs: return "music.note"
        case .albums: return "square.stack"
        case .artists: return "music.mic"
        case .playlists: return "music.note.list"
        case .genres: return "guitars"
        case .compilations: return "person.2.crop.square.stack"
        case .composers: return "music.quarternote.3"
        }
    }
    
    @ViewBuilder var body: some View {
        switch self {
        case .songs: SongListView()
        case .albums: Text("Unimplemented")
        case .artists: Text("Unimplemented")
        case .playlists: PlaylistsView()
        case .genres: Text("Unimplemented")
        case .compilations: Text("Unimplemented")
        case .composers: Text("Unimplemented")
        }
    }
    
    var id: String { rawValue }
}

I've tried adding .moveDisabled(false) and having the TabView reference the tabs as a binding. Not sure what the intended use for either of those are in the context of a TabView.

struct TabBarView: View {
    @State var tabs = Tabs.allCases
    
    var body: some View {
        TabView(selection: $tabs) {
            ForEach(tabs) { tab in
                NavigationView {
                    tab.body.navigationTitle(tab.rawValue)
                }.tabItem {
                    Label(tab.rawValue, systemImage: tab.systemImage)
                }.moveDisabled(false)
            }
        } // .moveDisabled(false)
    }
}

What I'm seeing

What I want to see (what I see in UIKit by default)

I tried a bunch of Googling and couldn't find anyone talking about it. And I swear the Edit button was there when I did it a year or two ago.

How do I enable the Edit button on SwiftUI TabView's More menu?

Mr_Ripman
  • 1
  • 1

1 Answers1

0

I swear the Edit button was there when I did it a year or two ago.

Indeed, as this Medium article shows, the Edit button did exist. Nevertheless, the same article also says that the Edit button doesn't actually respect the new order of the tabs, and just resets itself back.

From the article, the solution they came up with is simply to wrap UITabController with a UIViewControllerRepresentable.

Quoting their code here:

fileprivate struct UITabBarControllerWrapper: UIViewControllerRepresentable {
    var viewControllers: [UIViewController]
    
    func makeUIViewController(context: UIViewControllerRepresentableContext<UITabBarControllerWrapper>) -> UITabBarController {
        let tabBar = UITabBarController()
        
        // Configure Tab Bar here, if needed
        
        return tabBar
    }
    
    func updateUIViewController(_ uiViewController: UITabBarController, context: UIViewControllerRepresentableContext<UITabBarControllerWrapper>) {
        uiViewController.setViewControllers(self.viewControllers, animated: true)
    }
    
    func makeCoordinator() -> Coordinator {
        return Coordinator(self)
    }
    
    class Coordinator: NSObject {
        var parent: UITabBarControllerWrapper
        
        init(_ controller: UITabBarControllerWrapper) {
            self.parent = controller
        }
    }
}

struct UITabBarWrapper: View {
    var controllers: [UIHostingController<TabBarElement>]
    
    @MainActor
    init(_ elements: [TabBarElement]) {
        self.controllers = elements.enumerated().map {
            let hostingController = UIHostingController(rootView: $1)
            
            hostingController.tabBarItem = UITabBarItem(
                title: $1.tabBarElementItem.title,
                image: UIImage.init(systemName: $1.tabBarElementItem.systemImageName),
                tag: $0 // 4
            )
            
            return hostingController
        }
    }
    
    var body: some View {
        UITabBarControllerWrapper(viewControllers: self.controllers)
    }
}

struct TabBarElementItem {
    var title: String
    var systemImageName: String
}

protocol TabBarElementView: View {
    associatedtype Content
    
    var content: Content { get set }
    var tabBarElementItem: TabBarElementItem { get set }
}

struct TabBarElement: TabBarElementView {
    internal var content: AnyView
    
    var tabBarElementItem: TabBarElementItem
    
    init<Content: View>(tabBarElementItem: TabBarElementItem,
         @ViewBuilder _ content: () -> Content) {
        self.tabBarElementItem = tabBarElementItem
        self.content = AnyView(content())
    }
    
    var body: some View { self.content }
}

In your case, you can use it like this:

var body: some View {
    UITabBarWrapper(Tabs.allCases.map { t in
        TabBarElement(tabBarElementItem: .init(title: t.rawValue, systemImageName: t.systemImage)) {
            NavigationView {
                t.body.navigationTitle(t.rawValue)
            }
        }
    })
    
}
Sweeper
  • 213,210
  • 22
  • 193
  • 313