11

I have the following SwiftUI view:

struct ContentView: View {
    @State var model: Model
    
    var body: some View {
        ScrollView {
            LazyVGrid(columns: columns, spacing: 10) {
                ForEach(model.events, id: \.self) { event in
                    CardView(event: event)
                }
                .onMove { indices, newOffset in
                    model.events.move(fromOffsets: indices, toOffset: newOffset)
                }
            }
        }
    }
}

However, it doesn't appear that the onMove closure is executing. I believe this is because all gestures are only given to the ScrollView and so the inner views don't receive the gestures.

I tried converting this view to a List, however I don't want the row separators, which in iOS 14 I believe are impossible to hide.

So, I was wondering what I need to change to get this to allow the user to drag and drop CardViews to reorder them. Thanks!

Richard Robinson
  • 867
  • 1
  • 11
  • 38

3 Answers3

2

According to Apple reply in Drag and Drop to reorder Items using the new LazyGrids?

That's not built-today, though you can use the View.onDrag(_:) and View.onDrop(...) to add support for drag and drop manually.

Do file a feedback request if you'd like to see built-in reordering support. Thanks!

So the solution using .onDrag & .onDrop provided in my answer for actually duplicated question in https://stackoverflow.com/a/63438481/12299030

Main idea is to track drug & drop by grid card views and reorder model in drop delegate, so LazyVGrid animates subviews:

   LazyVGrid(columns: model.columns, spacing: 32) {
        ForEach(model.data) { d in
            GridItemView(d: d)
                .overlay(dragging?.id == d.id ? Color.white.opacity(0.8) : Color.clear)
                .onDrag {
                    self.dragging = d
                    return NSItemProvider(object: String(d.id) as NSString)
                }
                .onDrop(of: [UTType.text], delegate: DragRelocateDelegate(item: d, listData: $model.data, current: $dragging))
        }
    }.animation(.default, value: model.data)

...

    func dropEntered(info: DropInfo) {
        if item != current {
            let from = listData.firstIndex(of: current!)!
            let to = listData.firstIndex(of: item)!
            if listData[to].id != current!.id {
                listData.move(fromOffsets: IndexSet(integer: from),
                    toOffset: to > from ? to + 1 : to)
            }
        }
    }
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • Is there any way to get a release event i.e. user not dragging anymore? – Abhishek Thapliyal Jul 01 '22 at 05:08
  • @AbhishekThapliyal unfortunately there is not. But if your view is simple, you can make your background a drop target. Then in performDrop set self.dragging to nil. Also, to make sure it doesn't show the + beside the drag preview, return operation: .move in dropUpdated. Unfortunately, if the user drags to another view, then you won't ever know it has updated. – hayesk Nov 11 '22 at 14:11
0

As of iOS 15, .listRowSeparator(.hidden) is available.

To keep the user's order changes, an object-based property wrapper was used. In this example @StateObject was used instead of @State.

struct ContentView: View {
    @StateObject var model: Model = Model()
    
    var body: some View {
        List {
            ForEach(model.events, id: \.self) { event in
                CardView(event: event)
                    .listRowSeparator(.hidden)
            }
            .onMove { indices, newOffset in
                model.events.move(fromOffsets: indices, toOffset: newOffset)
            }
        }
    }
}

The result looks like this:

enter image description here

The following variation of the above uses .listStyle(.plain) and an EditButton() for dragging and dropping the rows more easily:

struct ContentView: View {
    @StateObject var model: Model = Model()
    
    var body: some View {
        VStack {
            EditButton()
            List {
                ForEach(model.events, id: \.self) { event in
                    CardView(event: event)
                        .listRowSeparator(.hidden)
                }
                .onMove { indices, newOffset in
                    model.events.move(fromOffsets: indices, toOffset: newOffset)
                }
            }
            .listStyle(.plain)
        }
    }
}

The result looks like this:

enter image description here

The following code was used with the above examples for testing:

class Model : ObservableObject {
    @Published var events: [String]
    init() { events = (1...10).map { "Event \($0)" }}
}


struct CardView: View {
    var event: String
    var body: some View {
        Text(event)
    }
}
Marcy
  • 4,611
  • 2
  • 34
  • 52
-10

You can use a List and hide the separators.

.onAppear {
 UITableView.appearance().separatorStyle = .none
}