SwiftUI - 如何制作可选择的可滚动列表?

SwiftUI - How to make a selectable scrollable list?

我正在尝试制作这样的东西:

滚动之前

滚动后

本质上:

  1. 项目排列在可滚动列表中
  2. 位于中间的项目是选中的项目
  3. 所选项目的属性可访问(通过更新@State 变量)
  4. 理想情况下,滚动手势是“粘性的”。例如,滚动后最靠近中心的项目重新调整其位置到中心,这样整体排列是一样的。

我尝试过使用 ScrollView,但我不知道如何实现 2 和 4。我想这个想法与 Picker 非常相似?

我已经坚持了一段时间。任何建议将不胜感激。提前致谢!

您可以检测到 select 居中的项目的位置。

// Data model
struct Item: Identifiable {
    let id = UUID()
    
    var value: Int
    // Other properties...
    var loc: CGRect = .zero
}

struct ContentView: View {
    @State private var ruler: CGFloat!
    
    @State private var items = (0..<10).map { Item(value: [=10=]) }
    @State private var centredItem: Item!
    
    var body: some View {
        HStack {
            if let item = centredItem {
                Text("\(item.value)")
            }
            
            HStack(spacing: -6) {
                Rectangle()
                    .frame(height: 1)
                    .measureLoc { loc in
                        ruler = (loc.minY + loc.maxY) / 2
                    }
                
                Image(systemName: "powerplug.fill")
                
                ScrollView(.vertical, showsIndicators: false) {
                    VStack(spacing: 0) {
                        ForEach($items) { $item in
                            Text("\(item.value)")
                                .padding()
                                .frame(width: 80, height: 80)
                                .background(centredItem != nil &&
                                            centredItem.id == item.id ? .yellow : .white)
                                .border(.secondary)
                                .measureLoc { loc in
                                    item.loc = loc
                                    
                                    if let ruler = ruler {
                                        if item.loc.maxY >= ruler && item.loc.minY <= ruler {
                                            withAnimation(.easeOut) {
                                                centredItem = item
                                            }
                                        }
                                        
                                        // Move outsides
                                        if ruler <= items.first!.loc.minY ||
                                            ruler >= items.last!.loc.maxY {
                                            withAnimation(.easeOut) {
                                                centredItem = nil
                                            }
                                        }
                                    }
                                }
                        }
                    }
                    // Extra space above and below
                    .padding(.vertical, ruler)
                }
            }
        }
        .padding()
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

检测位置:

struct LocKey: PreferenceKey {
    static var defaultValue: CGRect = .zero
    
    static func reduce(value: inout CGRect, nextValue: () -> CGRect) {}
}

extension View {
    func measureLoc(_ perform: @escaping (CGRect) ->()) -> some View {
        overlay(GeometryReader { geo in
            Color.clear
                .preference(key: LocKey.self, value: geo.frame(in: .global))
        }.onPreferenceChange(LocKey.self, perform: perform))
    }
}