Tabview cannot update selection when selection is optional?

196 views Asked by At

I have the following code snippet. The 'selectedTab' in 'FirstTabView' updates automatically when I swipe between tabs. But if I change 'selectedTab' to be an optional value as shown in 'SecondTabView', then the 'selectedTab' does not update and always show 2. Could someone help explain it?

struct FirstTabView: View {
    @State private var selectedTab: Int = 2

    var body: some View {
        VStack {
            TabView(selection: $selectedTab) {
                Text("Tab 1")
                    .tag(1)
                Text("Tab 2")
                    .tag(2)
                Text("Tab 3")
                    .tag(3)
            }
            .tabViewStyle(PageTabViewStyle())
            Text(String(selectedTab))
        }
    }
}

struct SecondTabView: View {
    @State private var selectedTab: Int?

    var body: some View {
        VStack {
            TabView(selection: $selectedTab) {
                Text("Tab 1")
                    .tag(1)
                Text("Tab 2")
                    .tag(2)
                Text("Tab 3")
                    .tag(3)
            }
            .tabViewStyle(PageTabViewStyle())
            Text(String(selectedTab ?? 2))
        }
    }
}

I create a new non-optional variable to bind the selection and then the 'selectedTab' can update automatically again. But I checked the SwiftUI Documentation and the tabview does accept an optional value as selection parameter.

struct SecondTabView: View {
    @State private var selectedTab: Int?
    
    var tabBinding: Binding<Int> {
        Binding(
            get: { selectedTab ?? 2 },
            set: { selectedTab = $0 }
        )
    }

    var body: some View {
        VStack {
            TabView(selection: $tabBinding) {
                Text("Tab 1")
                    .tag(1)
                Text("Tab 2")
                    .tag(2)
                Text("Tab 3")
                    .tag(3)
            }
            .tabViewStyle(PageTabViewStyle())
            Text(String(selectedTab ?? 2))
        }
    }
}
1

There are 1 answers

1
Sweeper On BEST ANSWER

The type of the selection must exactly match the type of the tags you give to the tabs.

You are giving Int tags to the tabs, but the selection's type is Int?, so the selection value is not updated. There is no Int? value SwiftUI can set, such that it is equal to (by using the == operator required by Equatable) the non-nullable Int "1". After all, Equatable can only compare things of the same type.

Note that although you can normally compare Int and Int? because as a language feature, you are allowed to write the non-optional value where an optional is expected. The internals of SwiftUI doesn't have this "feature".

If you want to use an optional type as the selection value, make the tags optional too:

Text("Tab 1")
    .tag(1 as Int?) // also can be written as Optional.some(1)
Text("Tab 2")
    .tag(2 as Int?)
Text("Tab 3")
    .tag(3 as Int?)

That said, the initial value of selectedTab would be nil, and there is no tabs with the tag nil, so (at least in my testing) the first tab is initially selected (but does not change the selection value to 1).

So I'm not sure why you would use an optional selection value in this case. In fact, I can't think of any case where it would be useful. If you want to have a "default" tab, just initialise selectedTab to one of the tags.

But I checked the SwiftUI Documentation and the tabview does accept an optional value as selection parameter.

You have probably found this:

init(
    selection: Binding<SelectionValue>?,
    @ViewBuilder content: () -> Content
)

Binding<SelectionValue>? is not a binding of an optional value, which would be written Binding<SelectionValue?>. It is a binding that is optional, i.e. it allows you to write:

TabView(selection: nil) { ... }

So when you pass your $selectedTab, which is a Binding<Int?>, the tab view's SelectionValue is Int?, and it looks for tags of type Int?.