SwiftUI 如何创建选择列表的 LazyVStack

Posted

技术标签:

【中文标题】SwiftUI 如何创建选择列表的 LazyVStack【英文标题】:SwiftUI How to create LazyVStack with selection as List 【发布时间】:2020-10-24 11:01:39 【问题描述】:

我想在LazyVStack 中有类似List(selection: ) 的东西。

问题是我不知道如何管理内容以拆分它包含的每个元素。

我尝试做的事情:

public struct LazyVStackSelectionable<SelectionValue, Content> : View where SelectionValue : Hashable, Content : View 

let content: Content
var selection: Binding<Set<SelectionValue>>?

@Environment(\.editMode) var editMode

public init(selection: Binding<Set<SelectionValue>>?, @ViewBuilder content: () -> Content) 
    self.content = content()
    self.selection = selection


public var body: some View 
    
    if self.editMode?.wrappedValue == EditMode.active 
        HStack 
            content //here I would like to have something like ForEach (content, id:\.self)
            
            Button(action: 
                //add the UUID to the list of selected item
            ) 
                Image(systemName: "checkmark.circle.fill")
                //Image(systemName: selection?.wrappedValue.contains(<#T##member: Hashable##Hashable#>) ? "checkmark.circle.fill" : "circle")
            
        
        
    
    else 
        content
    
    




struct ListView: View 


@State private var editMode: EditMode = .inactive
@State private var selection = Set<UUID>()

@State private var allElements: [MyElement] = [MyElement(id: UUID(), text: "one"),
                                               MyElement(id: UUID(), text: "two" ),
                                               MyElement(id: UUID(), text: "tree" )
]

var body: some View 

    NavigationView 
        VStack 
            Divider()
            Text("LazyVStack")
                .foregroundColor(.red)
            LazyVStack 
                ForEach(allElements, id: \.self)  element in //section data
                    Text(element.text)
                
            
            Divider()
            Text("LazyVStackSelectionable")
                .foregroundColor(.red)
            LazyVStackSelectionable(selection: $selection) 
                ForEach(allElements, id: \.self)  element in //section data
                    Text(element.text)
                
            
            Divider()
        
        .environment(\.editMode, self.$editMode)
        .navigationBarTitle(Text("LIST"), displayMode: .inline)
        .navigationBarItems(//EDIT
            trailing:
            Group 
                HStack (spacing: 15) 
                    self.editButton
                    self.delInfoButton
                    .contentShape(Rectangle())
                
            
        )
    
    




//MARK: EDIT MODE
private func deleteItems() 
    
    DispatchQueue.global(qos: .userInteractive).async 
        Thread.current.name = #function

        selection.forEach idToRemove in
            if let index = allElements.firstIndex(where:  $0.id == idToRemove ) 
                allElements.remove(at: index)
            
        
    


private var editButton: some View 
    Button(action: 
        self.editMode.toggle()
        self.selection = Set<UUID>()
    ) 
        Text(self.editMode.title)
    


private var delInfoButton: some View 
            
    if editMode == .inactive 
        return Button(action: ) 
            Image(systemName: "square.and.arrow.up")
        
     else 
        return Button(action: deleteItems) 
            Image(systemName: "trash")
        
    






struct ListView_Previews: PreviewProvider 
    static var previews: some View 
        ListView()
    

edit = .inactive

edit = .active


更新


