swiftswiftui

Disable TabView paging gesture while keeping ScrollView gesture


I am implementing an onboarding sequence in SwiftUI choreographed through a TabView, and want to only allow progression through the onboarding through buttons, so I need to disable any premature user gesturing through the entire thing. But, every method I find online to solve this problem also disables any scrollable components within the pages of the tabview, such as vertical scrollviews and other tabviews within the onboarding.

I am looking for a method to enable scrolling for components within the TabView while keeping the overarching Tabview disabled.

Here is the current implementation:

struct OnboardingView: View {
    @ObservedObject var vm = OnboardingVM()
    
    var body: some View {
        VStack {
            // ProgressIndicator(vm: vm)
            TabView(selection: $vm.currentScreenIndex) {
                ForEach(0..<vm.screenOrder.count, id:\.self) { index in
                    ScreenView(vm: vm, screenIndex: index)
                        .tag(index)
                }
            }.padding()
                .tabViewStyle(.page(indexDisplayMode: .never))
                .highPriorityGesture(DragGesture())
        }.environmentObject(vm)
            .background(Color.black)
    }
}

I expected the highPriorityGesture modifier to only alter the tabview indexing, but it also affects the sub-components.


Solution

  • Instead of using TabView, I suggest using a ScrollView instead. Then, disabling scrolling is as simple as .scrollDisabled(true). Use containerRelativeFrame to expand each onboarding screen to occupy the entirety of the scroll view container. Use .scrollPosition(id:) to control the scroll position programmatically.

    Here is an example:

    struct ContentView: View {
        @State private var index = 0
        
        var body: some View {
            ScrollView(.horizontal) {
                HStack(spacing: 0) {
    
                    // first page
                    List {
                        ForEach(0..<10) { _ in
                            Text("Some Content")
                        }
                    }
                    .containerRelativeFrame([.horizontal, .vertical])
                    .id(0)
                    
                    // second page
                    Text("Page 2")
                        .containerRelativeFrame([.horizontal, .vertical])
                        .background(.yellow)
                        .id(1)
                    
                    // third page
                    VStack {
                        Text("Page 3")
                        ScrollView {
                            ForEach(0..<10) { _ in
                                Text("Some Content")
                            }
                        }
                        .frame(width: 160, height: 160)
                    }
                    .containerRelativeFrame([.horizontal, .vertical])
                    .background(.green)
                    .id(2)
                }
            }
            .scrollPosition(id: Binding($index))
            .scrollDisabled(true)
            .containerRelativeFrame(.vertical)
            .animation(.default, value: index)
    
            // as an example, I have added the next and previous buttons as an overlay.
            // these can of course be in each of the onboarding screens instead. 
            .overlay {
                HStack {
                    Button("Previous") {
                        index -= 1
                    }
                    .disabled(index == 0)
                    Spacer()
                    Button("Next") {
                        index += 1
                    }
                    .disabled(index == 2)
                }
                .buttonStyle(.borderedProminent)
            }
        }
    }