Struggling with Swift Gesture/Observation

I'm trying to create an equivalent to TabView, but with the difference that the 2nd View slides in over the top of the primary view.

Maybe there's a more elegant way of coding this (suggestions appreciated), but I've almost succeeded using the dragGesture. When a user swipes right to left the observed variable showTab2 is set to true, and the 2nd tab glides in over the top of tab 1 and displays 🥳.

The only problem is, that when a user happens to start the swipe over a button, the observed status (showTab2) does change as expected but the main view does not catch this change and does not display tab2. And that despite the showTab2 being an @Observable.

Any clues what I've missed? Or how to capture that the start of a swipe gesture starts over the top of a button and should be ignored.

According to the code in SwipeTabView this screenshot 👆 should never occur.

Here's the code:

@Observable
class myclass {
    var showTab2 = false
}


struct SwipeTabView: View {
   
    @State var myClass = myclass()
    @State var dragAmount: CGSize = CGSize.zero
    
    var body: some View {
        
        VStack {
            ZStack {
                GeometryReader { geometryProxy in
                    VStack {
                        tab(tabID: 1, selectedTab: myClass.showTab2)
                            .zIndex(/*@START_MENU_TOKEN@*/1.0/*@END_MENU_TOKEN@*/)
                            .background(.black)
                            .transition(.identity)
                            .swipeable(stateOfViewAdded: $myClass.showTab2, dragAmount: $dragAmount, geometryProxy: geometryProxy, insertion: true)
                    }
                    
                    if myClass.showTab2 || dragAmount.width != 0 {
                        tab(tabID: 2, selectedTab: myClass.showTab2)
                            .zIndex(2.0)
                            .drawingGroup()
                            .transition(.move(edge: .trailing))
                            .offset(x:  dragAmount.width  )
                            .swipeable(stateOfViewAdded: $myClass.showTab2, dragAmount: $dragAmount, geometryProxy: geometryProxy, insertion: false)
                    }
                }
            }
        }
    }
}

extension View {
    
    func swipeable(stateOfViewAdded:    Binding<Bool>,
                   dragAmount:          Binding<CGSize>,
                   geometryProxy:       GeometryProxy,
                   insertion:           Bool) -> some View {
        
        self.gesture(
            DragGesture()
                .onChanged { gesture in
                    
                    // inserting must be minus, but removing must be positive - hence the multiplication.
                    if gesture.translation.width * (insertion ? 1 : -1 ) < 0 {
                        if insertion {
                            dragAmount.wrappedValue.width = geometryProxy.size.width + gesture.translation.width
                        } else {
                            dragAmount.wrappedValue.width =  gesture.translation.width
                        }
                    }
                }
                .onEnded { gesture in
                    if abs(gesture.translation.width) > 100.0 && gesture.translation.width * (insertion ? 1 : -1 ) < 0 {
                        withAnimation(.easeOut.speed(Double(gesture.velocity.width))) {
                            stateOfViewAdded.wrappedValue = insertion
                        }
                        
                    } else {
                        withAnimation(.easeOut.speed(Double(gesture.velocity.width))) {
                            stateOfViewAdded.wrappedValue = !insertion
                        }
                    }
                    
                    withAnimation(.smooth) {
                        dragAmount.wrappedValue = CGSize.zero
                    }
                }
        )
    }
}


struct tab: View {
    var tabID: Int
    var selectedTab: Bool
    
    var body: some View {
        
        ZStack {
            Color(tabID == 1 ? .yellow : .orange)
            
            VStack {
                Text("Tab \(tabID) ").foregroundColor(.black)
                
                Button(action:  {
                    print("Tab2 should display - \(selectedTab.description)")
                }, label: {
                    ZStack {
                        circle
                        label
                    }
                })
                Text("Tab2 should display - \(selectedTab.description)")
            }
        }
    }
    
    var circle: some View  {
        Circle()
            .frame(width: 100, height: 100)
            .foregroundColor(.red)
    }
    
    var label: some View {
        Text("\(tabID == 1 ? ">>" : "<<")").font(.title).foregroundColor(.black)
    }
}

Answered by Alanrick in 803654022

I compiled this using Xcode 16RC 16A242 (instead of 15) and it now just about works. In iPadOS 18RC it works perfectly. In iOS 17.6.1 the swipe does not react when started from on top of the button but the showTab2 property does not change either so it remains consistent. It seems this is a problem in Xcode 15 but it has been fixed in Xcode 16 as Sydney 🙏 suggested.

PS: 🏆Kudus to Xcode Cloud for making this test so easy to perform. I'd never used it before and it was truly simple to onboard.

Hi @Alanrick ,

Can you tell me what Xcode you're on as well as what iPhone and iOS version?

I tested with Xcode 16 RC from the developer site and iPhone 16 with iOS 18.1 and could not reproduce the issue. I put my finger on the button and then swiped and the pages switched as expected for me.

Thanks!

Sydney

Hi Sydney, Xcode Version 15.4 (15F31d) Swift 5 (according to the build settings) I'm surprised, but can't figure yet how to update to 5.1.

The code is running on the simulator (iPhone 15) and also iPadOS 18.0 (22A3354) iPad Air and iOS 17.6.1 on an iPhone 13 Pro.

I checked all gesture attributes and couldn't find anything dodgy about them.

Here's the video: https://youtube.com/shorts/SVlESw5nGX0?si=5JCUgu2iYBCh9Smk

Thanks for your interest, Alan

(Post obsolete)

Accepted Answer

I compiled this using Xcode 16RC 16A242 (instead of 15) and it now just about works. In iPadOS 18RC it works perfectly. In iOS 17.6.1 the swipe does not react when started from on top of the button but the showTab2 property does not change either so it remains consistent. It seems this is a problem in Xcode 15 but it has been fixed in Xcode 16 as Sydney 🙏 suggested.

PS: 🏆Kudus to Xcode Cloud for making this test so easy to perform. I'd never used it before and it was truly simple to onboard.

Struggling with Swift Gesture/Observation
 
 
Q