I'm trying to do a very simple drag and drop list that is animated (Not a classic List(), custom). That means the user can drag around the items in the list. However, I seem to have a problem with animations when accessing them through two arrays and a calculated index (see DropDelegate structs at the end of question and link to video: https://i.sstatic.net/ABHwI.jpg). To explain what I mean, here is the Model and ViewModel of an example:
struct Shelf: Identifiable {
var books: [Book]
var id: Int
}
struct Book: Identifiable {
var name: String
var id: Int
}
class ViewModel: ObservableObject {
@Published var shelves: [Shelf]
@Published var currentShelf: Shelf?
@Published var currentBook: Book?
init() {
let bookArray = [Book(name: "Romance", id: 1), Book(name: "Western", id: 2), Book(name: "Hemingway", id: 3), Book(name: "Cars", id: 4)]
shelves = [Shelf(books: bookArray, id: 1), Shelf(books: bookArray, id: 2), Shelf(books: bookArray, id: 3), Shelf(books: bookArray, id: 4)]
}
}
There are two views, one renders a list from an array of Shelf which is a variable of the ViewModel, the other a list from an array of Book which is a variable of each Shelf struct, as can be seen from the model. The two Views which use the animations:
struct ContentView: View {
@EnvironmentObject var viewModel: ViewModel
var body: some View {
NavigationView {
ScrollView {
VStack {
ForEach($viewModel.shelves) { $shelf in
NavigationLink(destination: ShelfDetailView(shelf: $shelf)) {
Text("Shelf \(shelf.id)")
.font(.headline)
.background(Color.mint)
.cornerRadius(12)
.padding()
.onDrag({
viewModel.currentShelf = shelf
return NSItemProvider(contentsOf: URL(string: "\(shelf.id)")!)! // <--- ??
})
.onDrop(of: [.url], delegate: ShelfDropViewDelegate(shelf: shelf, viewModel: viewModel))
}
}
}
}
}
}
}
struct ShelfDetailView: View {
@Binding var shelf: Shelf
@EnvironmentObject var viewModel: ViewModel
var body: some View {
ScrollView {
VStack {
ForEach(shelf.books) { book in
Text(book.name)
.font(.headline)
.background(Color.indigo)
.cornerRadius(12)
.padding()
.onDrag({
viewModel.currentBook = book
return NSItemProvider(contentsOf: URL(string: "\(book.id)")!)!
})
.onDrop(of: [.url], delegate: BookDropViewDelegate(book: book, viewModel: viewModel, shelfId: shelf.id))
}
}
}
}
}
Now, each view has its separate DropDelegate. In both delegates, only the dropEntered function differs, ShelfDropViewDelegate accesses only one array whilst BookDropViewDelegate accesses two and as such uses two indexes. Yet this causes that the former has smooth animations, whilst the latter has no animation and only reflects changes. Can someone explain how this works exactly? Much thanks. The code I posted is all that is required for a MRE
struct ShelfDropViewDelegate: DropDelegate {
var shelf: Shelf
var viewModel: ViewModel
func performDrop(info: DropInfo) -> Bool {
return true
}
func dropEntered(info: DropInfo) {
let fromIndex = viewModel.shelves.firstIndex { (shelf) -> Bool in
return shelf.id == viewModel.currentShelf?.id
} ?? 0
let toIndex = viewModel.shelves.firstIndex { (shelf) -> Bool in
return shelf.id == self.shelf.id
} ?? 0
if fromIndex != toIndex {
withAnimation (.default) {
let fromShelf = viewModel.shelves[fromIndex]
viewModel.shelves[fromIndex] = viewModel.shelves[toIndex]
viewModel.shelves[toIndex] = fromShelf
}
}
}
func dropUpdated(info: DropInfo) -> DropProposal? {
return DropProposal(operation: .move)
}
}
struct BookDropViewDelegate: DropDelegate {
var book: Book
var viewModel: ViewModel
var shelfId: Shelf.ID
var shelfIndex: Array<Shelf>.Index
init(book: Book, viewModel: ViewModel, shelfId: Shelf.ID) {
self.book = book
self.viewModel = viewModel
self.shelfId = shelfId
shelfIndex = viewModel.shelves.firstIndex { (shelf) -> Bool in
return shelfId == shelf.id
} ?? 0
}
func performDrop(info: DropInfo) -> Bool {
return true
}
func dropEntered(info: DropInfo) {
let fromIndex = viewModel.shelves[shelfIndex].books.firstIndex { (book) -> Bool in
return book.id == viewModel.currentBook?.id
} ?? 0
let toIndex = viewModel.shelves[shelfIndex].books.firstIndex { (book) -> Bool in
return book.id == self.book.id
} ?? 0
if fromIndex != toIndex {
withAnimation (.default) {
let fromShelf = viewModel.shelves[shelfIndex].books[fromIndex]
viewModel.shelves[shelfIndex].books[fromIndex] = viewModel.shelves[shelfIndex].books[toIndex]
viewModel.shelves[shelfIndex].books[toIndex] = fromShelf
}
}
}
func dropUpdated(info: DropInfo) -> DropProposal? {
return DropProposal(operation: .move)
}
}