Search code examples
iosswiftswiftuiswiftui-tabview

SwiftUI TabView is not working properly on Orientation Change


I created a simple tabView like this

struct ContentView: View {
    var body: some View {
        TabView{
            TabItemView(color: .red, title: "Page 1")
            TabItemView(color: .yellow, title: "Page 2")
            TabItemView(color: .green, title: "Page 3")
            TabItemView(color: .blue, title: "Page 4")
        }
        .tabViewStyle(.page)
        .background(Color.indigo)
    }
}

where

struct TabItemView: View {
    var color : Color
    var title : String
    var body: some View {
        Text("\(title)")
            .frame(width:UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
            .background(color)
        
    }
}

issue is when is switch to landscape entire thing is broken , like thisLandscape View

ie the tabs are jumped back to either some random position or position between two.

I searched online and some resources say its a bug thats not solved yet . Any solution for this?


Solution

  • Hi the bug is well known. Generally it helps when you put the tabview into inactive scrollview with certain frame modifier, like this:

    struct ContentView: View {
        var body: some View {
            ScrollView([])
                TabView{
                    TabItemView(color: .red, title: "Page 1")
                    TabItemView(color: .yellow, title: "Page 2")
                    TabItemView(color: .green, title: "Page 3")
                    TabItemView(color: .blue, title: "Page 4")
                }
                .tabViewStyle(.page)
                .background(Color.indigo)
            }
        }
        .frame(width: UIScreen.main.bounds.width)
    }
    

    This should fix the views in the middle in case of rotations. However, it might happen that when you use some array and iterate over it using ForEach inside the TabView, the elements are arbitrarily changed by TabView while rotating view. In that case it helps to keep tracking the current and previous orientation using states and build some logic onChange to prevent TabView to doing that. Like some wrapper adding layer between the binded state. Like:

    struct TabView: View {
        @State var pages: Int = 1
        var array: [Int] = [1,2,3,4,5]
    
        var body: some View {
            VStack {
                TabViewContent(selection: $pages, usage: .one) {
                    ForEach(array, id: \.self) { index in
                        Text("This is: \(pages) \(index)")
                        .tag(index)
                    }
                }
            }
        }
    }
    

    Wrapper with logic:

    struct TabViewContent<Selection: Hashable, Content: View>: View {
        @Binding var selection: Selection
        @State var selectionInternal: Selection
    
        @State var previousOrientation: UIDeviceOrientation = .unknown
        @State var currentOrientation: UIDeviceOrientation = .unknown
    
        @ViewBuilder var content: () -> Content
    
        internal init(selection: Binding<Selection>,     content: @escaping () -> Content) {
            self.content = content
            _selection = selection
            _selectionInternal = State(initialValue: selection.wrappedValue)
        }
    
        var body: some View {
            TabView(selection: $selection, content: content)
                .tabViewStyle(.page)
                .onChange(of: UIDevice.current.orientation) { newOrientation in
                    currentOrientation = newOrientation
                }
                .onChange(of: selectionInternal) { value in
                    if currentOrientation == previousOrientation {
                        selection = selectionInternal
                    }
    
                    previousOrientation = currentOrientation
                }
        }
    }