SwiftUI 2.0 TabView disable swipe to change page
Asked Answered
P

7

21

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.sstatic.net/20qnW.jpg

Pitta answered 30/7, 2020 at 7:15 Comment(1)
Would a workaround like this work for you? I achieved this only using VStack and animations. https://mcmap.net/q/659587/-swiftui-tabview-pagetabviewstyle-prevent-changing-tabTriny
G
21

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))
Ghat answered 30/7, 2020 at 9:42 Comment(11)
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.Conspecific
The index dots below are from the indexDisplayMode, you can remove them by setting it to .never @JacobWoodBoiney
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .never)) .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) is what I used to disable the dots and their backgroundUgly
@JacobWood did you get this to work reliably? the gesture override is buggy for meElectrograph
Same - the swipe gesture still appears to change the page. I actually opened a new question (#65524958) about it, but can close it if the answer here is updated. Thanks!Prolonge
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.Conspecific
@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.Electrograph
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.Conspecific
This does not work. See issue here: #72962947Brassy
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: #59008909"Gardenia
Thanks @KirillDubovitskiy .contentShape(Rectangle()) helped fixing this problem.Rash
R
8

The below works for me

TabView(selection: $selectedTab) {
             // contents   
}
.onAppear {
      UIScrollView.appearance().isScrollEnabled = false
}
Rockingham answered 27/7, 2023 at 13:53 Comment(4)
what about if you have a scrollview in one of the tabs? That would break the functionality of scrolling.Epineurium
Used it with a vertical scrollview (IOS17) works like a charm but the DatePicker does not scroll anymore.Scaphoid
Appearance proxy modifies every instance of that class. I suggest github.com/siteline/swiftui-introspect to modify underlying UIKit object. This library is a lifesaver if you are trying to support old iOS versions.Collings
I also confirm that DatePickers do not work anymore with this unfortunately.Triny
S
6

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

Sinusitis answered 6/7, 2022 at 7:53 Comment(3)
Does not work for me, swiping is still 100% possible. Did you miss something in your answer?Debrief
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.4Earthy
This worked for me with IOS17Scaphoid
S
5

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")
                }

            }
            
        }
        
    }
    
}
Saccule answered 31/1, 2022 at 14:23 Comment(2)
Gestures doesn't work on transparent views by default. I made your code work by adding .contentShape(Rectangle()) before adding the gesture. See: #59008909Ragouzis
Is it possible to add some kind of PageTabViewStyle so I can see dots at bottom?Enceladus
A
2

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)
    }
Aureaaureate answered 18/4, 2023 at 11:41 Comment(1)
I tried this, and it feels like the cleanest solution. I'm using iOS 16 - 17 so it works in my case.Karen
E
1

no workarounds here worked for me so I had to go with Introspect:

.introspect(.scrollView, on: .iOS(.v17)) { scrollView in
    scrollView.isScrollEnabled = false
}
Efta answered 22/1 at 18:3 Comment(0)
P
-2

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

Plethoric answered 23/2, 2021 at 16:37 Comment(1)
this works, but now none of the items are interactiveElectrograph

© 2022 - 2024 — McMap. All rights reserved.