使用 Asperi 的解决方案,我失去了 LazyVStack 的适当性,如果不显示所有行也会被加载(并且也不能滚动:

struct SampleRow: View 
    let number: Int

    var body: some View 
        Text("Sel Row \(number)")
    

    init(_ number: Int) 
        print("Loading LazySampleRow row \(number)")
        self.number = number
    

struct LazySampleRow: View 
    let number: Int

    var body: some View 
        Text("LVS element \(number)")
    

    init(_ number: Int) 
        print("Loading LazyVStack row \(number)")
        self.number = number
    


var aLotOfElements: [MyElement] 
    var temp: [MyElement] = []
    for i in 1..<200 
        temp.append(MyElement(id: UUID(), number: i))
    
    return temp


struct ContentView: View 
    
    
    @State private var editMode: EditMode = .inactive
    @State private var selection = Set<UUID>()
    
    @State private var allElements: [MyElement] = aLotOfElements//[MyElement(id: UUID(), number: 1)]
    
    var body: some View 

        NavigationView 
            HStack 
                VStack 
                    Text("LazyVStack")
                    .foregroundColor(.red)
                    ScrollView 
                        LazyVStack (alignment: .leading) 
                            ForEach(allElements, id: \.self)  element in //section data
                                LazySampleRow(element.number)
                            
                        
                    
                
                
                Divider()
                VStack 
                    LazyVStack (alignment: .leading) 
                        Divider()
                        Text("LazyVStackSelectionable")
                            .foregroundColor(.red)
                        LazyVStackSelectionable(allElements, selection: $selection)  element in
                            SampleRow(element.number)
                        
                        Divider()
                    
                
            
            .environment(\.editMode, self.$editMode)
            .navigationBarTitle(Text("LIST"), displayMode: .inline)
            .navigationBarItems(//EDIT
                trailing:
                Group 
                    HStack (spacing: 15) 
                        self.editButton
                        self.delInfoButton
                        .contentShape(Rectangle())
                    
                
            )
        
        
    
    
   

    //MARK: EDIT MODE
    private func deleteItems() 
        
        DispatchQueue.global(qos: .userInteractive).async 
            Thread.current.name = #function
    
            selection.forEach idToRemove in
                if let index = allElements.firstIndex(where:  $0.id == idToRemove ) 
                    allElements.remove(at: index)
                
            
        
    
    
    private var editButton: some View 
        Button(action: 
            self.editMode.toggle()
            self.selection = Set<UUID>()
        ) 
            Text(self.editMode.title)
        
    

    private var delInfoButton: some View 
                
        if editMode == .inactive 
            return Button(action: ) 
                Image(systemName: "square.and.arrow.up")
            
         else 
            return Button(action: deleteItems) 
                Image(systemName: "trash")
            
        
    
   




struct ContentView_Previews: PreviewProvider 
    static var previews: some View 
        ContentView()
    



extension EditMode 
    var title: String 
        self == .active ? NSLocalizedString("done", comment: "") : NSLocalizedString("edit", comment: "")
    

    mutating func toggle() 
        self = self == .active ? .inactive : .active
    

【问题讨论】:

【参考方案1】:

您需要为所需内容类型的所有变体创建自定义处理的容器。

以下是以下内容支持示例的可能方向演示(以List为例)

LazyVStackSelectionable(allElements, selection: $selection)  element in
    Text(element.text)

使用 Xcode 12 / ios 14 准备和测试的演示(它使用了一些 SwiftUI 2.0 功能,因此如果需要 SwiftUI 1.0 支持,需要进行更多调整)

struct LazyVStackSelectionable<SelectionValue, Content> : View where SelectionValue : Hashable, Content : View 
    @Environment(\.editMode) var editMode
    
    private var selection: Binding<Set<SelectionValue>>?
    private var content: () -> Content
    private var editingView: AnyView?
    
    init(selection: Binding<Set<SelectionValue>>?, @ViewBuilder content: @escaping () -> Content)
    
        self.selection = selection
        self.content = content
    
    
    var body: some View 
        Group 
        if editingView != nil && self.editMode?.wrappedValue == .active 
            editingView!
         else 
            self.content()
        
    


extension LazyVStackSelectionable 
    init<Data, RowContent>(_ data: Data, selection: Binding<Set<SelectionValue>>?, @ViewBuilder rowContent: @escaping (Data.Element) -> RowContent) where Content == ForEach<Data, Data.Element.ID, HStack<RowContent>>, Data : RandomAccessCollection, RowContent : View, Data.Element : Identifiable, SelectionValue == Data.Element.ID
    
        self.init(selection: selection, content: 
            ForEach(data)  el in
                HStack 
                    rowContent(el)
                
            
        )
        editingView = AnyView(
            ForEach(data)  el in
                HStack 
                    rowContent(el)
                    if let selection = selection 
                        Button(action: 
                            if selection.wrappedValue.contains(el.id) 
                                selection.wrappedValue.remove(el.id)
                             else 
                                selection.wrappedValue.insert(el.id)
                            
                        ) 
                             Image(systemName: selection.wrappedValue.contains(el.id) ? "checkmark.circle.fill" : "circle")
                        
                    
                
            
        )
    

【讨论】:

非常聪明的解决方案。但是检查我的更新,使用您的解决方案,所有行都已加载,我丢失了 LazyVStack 属性。【参考方案2】:

我建议修改ContentView 并将绑定传递给它,而不是创建自定义LazyVStack

struct SampleRow: View 
    let element: MyElement
    let editMode: Binding<EditMode>
    let selection: Binding<Set<UUID>>?
    
    var body: some View 
        HStack 
            if editMode.wrappedValue == .active,
               let selection = selection 
                Button(action: 
                    if selection.wrappedValue.contains(element.id) 
                        selection.wrappedValue.remove(element.id)
                     else 
                        selection.wrappedValue.insert(element.id)
                    
                ) 
                     Image(systemName: selection.wrappedValue.contains(element.id) ? "checkmark.circle.fill" : "circle")
                
            
            Text("Sel Row \(element.number)")
        
    

    init(_ element: MyElement,
         editMode: Binding<EditMode>,
         selection: Binding<Set<UUID>>?) 
        print("Loading LazySampleRow row \(element.number)")
        self.editMode = editMode
        self.element = element
        self.selection = selection
    

然后你可以将普通的LazyVStack 包裹在ScrollView 中来实现你所需要的。

ScrollView 
    LazyVStack(alignment: .leading) 
        ForEach(allElements, id: \.self) 
            SampleRow($0,
                      editMode: $editMode,
                      selection: $selection)
        
    

【讨论】:

以上是关于SwiftUI 如何创建选择列表的 LazyVStack的主要内容,如果未能解决你的问题,请参考以下文章

如何在 SwiftUI 中删除列表选择指示器和分隔符?

如何在 SwiftUI 中为列表创建标题?

SwiftUI 如何在其中创建带有自定义 UIViews 的列表

在 SwiftUI 列表中选择多个项目

如何在 SwiftUI 列表行中独立选择复选框按钮和 NavigationLink 到另一个视图

SwiftUI - 如何基于@ObservedObject 为每个列表视图项创建编辑视图