如何捕捉 SwiftUI ScrollView 中的每隔一个内容项?

问题描述 投票:0回答:1

背景

在 WWDC 2023 上,Apple 为 ScrollViews 引入了新的 API,让我们可以轻松地将视图对齐到位。当用户在滚动(拖动)手势后抬起手指时,以下代码示例将

Rectangle
捕捉到位。

struct ContentView: View {
    var body: some View {
        ScrollView {
            LazyVStack(spacing: 8) {
                ForEach(1..<100) { number in
                    Rectangle()
                        .fill(.mint)
                        .frame(height: 200)
                }
            }
            .scrollTargetLayout()
        }
        .scrollTargetBehavior(.viewAligned)
    }
}

问题

现在,如果我只想捕捉每一秒

Rectangle
并滚动其他秒怎么办?或者更一般地说,我如何准确控制
ScrollView
应捕捉到哪些项目(子视图)以及捕捉时应忽略哪些项目?

这似乎是一件非常微不足道的事情,但我不知道如何实现这一点。

提示?

scrollTargetLayout(isEnabled:)
修饰符的文档指出:

滚动目标布局可以方便地将

View/scrollTarget(isEnabled:)
修饰符应用于布局中的每个视图。

这告诉我,还有另一个名为

scrollTarget(isEnabled:)
的修改器,我可以将其附加到各个项目以精细地启用或禁用捕捉。然而,这个修饰符似乎并不存在。 🤷u200d♂️

文档根本就是错误的还是我误解了它?我怎样才能完成这项工作(不回退到 UIKit)?

swiftui layout swiftui-scrollview
1个回答
0
投票

事实证明,使用新的 iOS 17 ScrollViews API 可以做到这一点。为此,您可以在滚动视图上使用新的

.scrollPosition(id: Hashable)
修饰符。我对此感到有点乐趣。这是我到目前为止所取得的成就:

这是我使用的代码。要使其正常工作,您将需要 8 张名为“Pic (index)”的图像。

    struct ScrollTransition: View {
    
    // MARK: - UI
    @State private var pickerType: TripPicker = .normal
    @State private var activeID: Int?
    
    var body: some View {
        VStack {
            Picker("", selection: $pickerType) {
                ForEach(TripPicker.allCases, id: \.rawValue) {
                    Text($0.rawValue)
                        .tag($0)
                } //: LOOP TRIP PICKER
            } //: PICKER
            .pickerStyle(.segmented)
            .padding()
            
            Button("Move of two") {
                withAnimation {
                    if let activeID, (activeID + 2) < 9 {
                        self.activeID! += 2
                    } else {
                        self.activeID = 1
                    }
                    print("Index \(self.activeID ?? 0)")
                }
            }
            
            Text("Active ID: \(self.activeID ?? 0)")
                .padding(.top, 20)
            
            Spacer(minLength: 0)
            
            GeometryReader {
                
                let size = $0.size
                let padding = (size.width - 70) / 2
                
                /// Circular Slider
                ScrollView(.horizontal) {
                    HStack(spacing: 35) {
                        ForEach(1...8, id: \.self) { index in
                            Image("Pic \(index)")
                                .resizable()
                                .scaledToFill()
                                .frame(width: 70, height: 70)
                                .clipShape(.circle)
                                .shadow(color: .black.opacity(0.15), radius: 5, x: 5, y: 5)
                                .visualEffect { view, proxy in
                                    view
                                        .offset(y: offset(proxy))
                                        .offset(y: scale(proxy) * 15)
                                }
                                .scrollTransition(.interactive, axis: .horizontal) {view, phase in
                                    view
                                        //.offset(y: phase.isIdentity && activeID == index ? 15 : 0)
                                        .scaleEffect(phase.isIdentity && activeID == index && pickerType == .scaled ? 1.5 : 1, anchor: .bottom)
                                }
                                
                            
                        } //: LOOP IMAGES
                    } //: HSTACK
                    .frame(height: size.height)
                    .offset(y: -30)
                    .scrollTargetLayout()
                } //: SCROLL
                .background {
                    if pickerType == .normal {
                        Circle()
                            .fill(.white.shadow(.drop(color: .black.opacity(0.2), radius: 5)))
                            .frame(width: 85, height: 85)
                            .offset(y: -15)
                    }
                }
                .safeAreaPadding(.horizontal, padding) // <-- With this kind of padding the view's minX starts from 0
                .scrollIndicators(.hidden)
                // Snapping
                .scrollTargetBehavior(.viewAligned)
                .scrollPosition(id: $activeID)
                .frame(height: size.height)
                .onAppear {
                    // Place carousel at the middle item
                    activeID = 8/2
                }
                
            } //: GEOMETRY
            .frame(height: 200)
            
        } //: VSTACK
        .ignoresSafeArea(.container, edges: .bottom)
        
    }
    
    
    //MARK: - Functions
    
    func offset(_ proxy: GeometryProxy) -> CGFloat {
        let progress = progress(proxy)
        /// Simply moving view up/down based on progress
        return progress < 0 ? progress * -30 : progress * 30
    }
    
    func scale(_ proxy: GeometryProxy) -> CGFloat {
        let progress = min(max(progress(proxy), -1), 1)
        return progress < 0 ? 1 + progress : 1 - progress
    }
    
    func progress(_ proxy: GeometryProxy) -> CGFloat {
        let viewWidth = proxy.size.width
        let minX = (proxy.bounds(of: .scrollView)?.minX ?? 0)
        return minX / viewWidth
    }
    
}


enum TripPicker: String, CaseIterable {
    case scaled = "Scaled"
    case normal = "Normal"
}

也许所有这些代码都有点矫枉过正,但我对 SwiftUI 新功能很感兴趣。顺便说一句,修改 activeID 整数后,滚动视图将自动导航到该索引处的元素。希望我能帮到你,请告诉我!

© www.soinside.com 2019 - 2024. All rights reserved.