19

I have a TabView thats using the swiftUI 2.0 PageTabViewStyle. Is there any way to disable the swipe to change pages?

I have a search bar in my first tab view, but if a user is typing, I don't want to give the ability to change they are on, I basically want it to be locked on to that screen until said function is done.

Here's a gif showing the difference, I'm looking to disable tab changing when it's full screen in the gif. https://i.stack.imgur.com/20qnW.jpg

daee kang
  • 285
  • 1
  • 3
  • 9

6 Answers6

18

Try something like the following (tested with some stub code). The idea is to block tab view drag gesture when some condition (in you case start editing) happens

@State var isSearching = false

// ... other code

TabView {
    // ... your code here

    Your_View()
       .gesture(isSearching ? DragGesture() : nil)  // blocks TabView gesture !!
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .always))
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • This partially works. It only stops the user from trying to drag starting from visible elements on the display (they can still drag from the other areas) and doesn't disable the index dots below. – Jacob Wood Aug 17 '20 at 18:47
  • 1
    The index dots below are from the `indexDisplayMode`, you can remove them by setting it to `.never` @JacobWood – mwright Sep 03 '20 at 21:21
  • .indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .never)) .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) is what I used to disable the dots and their background – FontFamily Dec 25 '20 at 20:18
  • @JacobWood did you get this to work reliably? the gesture override is buggy for me – bze12 Dec 31 '20 at 05:20
  • 1
    Same - the swipe gesture still appears to change the page. I actually opened a new question (https://stackoverflow.com/questions/65524458/swiftui-tabview-pagetabviewstyle-prevent-changing-tab) about it, but can close it if the answer here is updated. Thanks! – Hoopes Dec 31 '20 at 18:30
  • I didn't end up getting a flawless solution. I came close but the user could still tap the page indicators to switch between tabs. I decided to go with a different design altogether and would encourage anybody looking to use this to just create their own custom view. – Jacob Wood Jan 11 '21 at 15:35
  • @JacobWood can you share what solution you used? This solution works, except when I programmatically change the page (e.g. via a button), I can intercept the change if I tap the screen. – bze12 Mar 03 '21 at 01:39
  • I didn't continue with this design. If I were to do it again I'd create a custom view and handle the swipes myself. – Jacob Wood Mar 08 '21 at 13:52
  • This does not work. See issue here: https://stackoverflow.com/questions/72962447/disable-to-change-page-on-swipe-of-tab-view-in-swiftui – Binh Ho Nov 11 '22 at 09:05
  • For views that have no content (for example a spacer) @hardyfelix in one of the comments below: "Gestures doesn't work on transparent views by default. I made your code work by adding .contentShape(Rectangle()) before adding the gesture. See: https://stackoverflow.com/questions/59008409/swiftui-vstack-hstack-zstack-drag-gesture-not-working" – Kirill Dubovitskiy Jul 11 '23 at 22:05
4

I tried Asperis's solution, but I still couldn't disable the swiping, and adding disabled to true didn't work since I want the child views to be interactive. The solution that worked for me was using Majid's (https://swiftwithmajid.com/2019/12/25/building-pager-view-in-swiftui/) custom Pager View and adding a conditional like Asperi's solution.

Majid's PagerView with conditional:

import SwiftUI

struct PagerView<Content: View>: View {
    let pageCount: Int
    @Binding var canDrag: Bool
    @Binding var currentIndex: Int
    let content: Content
    
    
    init(pageCount: Int, canDrag: Binding<Bool>, currentIndex: Binding<Int>, @ViewBuilder content: () -> Content) {
        self.pageCount = pageCount
        self._canDrag = canDrag
        self._currentIndex = currentIndex
        self.content = content()
    }
    
    
    @GestureState private var translation: CGFloat = 0
    
    var body: some View {
        GeometryReader { geometry in
            HStack(spacing: 0) {
                self.content.frame(width: geometry.size.width)
            }
            .frame(width: geometry.size.width, alignment: .leading)
            .offset(x: -CGFloat(self.currentIndex) * geometry.size.width)
            .offset(x: self.translation)
            .animation(.interactiveSpring(), value: currentIndex)
            .animation(.interactiveSpring(), value: translation)
            .gesture(!canDrag ? nil : // <- here
                
                DragGesture()
                    .updating(self.$translation) { value, state, _ in
                        
                        state = value.translation.width
                    }
                    .onEnded { value in
                        let offset = value.translation.width / geometry.size.width
                        let newIndex = (CGFloat(self.currentIndex) - offset).rounded()
                        self.currentIndex = min(max(Int(newIndex), 0), self.pageCount - 1)
                    }
            )
        }
    }
    
}

ContentView:

import SwiftUI

struct ContentView: View {
    
    
    @State private var currentPage = 0
    @State var canDrag: Bool = true
    
    
    var body: some View {
        
        PagerView(pageCount: 3, canDrag: $canDrag, currentIndex: $currentPage) {
            VStack {
                Color.blue
                
                
                Button {
                    canDrag.toggle()
                } label: {
                    Text("Toogle drag")
                }

            }
            
            VStack {
                Color.red
                
                
                Button {
                    canDrag.toggle()
                } label: {
                    Text("Toogle drag")
                }

            }
            
            VStack {
                Color.green
                
                
                Button {
                    canDrag.toggle()
                } label: {
                    Text("Toogle drag")
                }

            }
            
        }
        
    }
    
}
Thel
  • 396
  • 2
  • 8
  • 2
    Gestures doesn't work on transparent views by default. I made your code work by adding ```.contentShape(Rectangle())``` before adding the gesture. See: https://stackoverflow.com/questions/59008409/swiftui-vstack-hstack-zstack-drag-gesture-not-working – hardyfelix Feb 15 '22 at 12:00
  • Is it possible to add some kind of `PageTabViewStyle` so I can see dots at bottom? – Nikola C Feb 19 '22 at 14:26
3

Ok I think it is possible to block at least 99% swipe gesture if not 100% by using this steps:

  1. and 2. Add .gesture(DragGesture()) to each page Add .tabViewStyle(.page(indexDisplayMode: .never))
SwiftUI.TabView(selection: $viewModel.selection) {
            ForEach(pages.indices, id: \.self) { index in
                pages[index]
                    .tag(index)
                    .gesture(DragGesture())
            }
        }
        .tabViewStyle(.page(indexDisplayMode: .never))
  1. Add .highPriorityGesture(DragGesture()) to all remaining views images, buttons that still enable to drag and swipe pages

You can also in 1. use highPriorityGesture but it completely blocks drags on each pages, but I need them in some pages to rotate something

Michał Ziobro
  • 10,759
  • 11
  • 88
  • 143
  • Does not work for me, swiping is still 100% possible. Did you miss something in your answer? – Adriano Jul 08 '22 at 09:39
  • If I use this .highPriorityGesture(DragGesture()) for each button or view that still enables swiping left - right than it stops swiping for me. Maybe version of iOS is important here. I am testint on iOS 15.4 – Michał Ziobro Jul 12 '22 at 09:00
3

The below works for me

TabView(selection: $selectedTab) {
             // contents   
}
.onAppear {
      UIScrollView.appearance().isScrollEnabled = false
}
n1m1
  • 869
  • 1
  • 9
  • 20
0

The solution that worked for me is this one. It disables changing tabs by swiping and it keeps the drag gestures enabled on screens as I'm using List .onDelete on certain screens.

It is available only from iOS 16

@State private var selectedTab = 1

    TabView(selection: $selectedTab) {
        Text("Tab 1")
            .tag(0)
            .toolbar(.hidden, for: .tabBar)
        Text("Tab 2")
            .tag(1)
            .toolbar(.hidden, for: .tabBar)
        Text("Tab 3")
            .tag(2)
            .toolbar(.hidden, for: .tabBar)
    }
razvan
  • 355
  • 4
  • 19
-1

For anyone trying to figure this out, I managed to do this by setting the TabView state to disabled.

TabView(selection: $currentIndex.animation()) {
    Items()
 }.disabled(true)
 

Edit: as mentioned in the comments this will disable everything within the TabView as well

Goopher
  • 125
  • 8