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

iOS 17中SwiftUI设置菜单同时手势失效问题求助

问题:iOS 17.5/17.7下SwiftUI设置菜单滚动手势失效

使用SwiftUI开发的设置菜单在iOS 17.5和17.7版本出现手势冲突问题:滑动列表行无法触发ScrollView滚动,仅能在Section间隙滑动时实现滚动。尝试过自定义ScrollView、高优先级手势等方案均无效,需要适配iOS 17的修复方案。

相关代码
struct SettingsList: View {

    var body: some View {
        ScrollView(.vertical) {
            VStack(spacing: 17.5) {
                SettingsSection(content: [
                    SettingsRow(title: "A"),
                    SettingsRow(title: "B"),
                    SettingsRow(title: "C"),
                    SettingsRow(title: "D")
                ])
                .padding(.top, 10)
                SettingsSection(content: [
                    SettingsRow(title: "E", showsChevron: false)
                ])
                SettingsSection(content: [
                    SettingsRow(title: "F"),
                    SettingsRow(title: "G"),
                    SettingsRow(title: "H"),
                    SettingsRow(title: "I")
                ])
                SettingsSection(content: [
                    SettingsRow(title: "J")
                ])
                SettingsSection(content: [
                    SettingsRow(title: "K"),
                    SettingsRow(title: "L"),
                    SettingsRow(title: "M"),
                    SettingsRow(title: "N"),
                    SettingsRow(title: "O")
                ])
                SettingsSection(content: [
                    SettingsRow(title: "P"),
                    SettingsRow(title: "Q")
                ])
                .padding(.top, 25)
            }
            .padding(.vertical, 10)
        }
        
    }
}

struct SettingsSection: View {
    let content: [SettingsRow]
    
    init(content: [SettingsRow]) {
        self.content = content
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            ZStack {
                VStack(spacing: 0) {
                    ForEach(Array(content.indices), id: \.self) { index in
                        content[index]
                    }
                }
                VStack(spacing: SettingsRow.height) {
                    ForEach(0...content.count, id: \.self) { index in
                        if index > 0 && index != content.count {
                            Divider()
                                .padding(.leading, 15)
                                .frame(height: 0)
                        } else {
                            Divider()
                                .frame(height: 0)
                        }
                    }
                }
            }
            .background(Color.white)
        }
    }
}

struct SettingsRow: View {
    
    static let height: CGFloat = 53
    
    var title: String
    var showsChevron = true
    var onClick: () -> Void
    
    @GestureState private var isPressed = false
    @State private var isCancelled = false
        
    private let selectionColor = Color(red: 210.0 / 255.0, green: 209.0 / 255.0, blue: 214.0 / 255.0)
    private let chevronColor = Color(red: 197.0 / 255.0, green: 197.0 / 255.0, blue: 199.0 / 255.0)

    init(title: String, showsChevron: Bool = true, onClick: @escaping () -> Void = {}) {
        self.title = title
        self.showsChevron = showsChevron
        self.onClick = onClick
    }
    
    var backgroundColor: Color {
        return isPressed && !isCancelled ? Color.black.opacity(0.15) : Color.clear
    }
    
    var body: some View {
        HStack(spacing: 0) {
            Text(title)
                .font(.custom("Avenir-Light", size: 17))
                .foregroundColor(.black)
                .padding(.trailing, 10)
            Spacer()
            
            if showsChevron {
                Image(systemName: "chevron.right")
                    .font(.system(size: 13).weight(.semibold))
                    .foregroundColor(chevronColor)
            }
        }
        .padding(.horizontal, 15)
        .padding(.vertical, 12.5)
        .frame(height: Self.height)
        .contentShape(Rectangle()) // Makes the entire row tappable
        .background(backgroundColor) // Darkens when pressed
        .background(Color.white)
        .animation(.easeInOut(duration: 0.1), value: isPressed)
        .simultaneousGesture(
            DragGesture(minimumDistance: 0)
                .updating($isPressed) { value, isPressed, _ in
                    // Check vertical movement threshold
                    if abs(value.translation.height) > 10 { // Vertical threshold
                        isCancelled = true
                    } else {
                        isCancelled = false
                        isPressed = true
                    }
                }
                .onEnded { value in
                    if !isCancelled && abs(value.translation.height) <= 10 {
                        onClick()
                    }
                    isCancelled = false
                }
        )
    }
}
修复方案

核心原因

iOS 17对手势识别的优先级逻辑做了调整,自定义DragGesture(minimumDistance: 0)会拦截所有触摸事件,包括ScrollView的滚动手势,导致ScrollView无法响应滚动操作。原生组件的手势处理经过系统优化,能自动区分点击与滚动动作,避免冲突。

解决方案:替换为原生Button组件

将自定义的手势逻辑替换为SwiftUI原生Button,通过自定义ButtonStyle实现按压效果,完全规避手势冲突问题:

struct SettingsRow: View {
    
    static let height: CGFloat = 53
    
    var title: String
    var showsChevron = true
    var onClick: () -> Void
    
    private let chevronColor = Color(red: 197.0 / 255.0, green: 197.0 / 255.0, blue: 199.0 / 255.0)

    init(title: String, showsChevron: Bool = true, onClick: @escaping () -> Void = {}) {
        self.title = title
        self.showsChevron = showsChevron
        self.onClick = onClick
    }
    
    var body: some View {
        Button(action: onClick) {
            HStack(spacing: 0) {
                Text(title)
                    .font(.custom("Avenir-Light", size: 17))
                    .foregroundColor(.black)
                    .padding(.trailing, 10)
                Spacer()
                
                if showsChevron {
                    Image(systemName: "chevron.right")
                        .font(.system(size: 13).weight(.semibold))
                        .foregroundColor(chevronColor)
                }
            }
            .padding(.horizontal, 15)
            .padding(.vertical, 12.5)
            .frame(height: Self.height)
        }
        .buttonStyle(SettingsRowButtonStyle())
        .background(Color.white)
    }
}

// 自定义ButtonStyle实现按压背景效果
struct SettingsRowButtonStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .background(configuration.isPressed ? Color.black.opacity(0.15) : Color.clear)
            .animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
    }
}

效果说明

  • Button会自动处理手势优先级:当用户开始垂直滚动时,系统会放弃点击手势,让ScrollView正常响应滚动;当用户点击或短按行时,触发点击事件并显示按压效果。
  • 保留原有的UI样式和交互逻辑,无需修改其他组件(SettingsList、SettingsSection)的代码。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 04:29:50