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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.