我正在尝试创建一个底部带有自定义导航器的滚动视图。当滚动视图在其附近时,导航项应该有一个背景。
我使用了scrollviewReader将项目和yOffSet保存在数组中。然后,我在滚动视图中为整个HStack提供了一个YOffsetScrollValuePreferenceKey。最后,我将听取YOffsetScrollValuePreferenceKey值是否发生了更改,以及它是否将新值与数组中项目的值进行了比较。如果该值存在,则我将所选项目设置为属于该偏移的项目。
当我更改设备的方向时,就会出现问题。如果用户已经滚动到列表的中间,例如,项目的位置将从该位置计算出来。这意味着第一个项目的yOffSet不是0,而是负数(基于用户滚动的距离)。我需要根据项目yOffSet在滚动视图中的位置来计算,而不是根据用户在滚动视图内的位置。有办法做到这一点吗?
我已经尝试过让滚动视图在改变方向时滚动回第一个项目。这个解决方案不起作用,因为当方向改变时,项目的位置也会改变,这会导致其他一些错误行为。我已经没有什么想法了,所以希望有人能在这里帮助我
我将在下面隔离问题的地方提供简单的代码!如果您还有任何问题或需要我提供更多信息,请告诉我。要解决问题,请运行代码,将列表滚动到中间(或起始位置以外的任何位置),更改设备的方向,然后滚动到不同的部分。滚动视图下的导航视图现在与屏幕上的视图不同步。
import SwiftUI
struct ContentView: View {
@State private var numberPreferenceKeys = [NumberPreferenceKey]()
@State var selectedNumber = 0
@State var rectangleHeight: [CGFloat] = [
CGFloat.random(in: 500..<2000),
CGFloat.random(in: 500..<2000),
CGFloat.random(in: 500..<2000),
CGFloat.random(in: 500..<2000),
CGFloat.random(in: 500..<2000)
]
let colors: [Color] = [Color.blue, Color.red, Color.green, Color.gray, Color.purple]
var body: some View {
VStack {
ScrollViewReader { reader in
ScrollView(.horizontal) {
HStack {
ForEach(0..<5) { number in
Rectangle()
.fill(colors[number])
.frame(width: rectangleHeight[number], height: 200)
.id("(number)")
.background(
GeometryReader { proxy in
if numberPreferenceKeys.count < 6{
var yOffSet = proxy.frame(in: .named("number")).minX
let _ = DispatchQueue.main.async {
var yPositiveOffset: CGFloat = 0
if number == 1, yOffSet < 0 {
yPositiveOffset = abs(yOffSet)
}
numberPreferenceKeys.append(
NumberPreferenceKey(
number: number,
yOffset: yOffSet + yPositiveOffset
)
)
}
}
Color.clear
}
)
}
}
.background(GeometryReader {
Color.clear.preference(
key: YOffsetScrollValuePreferenceKey.self,
value: -$0.frame(in: .named("number")).origin.x
)
})
.onPreferenceChange(YOffsetScrollValuePreferenceKey.self) { viewYOffsetKey in
DispatchQueue.main.async {
for numberPreferenceKey in numberPreferenceKeys where numberPreferenceKey.yOffset <= viewYOffsetKey {
selectedNumber = numberPreferenceKey.number
}
}
}
}
HStack {
ForEach(0..<5) { number in
ZStack {
if number == selectedNumber {
Rectangle()
.frame(width: 30, height: 30)
}
Rectangle()
.fill(colors[number])
.frame(width: 25, height: 25)
.onTapGesture {
withAnimation {
reader.scrollTo("(number)")
}
}
}
}
}
}
.coordinateSpace(name: "number")
}
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
numberPreferenceKeys = []
}
}
}
struct NumberPreferenceKey {
let number: Int
let yOffset: CGFloat
}
struct YOffsetScrollValuePreferenceKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue = CGFloat.zero
static func reduce(value: inout Value, nextValue: () -> Value) {
value += nextValue()
}
}
这可以使用anchorPreference
以更简单的方式完成,然后您只需要一个首选项,而不需要使用coordinateSpace
。
我已经更新了您的代码以使用此代码。它解决了您遇到的问题,只需注意,它不会在旋转时将当前滚动到的项目重新居中,但如果选择因旋转而更改,它确实会更改选择。
struct ContentView: View {
// MARK: - Private Vars
@State private var selectedNumber = 0
private let rectangleHeight: [CGFloat] = [
CGFloat.random(in: 500..<2000),
CGFloat.random(in: 500..<2000),
CGFloat.random(in: 500..<2000),
CGFloat.random(in: 500..<2000),
CGFloat.random(in: 500..<2000)
]
private let colors: [Color] = [
.blue,
.red,
.green,
.gray,
.purple
]
// MARK: - View
var body: some View {
ScrollViewReader { reader in
ScrollView(.horizontal) {
HStack {
ForEach(0..<5) { index in
item(atIndex: index)
}
}
}
.overlayPreferenceValue(ItemLeadingPreferenceKey.self) { anchors in
GeometryReader { proxy in
// Find the index of the last anchor for which the x value is <= 0
// (indicating that it scrolled passed the beginning of the view)
let index = anchors.lastIndex(where: { proxy[$0].x <= 0 }) ?? 0
// Use this index to update the selected number
Color.clear
.onAppear {
selectedNumber = index
}
.onChange(of: index) {
selectedNumber = $0
}
}
.ignoresSafeArea()
}
footer(for: reader)
}
}
// MARK: - Utils
@ViewBuilder
private func item(atIndex index: Int) -> some View {
Rectangle()
.fill(colors[index])
.frame(width: rectangleHeight[index], height: 200)
.id(index)
.background {
GeometryReader { proxy in
// Use the leading of this view for offset calculation
// You can also use center if that makes more sense for selection determination
Color.clear
.anchorPreference(key: ItemLeadingPreferenceKey.self, value: .leading) { [$0] }
}
}
}
@ViewBuilder
private func footer(for proxy: ScrollViewProxy) -> some View {
HStack {
ForEach(0..<5) { index in
ZStack {
if index == selectedNumber {
Rectangle()
.frame(width: 30, height: 30)
}
Rectangle()
.fill(colors[index])
.frame(width: 25, height: 25)
.onTapGesture {
withAnimation {
proxy.scrollTo(index, anchor: .leading)
}
}
}
}
}
}
}
struct ItemLeadingPreferenceKey: PreferenceKey {
static let defaultValue: [Anchor<CGPoint>] = []
static func reduce(value: inout [Anchor<CGPoint>], nextValue: () -> [Anchor<CGPoint>]) {
value.append(contentsOf: nextValue())
}
}