You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI Z-Stack按钮触发卡片滑动时层级异常问题求助

问题描述

实现了类似Tinder的卡片左右滑动功能,支持手指滑动与按钮触发滑动操作。手指滑动时功能完全正常,但通过按钮触发滑动时,卡片会从Z-Stack的下一层被移除,而非手指滑动时那样从顶层移除。

相关代码

可滑动卡片组件

@EnvironmentObject var buttonState: ButtonState
@State var offset: CGFloat = 0
@GestureState var isDragging: Bool = false
@State var endSwipe: Bool = false
@State var buttonPressed = false

GeometryReader { proxy in
    let size = proxy.size
    let index = CGFloat(cardVM.getIndex(card: card))
    Card()
        .frame(width: size.width, height: size.height)
        .offset(x: offset)
        .rotationEffect(.init(degrees: getRotation(angle: 8)))
        .contentShape(Rectangle().trim(from: 0, to: endSwipe ? 0 : 1))
        .gesture(
            DragGesture()
                .updating($isDragging, body: { value, out, _ in
                    out = true
                })
                .onChanged({ value in
                    let translation = value.translation.width
                    offset = (isDragging ? translation : .zero)
                    self.swipeRight = translation > 0 ? true : false
                    
                    buttonState.right = translation > 0 ? true : false
                    buttonState.isDragging = true
                })
                .onEnded({ value in
                    let width = getRect().width - 50
                    let translation = value.translation.width
                    let checkingStatus = (translation > 0 ? translation : -translation)
                    buttonState.isDragging = false
                    
                    withAnimation {
                        if checkingStatus > (width / 2) {
                            
                            offset = (translation > 0 ? width : -width) * 2
                            endSwipe = true
                            translation > 0 ? rightSwipe() : leftSwipe()
                            endSwipeActions()
                        } else {
                            offset = .zero
                        }
                    }
                })
        )
        .onReceive(NotificationCenter.default.publisher(for: Notification.Name("ACTIONFROMBUTTON"), object: nil)) { data in
            guard let info = data.userInfo else {
                return
            }
            
            let id = info["id"] as? String ?? ""
            let rightSwipe = info["rightSwipe"] as? Bool ?? false
            let width = getRect().width - 50
            
            if card.id == id {
                withAnimation {
                    offset = (rightSwipe ? width : -width) * 2
                    endSwipeActions()
                    
                    if rightSwipe {
                        self.rightSwipe()
                    } else {
                        leftSwipe()
                    }
                }
            }
    }
}

ButtonState类

class ButtonState: ObservableObject {
    @Published var right = false
    @Published var isDragging = false
}

主视图右滑触发代码

func doSwipe(rightSwipe: Bool = false) {
    guard let first = cardVM.displayingCards?.first else {
        return
    }
    NotificationCenter.default.post(name: NSNotification.Name("ACTIONFROMBUTTON"), object: nil, userInfo: [
        "id": first.id,
        "rightSwipe": rightSwipe
    ])
}

Button("Swipe right") {
    buttonState.isDragging = true
    buttonState.right = true
    doSwipe(rightSwipe: true)
}
问题原因

按钮触发滑动时,同步调用了卡片移除方法(rightSwipe()/leftSwipe()),导致卡片在动画完成前就从数据源中被移除,Z-Stack的下层卡片提前显示,视觉上呈现为从下层被移除。同时,按钮触发逻辑中未设置endSwipe = true,和手指滑动的状态逻辑不一致,可能导致contentShape的裁剪规则未生效,进一步影响层级显示效果。

解决方案

1. 对齐状态设置逻辑

在按钮触发的onReceive回调中添加endSwipe = true,和手指滑动的状态保持一致:

.onReceive(NotificationCenter.default.publisher(for: Notification.Name("ACTIONFROMBUTTON"), object: nil)) { data in
    guard let info = data.userInfo else {
        return
    }
    
    let id = info["id"] as? String ?? ""
    let rightSwipe = info["rightSwipe"] as? Bool ?? false
    let width = getRect().width - 50
    
    if card.id == id {
        withAnimation {
            offset = (rightSwipe ? width : -width) * 2
            endSwipe = true // 补全状态设置
            endSwipeActions()
            
            if rightSwipe {
                self.rightSwipe()
            } else {
                leftSwipe()
            }
        }
    }
}

2. 延迟执行卡片移除操作

利用iOS 15+支持的withAnimation完成回调,等滑动动画结束后再移除卡片,避免动画未完成就切换层级:

.onReceive(NotificationCenter.default.publisher(for: Notification.Name("ACTIONFROMBUTTON"), object: nil)) { data in
    guard let info = data.userInfo else {
        return
    }
    
    let id = info["id"] as? String ?? ""
    let rightSwipe = info["rightSwipe"] as? Bool ?? false
    let width = getRect().width - 50
    
    if card.id == id {
        withAnimation {
            offset = (rightSwipe ? width : -width) * 2
            endSwipe = true
            endSwipeActions()
        } completion: {
            // 动画完成后再执行移除逻辑
            if rightSwipe {
                self.rightSwipe()
            } else {
                leftSwipe()
            }
        }
    }
}

3. 优化触发逻辑(可选)

如果NotificationCenter存在状态同步问题,可以改用ViewModel驱动的方式:给CardViewModel添加triggerSwipe(for cardId: String, rightSwipe: Bool)方法,卡片组件监听ViewModel的状态变化来执行滑动操作,减少跨组件通知的延迟问题。

验证

修改后,按钮触发滑动时,卡片会先完成顶层的滑动动画,动画结束后再从数据源中移除,视觉效果和手指滑动完全一致。

内容的提问来源于stack exchange,提问作者MaxPayne777

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 06:31:06