SwiftUI - How to setup a .tag for .tabItems build using ForEach

81 views Asked by At

In the following sample code, hope its not to long, I have difficulties to get it right. At first I did want to use a enum with associated values and actions, but choose a way that I did understand a little better.

In my current code, the .tag's are giving me a little headache, I did try several methods to get it right but to no a veal. The errors it shows is "No exact matches in reference to static method 'buildExpression'" on the line '.tag(item.tag)'and '.tabItem' line.

import SwiftUI

class SettingsTabItemManager: ObservableObject {
    @Published var tabItems: [SettingsTabItem] = []

    init() {
        createTabItems()
    }

    func createTabItems() {
        self.tabItems = [
             SettingsTabItem(
                label: "General",
                labelImage: "gear", 
                tag: "general", 
                action: {}, 
                view: GeneralSettingsView(), 
                description: "The general settings tabItem"),
                         
             SettingsTabItem(
                label: "Network", 
                labelImage: "network", 
                tag: "network", 
                action: {}, 
                view: NetworkSettingsView(), 
                description: "The network settings tabItem"),
                         
             SettingsTabItem(
                label: "Advanced", 
                labelImage: "star", 
                tag: "advanced", 
                action: {}, 
                view: AdvancedSettingsView(), 
                description: "The advanced settings tabItem")
        ]
    }
}

struct SettingsTabItem {
    var id : UUID = UUID()
    var label: String
    var labelImage: String
    var tag: String // or enum Tabs
    var action: () -> Void
    var view: any View
    var description: String
}

struct SettingsView: View {
    @StateObject private var tabItemManager = SettingsTabItemManager()
    @State private var selectedTab: String?

    init() {
        _selectedTab = State(initialValue: tabItemManager.tabItems.first?.tag)
    }


    @State var tapped = false

    var body: some View {
        TabView {
            Group {
                ForEach(tabItemManager.tabItems, id: \.id) { item in
                    item.view
                        .tabItem {
                            Label(item.label, systemImage: item.labelImage)
                        }
                        .tag(item.tag) // I did try also ( .tag(item.tag.hashValue) )
                }
                .onTapGesture {
                    tapped.toggle()
                    //  more is coming
                }
            }
            .padding(20)
            .frame(width: 375, height: 150)
        }
    }
}

struct GeneralSettingsView: View {
    var body: some View {
        Text("GeneralSettingsView")
    }
}

struct NetworkSettingsView: View {
    var body: some View {
        Text("NetworkSettingsView")
    }
}

struct AdvancedSettingsView: View {
    var body: some View {
        Text("AdvancedSettingsView")
    }
}

What do I overlook here? Maybe I should do it different but this is what I came up with. Maybe I should use some sort of viewbuilder, but don't know that how to. Thanks in advance.

1

There are 1 answers

0
Sweeper On

You should not have a View in your SettingsTabItem.

var view: any View // remove this!

I assume you want to be able to change the tabs in the tab view dynamically, and that's why you created the SettingsTabItem struct. So the array of SettingsTabItems represents state, and views in SwiftUI are a function of state.

You should write a function that, given a SettingsTabItem, return some View. For example, in the simple case you showed here:

@ViewBuilder
func tabView(forTabItem tabItem: SettingsTabItem) -> some View {
    switch tabItem.tag { // also consider using an enum for the tag
    case "general":
        GeneralSettingsView()
    case "network":
        NetworkSettingsView()
    case "advanced":
        AdvancedSettingsView()
    default:
        EmptyView()
    }
}

You can expand this function to do whatever things you need to do, to figure out what the View corresponding to a given SettingsTabItem should be. You might need to add more properties to SettingsTabItem in your real scenario.

Notice that I avoided the problem of the views all having different types by using @ViewBuilder, and this could only be done if the view is written as a function of state. You tried to use any View to solve this problem, and that caused the error.

Then you can just call this function in ForEach:

tabView(forTabItem: item)
    .tabItem {
        Label(item.label, systemImage: item.labelImage)
    }
    .tag(item.tag)

Side note: The way you initialise selectedTab is an an anti pattern. (See also the many answers here) I would suggest initialising selectedTab to some dummy value first, then assign it the first tab's tag in onAppear, and/or every time the tab items do not contain an item with the tag selectedTab (you can detect this with onChange).