Как стать автором
Обновить

Комментарии 6

Идея неплохая, но почему бы не использовать LazyVStack с pinnedViews? или религия таргет не позволяет? :)

вроде такой подход не сработает так как pinnedViews норм для хэдеров и футеров, а тут хотели любой айтем пинить

Вызов был принят ) Собственно наскоро запилил примерчик.

Выглядит вот так:

Код не претендует на перфенционизм автора, но реализован через хедер секции. А если еще и с напильником, чтобы краюшки скруглить где надо...

enum Constants {
    static let scrollViewCoordinateSpace = "ScrollView"
    static let itemHeight: CGFloat = 40
}

struct ContentView: View {
    enum ScrollDirection {
        case idle
        case up
        case down
    }
    
    @State private var selectedItemIndex: Int? = .none
    @State private var selectedItemOffset = CGFloat.zero
    @State private var scrollDirection: ScrollDirection = .idle
    
    var offsetFactor: CGFloat {
        switch scrollDirection {
            case .idle, .down: 0
            case .up: Constants.itemHeight
        }
    }
    
    var body: some View {
        NavigationStack {
            ScrollView {
                LazyVStack(pinnedViews: [.sectionHeaders]) {
                    Section {
                        ForEach(1...100, id: \.self) { count in
                            itemView(for: count)
                                .background(GeometryReader { proxy in
                                    if selectedItemIndex == count {
                                        Color.clear.preference(
                                            key: ViewOffsetKey.self,
                                            value: proxy.frame(in: .named(Constants.scrollViewCoordinateSpace)).minY
                                        )
                                    }
                                })
                        }
                    } header: {
                        selectedPinnedView
                    }
                }
                .padding()
                .onPreferenceChange(ViewOffsetKey.self) {
                    if selectedItemOffset < $0 {
                        scrollDirection = .up
                    } else {
                        scrollDirection = .down
                    }
                    
                    if selectedItemOffset <= -UIScreen.main.bounds.height 
                        && $0 < selectedItemOffset { return }
                    
                    self.selectedItemOffset = $0
                }
            }
            .coordinateSpace(name: Constants.scrollViewCoordinateSpace)
            .navigationTitle("SelectedPinnedView")
            .navigationBarTitleDisplayMode(.inline)
        }
    }
    
    func itemView(for index: Int) -> some View {
        Button(action: {
            selectedItemIndex = index
        })  {
            Text("Item \(index)")
                .foregroundStyle(.white)
                .frame(height: Constants.itemHeight)
                .frame(maxWidth: .infinity)
                .background(selectedItemIndex == index ? .green : .gray)
                .clipShape(RoundedRectangle(cornerRadius: 8))
                .contentShape(RoundedRectangle(cornerRadius: 8))
        }
        .buttonStyle(.plain)
    }
    
    @ViewBuilder
    var selectedPinnedView: some View {
        if let selectedItemIndex, selectedItemOffset < 0 + offsetFactor {
            itemView(for: selectedItemIndex)
        }
    }
}

ЗЫ.: Отредактировал onPreferenceChange, заметил, что LazyVStack убирает View когда она выходит за 1 высоту экрана, отчего offset выставлялся в 0. Но теперь все гладко и шелковисто.

Такая идея была, но не придумала, как ее использовать для того, чтобы прилеплять любой элемент (отличный от хедера или футера). Сейчас вижу ваш ответ с реализацией такого способа решения - классно, оказалось очень познавательно ? Спасибо!

И вам спасибо за статью! Пишите еще!

Крутенько, только недавно задавался вопросом, сделать на подобии

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории