I'm working with Lists in SwiftUI, and the interface seems pretty intuitive. However, if I want more complex views in a list, and I decide to use DisclosureGroup, then the List treats the expanded content of the disclosure group as its own list cell. When implementing onMove and onDelete, this causes undefined behavior.
Consider the following view:
struct ContentView: View {
@State var items: [Int] = [0, 1, 2, 3]
var body: some View {
List {
ForEach($items, id: \.self) { $item in
DisclosureGroupView()
}
.onDelete { indexSet in
items.remove(atOffsets: indexSet)
}
.onMove { indices, newOffset in
items.move(fromOffsets: indices, toOffset: newOffset)
}
}
}
}
struct DisclosureGroupView: View {
var body: some View {
DisclosureGroup {
VStack(spacing: 8) {
Text("Test text")
Text("Test text")
Text("Test text")
}
} label: {
HStack {
Text("Tap to Expand")
Spacer()
Image(systemName: "ellipsis")
}
}
}
}
this leads to the following behavior when deleting/reordering:

You can see that I can attempt to individually delete/reorder only the expanded content of the disclosure group, which in my context does not make sense and leads to undefined behavior; the entirety of the group should be treated as a single element.
Assuming that DisclosureGroup is the only option I have for the complex view I wish to implement, how can I make it so that the entire group is treated as a cell, rather than the label and expanded content being treated as different cells?
The final proposal:
When using a custom view, use DisclosureGroup and replace List with SrcollView
Attention: The interface is not perfect yet. If you need to improve it, you can refer to it.
https://www.youtube.com/watch?v=K8VnH2eEnK4
I didn’t watch the video carefully, but the effect looks good. The source code costs 3 dollars.(o_o)

import SwiftUI
struct ContentView: View {
@State var items: [Item] = (0...5).map { Item(number: $0) }
var body: some View {
ScrollView {
VStack(spacing: 0) {
Divider()
ForEach(0 ..< items.count, id: \.self) { i in
ZStack(alignment: .trailing) {
CollapsibleView(item: $items[i])
.padding()
.background(Color.white)
.offset(x: items[i].offset)
.gesture(DragGesture()
.onChanged({ gesture in
if gesture.translation.width < 0 {
// Swipe left to show delete button
items[i].offset = gesture.translation.width
}
})
.onEnded({ _ in
if items[i].offset < -100 {
// Swipe far enough, delete the items[i]
items[i].offset = -1000
withAnimation {
delete(item: items[i])
}
} else {
// Not far enough, reset
items[i].offset = 0
}
})
)
if items[i].offset <= -100 {
Button(action: {
withAnimation {
delete(item: items[i])
}
}) {
Image(systemName: "trash.fill")
.foregroundColor(.white)
.padding()
.background(Color.red)
.cornerRadius(8)
}
.offset(x: -100) // Adjust the position as needed
}
}
Divider()
}
}
}
}
func delete(item: Item) {
items.removeAll { $0.id == item.id }
}
}
struct CollapsibleView: View {
@Binding var item: Item
var body: some View {
DisclosureGroup(
isExpanded: $item.isExpanded,
content: {
VStack(spacing: 8) {
Text("Detail for item \(item.number)")
Text("Another detail")
}
.foregroundColor(.black) // Set the color here
},
label: {
HStack {
Text("Item \(item.number)").foregroundColor(.black) // Set the color here
Spacer()
Image(systemName: "ellipsis").foregroundColor(.black) // Set the color here
}
}
)
.animation(.easeInOut, value: item.isExpanded)
.transition(.opacity)
}
}
struct Item: Identifiable {
let id = UUID()
var number: Int
var isExpanded: Bool = false
var offset: CGFloat = 0
}
#Preview {
ContentView()
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With