【问题标题】:SwiftUI - How to make a selectable scrollable list?SwiftUI - 如何制作可选择的可滚动列表?
【发布时间】:2021-09-19 08:12:20
【问题描述】:

我正在尝试做这样的事情:

滚动前

滚动后

基本上:

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

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

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

【问题讨论】:

  • 你看过ScrollViewReader吗?

标签: list swiftui scrollview selector picker


【解决方案1】:

您可以检测项目的位置以选择居中的项目。

// 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: $0) }
    @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))
    }
}

【讨论】:

  • 您好,感谢您的回复。仅从查看您的代码来看,这似乎就是我想要做的。但是,我复制并粘贴了您的代码,但出现了一些错误。具体来说,我在 ForEach 部分收到“无法声明名为 '$item' 的实体;'$' 前缀是为隐式综合声明保留的”错误。这是我的一些问题吗?谢谢!
  • 那个传递Bindings的方法仅限于iOS 15
  • @softandwet 是的,这是一种 SwiftUI 3 方式。在 SwiftUI 2 中,你可以写 ForEach(Array(items.enumerated()), id: \.offset) { index, item in ... } 并在想要更改值时使用 items[index]
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-03-26
  • 2019-11-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-05-27
  • 2021-05-18
相关资源
最近更新 更多