3

I would like to have something like List(selection: ) in LazyVStack.

The problem is that I don't know how to manage the content to split in each element that it contains.

What I've tried to do:

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

enter image description here

edit = .active

enter image description here


UPDATE


with Asperi's solution, I lose the propriety of LazyVStack, all the rows are loaded also if not displayed (and is also not scrollable:

enter image description here

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

2 Answers 2

2

You need to create custom handled containers for all variants of desired content types.

Below is a demo of possible direction on the example of following content support (by example of List)

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

Demo prepared and tested with Xcode 12 / iOS 14 (it is used some SwiftUI 2.0 features so if needed SwiftUI 1.0 support some more tuning will be needed)

demo

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")
                        }
                    }
                }
            }
        )
    }
}
Sign up to request clarification or add additional context in comments.

1 Comment

very clever solution. But check my update, with your solution, all the row are loaded and I lose the LazyVStack property.
0

Instead of creating custom LazyVStack I suggest to modify ContentView and pass bindings to it.

enter image description here

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

And then you can just wrap normal LazyVStack in ScrollView to achieve what you need.

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.