Search code examples
swiftuitabviewxcode12

TabView does not respect selection binding when displayed modally


Considering the below code, I would expect my TabView to show the randomly calculated image when my tab view is displayed, however this is not the case. It will always display the first image at index 0. If I hardcode the selection in the TabView initialiser (i.e. .constant(n)) then it will display the chosen image correctly. I have filed a radar (FB7844985) on this, but want to check that I am not missing something obvious?

struct ContentView: View {
    let assets: [String] = (1...32).map { "preview_\($0)"}
    
    @State private var selection: Int = 0
    @State private var isPresented: Bool = false
    var body: some View {
        VStack {
            Button("Random element") {
                selection = Int.random(in: 1...32)
                isPresented = true
            }
        }
        .sheet(isPresented: $isPresented) {
            SlideshowView(selection: $selection, assets: assets)
        }
    }
}

struct SlideshowView: View {
    @Binding var selection: Int
    var assets: [String]
    var body: some View {
        TabView(selection: $selection) {
            ForEach(assets.indices, id: \.self) { index in
                Image(assets[index])
                    .resizable()
                    .aspectRatio(contentMode: .fit)
            }
        }
        .tabViewStyle(PageTabViewStyle())
        .indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
    }
}

Solution

  • The reason is not in modality (ie showing in sheet), but in that TabView does not read initial (!) selection (and this is definitely a SwiftUI bug)

    Here is workaround (tested with Xcode12 / iOS 14 on replicated code)

    demo

    struct SlideshowView: View {
        @Binding var selection: Int
        var assets: [String]
        var body: some View {
            TabView(selection: $selection) {
                ForEach(assets.indices, id: \.self) { index in
                    Image(assets[index])
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                }
            }
            .tabViewStyle(PageTabViewStyle())
            .indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
            .onAppear {
                // WORKAROUND: simulate change of selection on appear !!
                let value = selection
                selection = -1
                DispatchQueue.main.async {
                    selection = value
                }
            }
        }
    }