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
相关产品推荐
相关产品推荐

