ios – SwiftUI collapsible header hides after pull-down gesture

0
2
ios – SwiftUI collapsible header hides after pull-down gesture


I’m attempting to implement a collapsible header in SwiftUI that collapses when the consumer scrolls down and expands when the consumer scrolls up.

I adopted this Medium put up: Collapsible Header in SwiftUI

The implementation works tremendous when scrolling usually, however I’ve run into an issue:

  • When the consumer drags the view down from the highest (pulls to refresh fashion) after which releases, the header disappears/hides unexpectedly.

Right here’s a brief display recording exhibiting the problem:
Display Recording

And right here is the code I’m utilizing:

public struct CollapsibleHeaderList: View {
    // MARK: - Non-public Vars
    non-public let gadgets = Array(0..<100)
    @State non-public var currentHeight: Double = 0
    @State non-public var lastOffset: Double = 0
    @State non-public var animationDuration: TimeInterval = 0.2
    @State non-public var lastAnimationDate: Date?
    non-public var expandedHeight: CGFloat = 40.0
    @ObservedObject var viewModel: CollapsibleHeaderViewModel

    public init(viewModel: CollapsibleHeaderViewModel) {
        self.viewModel = viewModel
    }
    
    // MARK: - View
    public var physique: some View {
        VStack {
            Rectangle()
                .foregroundColor(.pink)
                .body(peak: currentHeight)
            Checklist {
                ForEach(gadgets, id: .self) { merchandise in
                    Textual content("Merchandise (merchandise)")
                    .onAppear {
                        viewModel.onCellAppear(index: merchandise)
                    }
                }
            }
        }.atmosphere(.defaultMinListRowHeight, 0)
        .listStyle(.plain)
        .onReceive(viewModel.$state) { state in
            DispatchQueue.principal.async {
                change(state) {
                case .collapse:
                    collapseHeader()
                case .develop:
                    expandHeader()
                case .preliminary: 
                    break
                }
            }

        }
    }
    

    func expandHeader() {
        withAnimation(.easeOut(period: animationDuration)) {
            currentHeight = expandedHeight
            lastAnimationDate = Date()
        }
    }
    
    func collapseHeader() {
        withAnimation(.easeOut(period: animationDuration)) {
            currentHeight = 0
            lastAnimationDate = Date()
        }
    }
    
    func didFinishLastAnimation() -> Bool {
        guard let lastAnimationDate else {
            return true
        }
        
        return abs(lastAnimationDate.timeIntervalSinceNow) > animationDuration
    }
}

public enum HeaderState {
    case preliminary
    case collapse
    case develop
}
public class CollapsibleHeaderViewModel: ObservableObject {
    @Printed non-public(set) public var state: HeaderState
    non-public var indexSubject = PassthroughSubject()
    non-public var cancellables = Set()

    init() {
        self.state = .preliminary
        setupCollapsibleHeaderListener()
    }

    public func onCellAppear(index: Int) {
        indexSubject.ship(index)
    }

    non-public func setupCollapsibleHeaderListener() {
        indexSubject
            .throttle(for: .seconds(0.5), scheduler: DispatchQueue.principal, newest: true)
            .withPrevious()
            .map { (earlier, present) in
                if let earlier, earlier < present {
                    return .collapse
                } else {
                    return .develop
                }
            }
            .removeDuplicates()
            .sink { [weak self] headerState in
                self?.state = headerState
            }.retailer(in: &cancellables)
    }
}



extension Writer {
    /// Consists of the present component in addition to the earlier component from the upstream writer in a tuple the place the earlier component is elective.
    /// The primary time the upstream writer emits a component, the earlier component will likely be `nil`.
    /// This code was copied from https://stackoverflow.com/questions/63926305/combine-previous-value-using-combine
    ///
    ///     let vary = (1...5)
    ///     cancellable = vary.writer
    ///         .withPrevious()
    ///         .sink { print ("(($0.earlier), ($0.present))", terminator: " ") }
    ///      // Prints: "(nil, 1) (Elective(1), 2) (Elective(2), 3) (Elective(3), 4) (Elective(4), 5) ".
    ///
    /// - Returns: A writer of a tuple of the earlier and present parts from the upstream writer.
    public func withPrevious() -> AnyPublisher<(earlier: Output?, present: Output), Failure> {
        scan(Elective<(Output?, Output)>.none) { ($0?.1, $1) }
            .compactMap { $0 }
            .eraseToAnyPublisher()
    }
}

