Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI Lists treat DisclosureGroup content as list items

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:

Demo

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?

like image 441
Nicolas Gimelli Avatar asked Dec 22 '25 02:12

Nicolas Gimelli


1 Answers

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)

custom view

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()
}
like image 173
J W Avatar answered Dec 25 '25 17:12

J W



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!