当用户更改方向时,我如何从0开始重新计算yoffset

发布于 2025-02-11 02:31:15 字数 5088 浏览 4 评论 0原文

我正在尝试在底部使用自定义导航器创建滚动浏览量。当滚动浏览在其内部近距离时,导航项目应具有背景。

我已经使用了scrollViewReader将项目和yoffset保存在数组中。然后,我将yoffsetscrollvaluepreferencekeky放在滚动浏览中的整个hstack上。最后,我会听YoffSetsCrollValuePreferenceKey的值,并且是否确实将新值与数组中的项目的值进行了比较。如果该值存在,则将所选项目设置为属于该偏移的项目。

当我更改设备的方向时,我的问题就会发生。例如,如果用户滚动,请说例如在列表的中间说,将从该位置计算项目的位置。这意味着它不是第一个物品为0,而是具有负数(基于用户滚动多远的数字)。我需要根据scrollview中的位置来计算的项目,而不是基于用户在滚动浏览中的位置。有办法这样做吗?

我已经尝试在更改方向后将滚动浏览滚动回到第一项。该解决方案不起作用,因为在方向发生变化时,项目的位置发生了变化,这给出了其他一些错误的行为。我已经用完了所有的想法,所以希望有人可以在这里帮助我。 :)

我将提供简单的代码,在下面隔离问题的情况下!如果您还有更多问题或需要我提供更多信息,请告诉我。要陷入问题,请运行代码,请将列表滚动到中间(或除起始位置以外的其他地方)更改设备的方向,然后滚动到其他部分。现在,ScrollView下的导航视图与屏幕上的视图不同步。

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()
    }
}

I'm trying to create a scrollview with a custom navigator in the bottom. The navigation item should get a background when the scrollview is inside it's proximity.

I've used a scrollviewReader to save the item and the yOffSet inside an array. Then I've given a YOffsetScrollValuePreferenceKey to the entire HStack inside the scrollview. Lastly I listen to if YOffsetScrollValuePreferenceKey value changes, and if it does compare the new value with the value of the items inside the array. If the value exists then I set the selected item to the item belonging to that offset.

My problem occurs when I change the orientation of the device. If a user has scrolled let's say for example to the middle of the list, the position of the items will be calculated from that position. This means that instead of the first item having a yOffSet of 0, it now has a negative number (based on how far the user has scrolled). I need the items yOffSet to be calculated based on their position inside the scrollview, not based on where the user is inside the scrollview. Is there a way to do this?

I've already tried to let the scrollview scroll back to the first item upon change of orientation. This solution did not work, as the position of the item changed when the orientation changed, and this gave some other buggy behaviour. I've ran all out of ideas so hoped someone could help me here. :)

I will provide simple code where the problem is isolated below! If you have any more questions or need me to provide more information please let me know. To run into the problem run the code, scroll the list to the middle (or anywhere else apart from the starting position) change the orientation of the device, and scroll to a different section. The navigation view under the scrollview now does not run in synch with which view is on screen.

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()
    }
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

傾旎 2025-02-18 02:31:16

可以使用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())
    }
}

This can be done in a much easier way using anchorPreference, then you only need one preference and there is no need to use a coordinateSpace.

I've updated your code to use this instead. It solves the issue you are having, just note that it will not re-center the currently scrolled to item on rotation, but it does change the selection if it changed because of rotation.

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())
    }
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文