Swift UI: TabView Inside scroll view does not work

560 Views Asked by At

I want to put a tabView inside a scroll View but when I do so the content of the tabview does not show up even though it is there. How can I fix this bug? The update uses a child size reader as suggested by the comment. Please reference the other so thread for the code for child size reader.

The size of the content in the tabview is dynamic. I know I added 800 for the frame height for each item but this can change.

import SwiftUI

struct SwiftUIView: View {
    @State var selectedFilter = 1
    var body: some View {
        VStack {
            Color.blue.frame(height: 85) //header
            ScrollView {
                LazyVStack(spacing: 0, pinnedViews: [.sectionHeaders]) {
                    
                    
                    Color.green.frame(height: 100) //view body
                    
                    
                    //red color is tabview header
                    Section(header: Color.red.frame(height: 30)) {
                        //view content (THIS DOESNT SHOW)
                        TabView(selection: $selectedFilter) {
                            Color.yellow.frame(height: 800).tag(1)
                            Color.black.frame(height: 800).tag(2)
                            Color.indigo.frame(height: 800).tag(3)
                        }
                    }
                }
            }
        }
    }
}

#Preview {
    SwiftUIView()
}

Update for Answer

struct SwiftUIView: View {
    @State var selectedFilter = 1
    var body: some View {
        VStack {
            Color.blue.frame(height: 85) //header
            ScrollView {
                LazyVStack(spacing: 0, pinnedViews: [.sectionHeaders]) {
                    
                    
                    Color.green.frame(height: 100) //view body
                    
                    
                    //red color is tabview header
                    Section(header: Color.red.frame(height: 30)) {
                        //view content (THIS DOESNT SHOW)
                        VStack {
                            ZStack {
                                if selectedFilter == 1 {
                                    Color.yellow.frame(height: 5000)
                                } else if selectedFilter == 2 {
                                    Color.green.frame(height: 100)
                                }
                                HStack {
                                    Button("Tab 1") { selectedFilter = 1 }
                                    
                                    Button("Tab 2") { selectedFilter = 2 }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
1

There are 1 best solutions below

3
VonC On BEST ANSWER

You want to place a TabView inside a ScrollView in SwiftUI:

+----------------------------------------+
|                                        |
|                 Header                 |
|                                        |
+----------------------------------------+
|                                        |
|              ScrollView                |
|                                        |
|    +-------------------------------+   |
|    |                               |   |
|    |          LazyVStack           |   |
|    |                               |   |
|    +-------------------------------+   |
|    |            TabView            |   |
|    | (Content not displaying)      |   |
|    +-------------------------------+   |
|                                        |
+----------------------------------------+

Instead of using a fixed frame size, which does not accommodate dynamic content, a GeometryReader can be used to adapt to the content's size. However, you have mentioned that using a GeometryReader directly did not work as expected.
You have updated the code to use a ChildSizeReader, but it still does not display the content.

Consider the design implications of having a TabView inside a ScrollView. If the content of the TabView is also scrollable, this can lead to a confusing user experience. It is generally advisable to keep scrolling contexts separate.

Yet, as suggested in the comments, creating a custom component that mimics the behavior of TabView might be more effective.
That can include a horizontal row of buttons for tab selection and a ZStack to display the content of the selected tab. That approach offers more control and can be adapted to work within a ScrollView.

You would get:

+----------------------------------------+
|                                        |
|                 Header                 |
|                                        |
+----------------------------------------+
|                                        |
|              ScrollView                |
|                                        |
|    +-------------------------------+   |
|    |                               |   |
|    |          LazyVStack           |   |
|    |                               |   |
|    +-------------------------------+   |
|    |        Custom Tab View        |   |
|    | (With dynamic size handling)  |   |
|    +-------------------------------+   |
|                                        |
+----------------------------------------+

As an example:

struct CustomTabView: View {
    @State var selectedTab = 1
    var body: some View {
        VStack {
            // Tab selector
            HStack {
                Button("Tab 1") { selectedTab = 1 }
                Button("Tab 2") { selectedTab = 2 }
                // Add more tabs as needed
            }

            // Tab content
            ZStack {
                if selectedTab == 1 {
                    Color.yellow // Replace with actual content
                } else if selectedTab == 2 {
                    Color.green // Replace with actual content
                }
                // Add more content views as needed
            }
        }
    }
}

This was my original approach but there's a flaw in it.
Lets say the yellow 'tab' is height 5000 and the green 'tab' is height 50. If I scroll down the yellow tab (offset > 50) and I try to switch to the green tab, then the entire view crashes and nothing is shown.

That's why I wanted to use a default tabview, because the scroll position of each tab is independent of the other tabs.
I tried use a scroll view reader and scrolling to the very top before switching tabs, this is very buggy however and it fails sometimes.

So the core issue is maintaining the scroll position for each tab independently in a custom tab view implementation.
You might consider a combination of ScrollViewReader and GeometryReader.

  • Store the scroll offset for each tab independently. That way, you can restore the correct scroll position when switching tabs.
  • ScrollViewReader can be used to scroll to a specific position or element, ensuring the correct scroll position is set when switching tabs.
  • Before switching tabs, use the ScrollViewReader to scroll to the top (or the respective stored offset for the new tab).

That would be:

struct SwiftUIView: View {
    @State var selectedFilter = 1
    @State private var scrollOffsets = [1: 0.0, 2: 0.0] // Stores scroll offsets for each tab

    var body: some View {
        VStack {
            Color.blue.frame(height: 85) // Header
            ScrollViewReader { scrollViewProxy in
                ScrollView {
                    LazyVStack(spacing: 0, pinnedViews: [.sectionHeaders]) {
                        Color.green.frame(height: 100) // View body

                        Section(header: Color.red.frame(height: 30)) {
                            VStack {
                                ZStack {
                                    if selectedFilter == 1 {
                                        GeometryReader { geometry in
                                            Color.yellow.frame(height: 5000)
                                                .onAppear {
                                                    scrollOffsets[1] = geometry.frame(in: .global).minY
                                                }
                                        }
                                    } else if selectedFilter == 2 {
                                        GeometryReader { geometry in
                                            Color.green.frame(height: 50)
                                                .onAppear {
                                                    scrollOffsets[2] = geometry.frame(in: .global).minY
                                                }
                                        }
                                    }
                                    HStack {
                                        Button("Tab 1") {
                                            switchToTab(1, with: scrollViewProxy)
                                        }
                                        Button("Tab 2") {
                                            switchToTab(2, with: scrollViewProxy)
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    private func switchToTab(_ tab: Int, with scrollViewProxy: ScrollViewProxy) {
        selectedFilter = tab
        let offset = scrollOffsets[tab] ?? 0.0
        scrollViewProxy.scrollTo(offset, animated: true)
    }
}

Such a code tries to maintain individual scroll positions for each tab: it uses GeometryReader to dynamically track and update the scroll position for each tab. And ScrollViewReader to programmatically control the scroll position when switching tabs.

It should help in managing the scroll behavior for each tab independently, preventing the view from crashing when switching between tabs with different content sizes.