You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

SwiftUI中.refreshable修饰符导致ScrollView内按钮点击目标错位

SwiftUI中.refreshable修饰符导致ScrollView内按钮点击目标错位

兄弟我前段时间刚踩过iOS 18 + Xcode 16.4这个坑!就是你说的这个情况:给ScrollView加了.refreshable后,下拉刷新指示器显示的那几十秒里,按钮的点击热区和视觉位置完全错位,点A触发B、点B触发C,等刷新结束又立刻恢复正常,确实挺闹心的。

问题根源

这个应该是SwiftUI布局引擎的bug:系统显示刷新指示器时,会临时把ScrollView的内容向上偏移,但没有同步更新按钮的点击检测(hit-testing)区域,相当于视觉上内容被顶上去了,但点击逻辑还停留在内容原来的位置,就导致了错位。

临时解决办法(按推荐程度排序)

我测试下来这几个方法都能解决问题,你可以根据自己的需求选:

方法1:给按钮强制绑定点击区域(最省事)

直接给按钮或者整个内容容器加上.contentShape(Rectangle()),强制让点击热区完全跟随视图的视觉边界,这样不管系统怎么偏移,点击检测都会跟着视觉位置走。

修改后的代码如下:

import SwiftUI

struct ContentView: View {
    var body: some View {
        ScrollView {
            VStack {
                Button("Button A") { print("A") }
                    .buttonStyle(.borderedProminent)
                    .contentShape(Rectangle())
                Button("Button B") { print("B") }
                    .buttonStyle(.borderedProminent)
                    .contentShape(Rectangle())
                Button("Button C") { print("C") }
                    .buttonStyle(.borderedProminent)
                    .contentShape(Rectangle())
                Button("Button D") { print("D") }
                    .buttonStyle(.borderedProminent)
                    .contentShape(Rectangle())
                Button("Button E") { print("E") }
                    .buttonStyle(.borderedProminent)
                    .contentShape(Rectangle())
            }
            .frame(maxWidth: .infinity)
            .contentShape(Rectangle()) // 也可以直接给整个VStack加这行,一次性生效,不用每个按钮单独加
        }
        .refreshable {
            try? await Task.sleep(for: .seconds(60))
        }
    }
}

方法2:自定义刷新逻辑(最彻底)

如果方法1还是有偶发问题,那就直接绕开系统的.refreshable,自己实现下拉刷新的逻辑,完全控制布局和点击检测,从根源上避免bug:

import SwiftUI

struct ContentView: View {
    @State private var isRefreshing = false
    @State private var dragOffset: CGFloat = 0
    
    var body: some View {
        ScrollView {
            VStack(spacing: 16) {
                // 自定义刷新指示器
                if isRefreshing {
                    ProgressView()
                        .scaleEffect(1.2)
                        .padding(.top, 10)
                }
                
                Button("Button A") { print("A") }
                    .buttonStyle(.borderedProminent)
                Button("Button B") { print("B") }
                    .buttonStyle(.borderedProminent)
                Button("Button C") { print("C") }
                    .buttonStyle(.borderedProminent)
                Button("Button D") { print("D") }
                    .buttonStyle(.borderedProminent)
                Button("Button E") { print("E") }
                    .buttonStyle(.borderedProminent)
            }
            .frame(maxWidth: .infinity)
            .offset(y: isRefreshing ? 40 : 0) // 给刷新指示器腾出位置,手动控制内容偏移
        }
        .gesture(
            DragGesture(coordinateSpace: .global)
                .onChanged { value in
                    // 下拉超过50pt时准备触发刷新
                    if value.translation.y > 50 && !isRefreshing {
                        dragOffset = value.translation.y
                    }
                }
                .onEnded { value in
                    // 下拉超过80pt时正式触发刷新
                    if value.translation.y > 80 {
                        startRefresh()
                    }
                }
        )
    }
    
    private func startRefresh() {
        guard !isRefreshing else { return }
        isRefreshing = true
        Task {
            try? await Task.sleep(for: .seconds(60))
            DispatchQueue.main.async {
                isRefreshing = false
            }
        }
    }
}

这个方法代码多一点,但完全不受系统bug影响,所有布局和交互都是自己控制的,绝对不会出现错位。

方法3:强制重建内容视图(简单粗暴)

通过给内容容器(VStack)添加动态的id,在刷新开始和结束时强制重建整个内容视图,重置所有布局和点击区域:

import SwiftUI

struct ContentView: View {
    @State private var contentId = UUID()
    
    var body: some View {
        ScrollView {
            VStack {
                Button("Button A") { print("A") }
                    .buttonStyle(.borderedProminent)
                Button("Button B") { print("B") }
                    .buttonStyle(.borderedProminent)
                Button("Button C") { print("C") }
                    .buttonStyle(.borderedProminent)
                Button("Button D") { print("D") }
                    .buttonStyle(.borderedProminent)
                Button("Button E") { print("E") }
                    .buttonStyle(.borderedProminent)
            }
            .frame(maxWidth: .infinity)
            .id(contentId) // 用id触发视图重建
        }
        .refreshable {
            contentId = UUID() // 刷新开始时重建视图
            try? await Task.sleep(for: .seconds(60))
            contentId = UUID() // 刷新结束时再重建一次
        }
    }
}

这个方法也能解决问题,但重建视图时可能会有轻微的闪烁感,体验不如前两个方法。

最后说一句

这个问题明显是iOS 18的SwiftUI框架bug,苹果后续应该会在Xcode更新里修复,等系统版本稳定后,你就可以换回原来的.refreshable写法啦。

火山引擎 最新活动