Query:
How can I forestall the header from hiding when the consumer pulls down from the highest?

Replace:

I attempted to implement the answer @Benzy Neez
advised, however now I get a bizarre shaking/jitter impact when the header seems. Additionally, when the view seems for the very first time, the header just isn’t seen — it disappears instantly.

Right here is the display recording : Display recording 2

Right here is myCurrent code:

@State non-public var showingHeader = true
    
    var physique: some View {
        ZStack {
            Coloration.backgroundColor3
                .edgesIgnoringSafeArea(.all)
            VStack(spacing: 0) {
                if showingHeader {
                    GlobalContactsFilterView(viewModel: viewModel)
                        .body(peak: 60)
                        .transition(
                            .uneven(
                                insertion: .push(from: .high),
                                removing: .push(from: .backside)
                            )
                        )
                }
                
                if viewModel.isRefreshing {
                    VStack {
                        ProgressView()
                            .progressViewStyle(CircularProgressViewStyle())
                    }
                    .body(maxWidth: .infinity)
                    .padding(.high, 10)
                }
                
                if viewModel.contacts.isEmpty && !viewModel.isLoading && !viewModel.isRefreshing {
                    NoDataView()
                } else {
                    GeometryReader { outer in
                        let outerHeight = outer.dimension.peak
                        ScrollView(.vertical) {
                            LazyVStack {
                                ForEach(Array(viewModel.contacts.enumerated()), id: .component.id) { index, contact in
                                    VStack(alignment: .main) {
                                        VStack {
                                            VStack(spacing: 12) {
                                                
                                                if contactVisibleFields.incorporates(.firstName) {
                                                    ContactDetailField(title: Strings.firstName, textual content: contact.firstName, textFont: .system(dimension: 13, weight: .semibold))
                                                }
                                                
                                                Rectangle()
                                                    .fill(.clear)
                                                    .body(maxWidth: .infinity, maxHeight: 1)
                                            }
                                            .padding(.high, 10)
                                        }
                                        .body(maxWidth: .infinity)
                                        .background(.backgroundColor2)
                                        .padding(.backside, 3)
                                    }
                                    .body(maxWidth: .infinity)
                                    .background(index == viewModel.contacts.depend - 1 ? Coloration.clear : Coloration.filterBarBackground)
                                    .padding(.backside, index == viewModel.contacts.depend - 1 ? 50 : 0)
                                    
                                }
                                .listRowBackground(Coloration.clear)
                                .listRowInsets(EdgeInsets())
                                .listRowSeparator(.hidden)
                                
                                HStack {
                                    Spacer()
                                    if viewModel.isLoading && !viewModel.isRefreshing {
                                        ProgressView()
                                            .progressViewStyle(CircularProgressViewStyle())
                                    } else {
                                        Coloration.clear
                                            .body(peak: 1)
                                            .onAppear {
                                                if let _ = clerk.consumer {
                                                    viewModel.fetchContacts()
                                                }
                                            }
                                    }
                                    Spacer()
                                }
                                .body(maxWidth: .infinity)
                                .listRowSeparator(.hidden)
                                .listRowBackground(Coloration.clear)
                            }
                            .background {
                                GeometryReader { proxy in
                                    let contentHeight = proxy.dimension.peak
                                    let minY = max(
                                        min(0, proxy.body(in: .named("ScrollView")).minY),
                                        outerHeight - contentHeight
                                    )
                                    Coloration.clear
                                        .onChange(of: minY) { oldVal, newVal in
                                            if (showingHeader && newVal < oldVal) || !showingHeader && newVal > oldVal {
                                                showingHeader = newVal > oldVal
                                            }
                                        }
                                }
                            }
                        }
                        .coordinateSpace(identify: "ScrollView")
                    }
                    .padding(.high, 1)
                }
            }
            //.body(maxHeight: .infinity, alignment: .high)
            .animation(.easeInOut, worth: showingHeader)

LEAVE A REPLY

Please enter your comment!
Please enter your name here