我正在尝试在 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,但这会导致滚动视图行为不正确,跳过项目,有时会捕捉到每个项目的中间或末尾,而不是一次按比例移动一个项目。如何修复此行为,同时保持平滑一致的滚动?
如有任何帮助或建议,我们将不胜感激!
如果您知道屏幕的宽度和项目的宽度,那么您可以使用
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)
}
}