HStack 在滚动 SwiftUI 中仅移动一项

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

我正在尝试在 SwiftUI 中创建一个水平滚动视图,一次只能移动一个项目,并且我想保持窥视视图的比例(即滚动时项目保持可见的部分)。

如果可能,我想避免使用 GeometryReader,并且该解决方案应该支持 iOS 16 及更高版本(因此,我无法使用 .scrollTargetBehavior(.viewAligned) 或 .scrollTargetLayout(),因为这些功能仅在 iOS 17 及更高版本中可用)。

这是我编写的代码,仅适用于 iOS 17 及以上版本。如何修改它以使其兼容 iOS 16 并且仍然保持相同的布局逻辑而不破坏视图或功能?

let itemCount = 5 // Number of items

var body: some View {
    let screenWidth = UIScreen.main.bounds.width
    let itemWidth: CGFloat = screenWidth - (16 + 30 + 48)
    let spacingValue: CGFloat = 8 // Define your spacing value
    
    ZStack {
        RoundedRectangle(cornerRadius: 25)
            .fill(Color.black)
            .frame(width: screenWidth - 48, height: 400)
        
        ScrollView(.horizontal, showsIndicators: false) {
            if #available(iOS 17.0, *) {
                LazyHStack(spacing: spacingValue) {
                    ForEach(0..<itemCount, id: \.self) { i in
                        RoundedRectangle(cornerRadius: 25)
                            .fill(Color(hue: Double(i) * 10, saturation: 1, brightness: 1).gradient)
                            .frame(width: itemWidth, height: 200)
                    }
                }
                .padding(.horizontal)
                .scrollTargetLayout()
            } else {
                // Fallback on earlier versions
            }

        }
       
        .scrollTargetBehavior(.viewAligned)
        .safeAreaPadding(.horizontal, itemCount == 1 ? 8 : 0)
        .environment(\.layoutDirection, .rightToLeft)
        .frame(width: screenWidth - 48, height: 200)
        .padding(.top, 100)
    }
    .onAppear {
        // Print the item width and spacing value to the console
        print("Item width: \(itemWidth), screen Width: \(screenWidth), spacing: \(spacingValue)")
    }
}

}

**

  • 问题:

**

如何在不使用 iOS 17 特定方法的情况下实现一次仅移动一项的功能(同时保持窥视视图的比例)? 我可以使用哪些替代策略来在 iOS 16 中实现所需的行为,例如捕捉项目或限制滚动速度? 有没有办法在没有 .scrollTargetBehavior(.viewAligned) 和 .scrollTargetLayout() 的情况下保持视觉一致性和布局(包括安全区域和填充)? 附加背景:

我还尝试使用 UIScrollView.appearance().isPagingEnabled = true,但这会导致滚动视图行为不正确,跳过项目,有时会捕捉到每个项目的中间或末尾,而不是一次按比例移动一个项目。如何修复此行为,同时保持平滑一致的滚动?

如有任何帮助或建议,我们将不胜感激!

swiftui scrollview scroll-paging swiftui-zstack lazyhstack
1个回答
0
投票

如果您知道屏幕的宽度和项目的宽度,那么您可以使用

HStack
DragGesture
来实现自己的可滚动视口。

  • 在您的示例中,您是从屏幕宽度得出项目的宽度,因此我认为假设宽度已知是合理的。

  • 要查找屏幕宽度,最好使用

    GeometryReader
    而不是
    UIScreen.main
    UIScreen.main
    已弃用,并且不适用于 iPad 分屏。

这是示例的简化版本,以显示其工作原理。一些注意事项:

  • HStack
    的间距也需要知道。我想这没什么大不了的。

  • HStack
    添加了水平填充,以确保第一个和最后一个项目居中。

  • .fixedSize()
    应用于
    HStack
    ,以使其占用适合内容所需的所有宽度。然后应用一个框架来约束宽度并剪辑到该框架,以隐藏溢出。

  • 使用

    .offset
    将所选项目移动到可见位置。

  • 您最初将环境值

    layoutDirection
    设置为
    .rightToLeft
    (这让我非常困惑,直到我注意到!)。如果您想这样做,则需要反向解释拖动手势。

struct ContentView: View {
    let itemCount = 5 // Number of items
    let spacingValue: CGFloat = 8 // Define your spacing value
    let sideMargin: CGFloat = 24
    @State private var selectedIndex: Int?
    @GestureState private var dragOffset = CGFloat.zero

    var body: some View {
        GeometryReader { proxy in
            let screenWidth = proxy.size.width
            let itemWidth: CGFloat = screenWidth - (16 + 30 + 48)
            HStack(spacing: spacingValue) {
                ForEach(0..<itemCount, id: \.self) { i in
                    RoundedRectangle(cornerRadius: 25)
                        .fill(Color(hue: Double(i) * 0.1, saturation: 1, brightness: 1).gradient)
                        .overlay {
                            Text("\(i)")
                                .font(.largeTitle)
                                .foregroundStyle(.white)
                        }
                        .frame(width: itemWidth, height: 200)
                }
            }
            .padding(.horizontal, ((screenWidth - itemWidth) / 2) - sideMargin)
            .fixedSize()
            .offset(x: -CGFloat(selectedIndex ?? 0) * (itemWidth + spacingValue))
            .offset(x: dragOffset)
            .frame(width: screenWidth - (2 * sideMargin), alignment: .leading)
            .clipped()
            .animation(.easeInOut, value: selectedIndex)
            .animation(.easeInOut(duration: 0.1), value: dragOffset)
            .gesture(
                DragGesture(minimumDistance: 0)
                    .updating($dragOffset) { val, state, trans in
                        state = val.translation.width
                    }
                    .onEnded { val in
                        let dx = val.translation.width
                        if dx > 0 {
                            selectedIndex = max(0, (selectedIndex ?? 0) - 1)
                        } else if dx < 0 {
                            selectedIndex = min(itemCount - 1, (selectedIndex ?? 0) + 1)
                        }
                    }
            )
            .frame(maxHeight: .infinity)
            .background {
                RoundedRectangle(cornerRadius: 25)
                    .fill(Color.black)
                    .frame(maxWidth: .infinity)
            }
            .padding(.horizontal, sideMargin)
        }
        .frame(height: 400)
        // .environment(\.layoutDirection, .rightToLeft)
    }
}

Animation

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