1

Considering the simplest possible tab view app in SwiftUI:

struct ContentView: View {
    @State private var selection = 0
 
    var body: some View {
        TabView(selection: $selection){
            Text("First View")
                .font(.title)
                .tabItem {
                    VStack {
                        Image("first")
                        Text("First")
                    }
                }
                .tag(0)
            Text("Second View")
                .font(.title)
                .tabItem {
                    VStack {
                        Image("second")
                        Text("Second")
                    }
                }
                .tag(1)
        }
    }
}

Whenever a tab is tapped, the tab items are reloaded and you have the opportunity to change the image for a selected tab if you needed by comparing the selection with the tag. However, if you have a dynamic number of tabs in a variable and use a ForEach to display them, that doesn't work:

struct ContentView: View {
    @State private var selection = 0

    private var items: [AnyView] {
        return [
            AnyView(Text("First View")
                .font(.title)
                .tabItem {
                        VStack {
                            Image("first1")
                            Text("First1")
                        }
                }
                .tag(0)),
            AnyView(Text("Second View")
                .font(.title)
                .tabItem {
                    VStack {
                        Image("second")
                        Text("Second")
                    }
                }
                .tag(1))
        ]
    }
 
    var body: some View {
        TabView(selection: $selection){
            ForEach(0..<self.items.count) { index in
                self.items[index]
            }
        }
    }
}

The body of the ForEach is not called when the view reloads. Is there a way to accomplish changing an image while also using a ForEach in your TabView?

KerrM
  • 5,139
  • 3
  • 35
  • 60
  • 1
    It is like to drive a nail with microscope... use model where expected model and views where expected views and all will work. – Asperi Sep 02 '20 at 15:34
  • Thanks for your comment @Asperi. Unfortunately that's neither possible nor helpful. Anyway, I figured out how to do it: https://stackoverflow.com/a/63710886/1027644. – KerrM Sep 02 '20 at 17:40
  • @KerrM As a side note, using `AnyView` is not recommended (see [Static Types in SwiftUI](https://www.objc.io/blog/2019/11/05/static-types-in-swiftui/)). You may want to refactor your code using some of the solutions from [this question](https://stackoverflow.com/questions/56736466/alternative-to-switch-statement-in-swiftui-viewbuilder-block) – pawello2222 Sep 02 '20 at 17:59
  • Thanks, I understand the downsides of using `AnyView`. – KerrM Sep 02 '20 at 18:44

2 Answers2

1

Try the following:

struct ContentView: View {
    @State private var selection = 0
 
    var body: some View {
        TabView(selection: $selection){
            Text("First View")
                .font(.title)
                .tabItem {
                    VStack {
                        Image(systemName: selection == 0 ? "xmark" : "plus")
                        Text("First")
                    }
                }
                .tag(0)
            Text("Second View")
                .font(.title)
                .tabItem {
                    VStack {
                        Image(systemName: selection == 1 ? "xmark" : "minus")
                        Text("Second")
                    }
                }
                .tag(1)
        }
    }
}
pawello2222
  • 46,897
  • 22
  • 145
  • 209
  • Thanks for your answer, but you are not using a `ForEach` – KerrM Sep 02 '20 at 17:35
  • @KerrM I thought you took a hint from Asperi. Your solution is far from the cleanest. I recommend not to use `AnyView` whenever possible, especially `[AnyView]`. What you're trying to can probably be achieved in a much simpler way. Here I just gave you an example how to change the tabItem's image which you can easily adapt to your needs. – pawello2222 Sep 02 '20 at 17:54
  • Ah, sure thanks. That was just an over-simplified example. My question was specifically how to do it with ForEach. – KerrM Sep 02 '20 at 18:42
0

The answer is in the docs! When using a ForEach with a range:

    /// Creates an instance that computes views on demand over a *constant*
    /// range.
    ///
    /// This instance only reads the initial value of `data` and so it does not
    /// need to identify views across updates.
    ///
    /// To compute views on demand over a dynamic range use
    /// `ForEach(_:id:content:)`.

So to make this work, use ForEach(_:id:content:).

KerrM
  • 5,139
  • 3
  • 35
  • 60