簡體   English   中英

當用戶更改方向時,如何從 0 開始重新計算 yOffSet,用於 SwiftUI 中滾動視圖中的項目

[英]How do I recalculate the yOffSet starting from 0, for items in a scrollview in SwiftUI when the user changes orientation

我正在嘗試在底部創建一個帶有自定義導航器的滾動視圖。 當滾動視圖在其附近時,導航項應獲得背景。

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM