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

SwiftUI可折叠头部下拉手势后意外隐藏及优化后抖动/初始不显示问题求助

SwiftUI可折叠头部下拉手势后意外隐藏及优化后抖动/初始不显示问题求助

大家好,我最近在SwiftUI里实现一个滚动响应的可折叠头部:向下滚动列表时收起头部,向上滚动时展开头部。跟着教程做完后,正常滚动时功能都没问题,但遇到了两个头疼的问题,想请教下大家怎么解决:

第一个问题:下拉手势导致头部意外消失

当用户从列表顶部下拉(类似触发下拉刷新的手势)然后松开,头部会直接消失,录屏里能看到这个异常行为。下面是我最开始的实现代码:

public struct CollapsibleHeaderList: View {
    // MARK: - Private Vars
    private let items = Array(0..<100)
    @State private var currentHeight: Double = 0
    @State private var lastOffset: Double = 0
    @State private var animationDuration: TimeInterval = 0.2
    @State private var lastAnimationDate: Date?
    private var expandedHeight: CGFloat = 40.0
    
    @ObservedObject var viewModel: CollapsibleHeaderViewModel
    
    public init(viewModel: CollapsibleHeaderViewModel) {
        self.viewModel = viewModel
    }
    
    // MARK: - View
    public var body: some View {
        VStack {
            Rectangle()
                .foregroundColor(.pink)
                .frame(height: currentHeight)
            
            List {
                ForEach(items, id: \.self) { item in
                    Text("Item \(item)")
                        .onAppear {
                            viewModel.onCellAppear(index: item)
                        }
                }
            }
        }
        .environment(\.defaultMinListRowHeight, 0)
        .listStyle(.plain)
        .onReceive(viewModel.$state) { state in
            DispatchQueue.main.async {
                switch(state) {
                case .collapse:
                    collapseHeader()
                case .expand:
                    expandHeader()
                case .initial:
                    break
                }
            }
        }
    }
    
    func expandHeader() {
        withAnimation(.easeOut(duration: animationDuration)) {
            currentHeight = expandedHeight
            lastAnimationDate = Date()
        }
    }
    
    func collapseHeader() {
        withAnimation(.easeOut(duration: animationDuration)) {
            currentHeight = 0
            lastAnimationDate = Date()
        }
    }
    
    func didFinishLastAnimation() -> Bool {
        guard let lastAnimationDate else { return true }
        return abs(lastAnimationDate.timeIntervalSinceNow) > animationDuration
    }
}

public enum HeaderState {
    case initial
    case collapse
    case expand
}

public class CollapsibleHeaderViewModel: ObservableObject {
    @Published private(set) public var state: HeaderState
    private var indexSubject = PassthroughSubject<Int, Never>()
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        self.state = .initial
        setupCollapsibleHeaderListener()
    }
    
    public func onCellAppear(index: Int) {
        indexSubject.send(index)
    }
    
    private func setupCollapsibleHeaderListener() {
        indexSubject
            .throttle(for: .seconds(0.5), scheduler: DispatchQueue.main, latest: true)
            .withPrevious()
            .map { (previous, current) in
                if let previous, previous < current {
                    return .collapse
                } else {
                    return .expand
                }
            }
            .removeDuplicates()
            .sink { [weak self] headerState in
                self?.state = headerState
            }.store(in: &cancellables)
    }
}

extension Publisher {
    /// Includes the current element as well as the previous element from the upstream publisher in a tuple where the previous element is optional.
    /// The first time the upstream publisher emits an element, the previous element will be `nil`.
    ///
    /// let range = (1...5)
    /// cancellable = range.publisher
    ///     .withPrevious()
    ///     .sink { print ("(\($0.previous), \($0.current))", terminator: " ") }
    /// // Prints: "(nil, 1) (Optional(1), 2) (Optional(2), 3) (Optional(3), 4) (Optional(4), 5) ".
    ///
    /// - Returns: A publisher of a tuple of the previous and current elements from the upstream publisher.
    public func withPrevious() -> AnyPublisher<(previous: Output?, current: Output), Failure> {
        scan(Optional<(Output?, Output)>.none) { ($0?.1, $1) }
            .compactMap { $0 }
            .eraseToAnyPublisher()
    }
}

第二个问题:优化后出现的抖动与初始不显示

后来我尝试了@Benzy Neez的解决方案,引入了showingHeader状态来控制头部显示,结果又出现新问题:头部出现时会有奇怪的抖动/闪烁效果,而且视图首次加载的时候,头部直接就不见了,完全不显示。修改后的核心代码如下:

@State private var showingHeader = true

var body: some View {
    ZStack {
        Color.backgroundColor3
            .edgesIgnoringSafeArea(.all)
        
        VStack(spacing: 0) {
            if showingHeader {
                GlobalContactsFilterView(viewModel: viewModel)
                    .frame(height: 60)
                    .transition(
                        .asymmetric(
                            insertion: .push(from: .top),
                            removal: .push(from: .bottom)
                        )
                    )
            }
            
            if viewModel.isRefreshing {
                VStack {
                    ProgressView()
                        .progressViewStyle(CircularProgressViewStyle())
                }
                .frame(maxWidth: .infinity)
                .padding(.top, 10)
            }
            
            if viewModel.contacts.isEmpty && !viewModel.isLoading && !viewModel.isRefreshing {
                NoDataView()
            } else {
                GeometryReader { outer in
                    let outerHeight = outer.size.height
                    ScrollView(.vertical) {
                        LazyVStack {
                            ForEach(Array(viewModel.contacts.enumerated()), id: \.element.id) { index, contact in
                                VStack(alignment: .leading) {
                                    VStack {
                                        VStack(spacing: 12) {
                                            if contactVisibleFields.contains(.firstName) {
                                                // 这里是Contact的具体内容
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

现在想请教各位:

  1. 最初的下拉手势导致头部消失的问题,该怎么从根源上解决?
  2. 修改后出现的头部抖动和初始不显示的问题,又该如何修复?

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.07 09:08:07