#swift #xcode
Вопрос:
Как я могу правильно сортировать свои элементы и заголовки при перетаскивании их в Swift с использованием основных данных и получать правильный элемент при использовании пути к индексу?
Проблема, с которой я сталкиваюсь, заключается в том, что мой алгоритм сортировки неправильно извлекает правильные строки и разделы в моем представлении коллекции (я получаю ошибки индекса вне диапазона в строке с функцией stride). Я хотел бы иметь возможность перетаскивать строки и удалять их вместе с их порядком для сохранения, а также перетаскивать заголовки, чтобы изменить их порядок и сохранить их порядок.
Проблема, по-видимому, возникает при присвоении значения «верхней» и «нижней» переменным, а также при переходе к получению значения для свойства «Новый порядок» моей основной сущности данных. Похоже, что при попытке получить перетаскиваемый элемент с помощью sourceIndexPath.section и sourceIndexPath.item возникает проблема.
Посмотрите, где я пытаюсь заставить перетаскивание работать.
import UIKit
class DetailViewController: UIViewController {
var fromItem: Item
init(item: Item) {
self.fromItem = item
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var dataSourceSnapshot = NSDiffableDataSourceSnapshot<Header, ListData>()
var collectionView: UICollectionView!
var dataSource: UICollectionViewDiffableDataSource<Header, ListData>!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemGroupedBackground
var layoutConfig = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
layoutConfig.headerMode = .firstItemInSection
let listLayout = UICollectionViewCompositionalLayout.list(using: layoutConfig)
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: listLayout)
view.addSubview(collectionView)
collectionView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor, constant: 0.0),
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0.0),
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0.0),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0.0),
])
collectionView.dragDelegate = self
collectionView.dropDelegate = self
collectionView.dragInteractionEnabled = true
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addNewHeader))
let headers = fromItem.headers?.allObjects as? [Header] ?? []
print("headers here (headers)")
dataSourceSnapshot.appendSections(headers.sorted(by: {$0.order < $1.order}))
dataSource.apply(dataSourceSnapshot)
for header in headers.sorted(by: {$0.order < $1.order}) {
let children = header.children!.allObjects as? [Child] ?? []
let sortedChildren = children.sorted{$0.order > $1.order}
var dataSourceSnapshot = NSDiffableDataSourceSectionSnapshot<ListData>()
let headerItem = ListData.header(header)
dataSourceSnapshot.append([headerItem])
for child in sortedChildren {
dataSourceSnapshot.append([.child(child)], to: headerItem)
}
dataSourceSnapshot.expand([headerItem])
dataSource.apply(dataSourceSnapshot, to: header, animatingDifferences: false)
}
}
@objc func addNewHeader() {
var order: Double = 0
let header = fromItem.headers?.allObjects as? [Header] ?? []
if header.count > 0 {
order = header.last!.order 25
} else {
order = 100
}
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
let context = appDelegate.persistentContainer.viewContext
let newHeader = Header(context: context)
newHeader.name = "Header"
newHeader.id = UUID()
newHeader.order = order
newHeader.item = fromItem
var childOrder: Double = 0
let firstChild = header.first(where: {$0.id == newHeader.id })?.children?.allObjects as? [Child] ?? []
if header.count > 0 amp;amp; firstChild.count > 0 {
childOrder = firstChild.last!.order 25
} else {
childOrder = 100
}
var snapshot = NSDiffableDataSourceSectionSnapshot<ListData>()
dataSourceSnapshot.appendSections([newHeader])
let headerItem = ListData.header(newHeader)
snapshot.append([headerItem])
var childOrderIncrement = childOrder
for i in 1...4 {
let child = Child(context: context)
child.order = childOrderIncrement
child.name = "child (i)"
child.id = UUID()
child.header = newHeader
snapshot.append([.child(child)], to: headerItem)
childOrderIncrement = 25
}
snapshot.expand([headerItem])
dataSource.apply(snapshot, to: newHeader, animatingDifferences: false)
try? context.save()
print("Headers = (fromItem.headers?.allObjects as? [Header] ?? [])")
}
}
extension DetailViewController: UICollectionViewDragDelegate {
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
guard let item = dataSource.itemIdentifier(for: indexPath) else {
collectionView.deselectItem(at: indexPath, animated: true)
return [UIDragItem]()
}
switch item {
case .header(let header):
let itemProvider = NSItemProvider(object: header.objectID.uriRepresentation().absoluteURL as NSURL)
let dragItem = UIDragItem(itemProvider: itemProvider)
return [dragItem]
case .child(let child):
let itemProvider = NSItemProvider(object: NSString(string: child.id!.uuidString))
let dragItem = UIDragItem(itemProvider: itemProvider)
return [dragItem]
}
}
func DetailViewController(_ collectionView: UICollectionView, itemsForAddingTo session: UIDragSession, at indexPath: IndexPath, point: CGPoint) -> [UIDragItem] {
guard let item = dataSource.itemIdentifier(for: indexPath) else {
collectionView.deselectItem(at: indexPath, animated: true)
return [UIDragItem]()
}
switch item {
case .header(_):
return [UIDragItem]()
case .child(let child):
let itemProvider = NSItemProvider(object: NSString(string: child.id!.uuidString))
let dragItem = UIDragItem(itemProvider: itemProvider)
return [dragItem]
}
}
}
extension DetailViewController: UICollectionViewDropDelegate {
private func reorderItems(coordinator: UICollectionViewDropCoordinator, destinationIndexPath: IndexPath, collectionView: UICollectionView) {
let items = coordinator.items
if items.count == 1, let item = items.first, let sourceIndexPath = item.sourceIndexPath {
var destIndexPath = destinationIndexPath
if destIndexPath.item >= collectionView.numberOfItems(inSection: destIndexPath.section) {
destIndexPath.item = collectionView.numberOfItems(inSection: destIndexPath.section) - 1
}
guard let fromChild = dataSource.itemIdentifier(for: sourceIndexPath),
sourceIndexPath != destIndexPath else { return }
var snap = dataSource.snapshot()
let header = dataSource.sectionIdentifier(for: destIndexPath.section)!
let sourceHeader = dataSource.sectionIdentifier(for: sourceIndexPath.section)!
switch fromChild {
case .header(_):
print("cannot reorder headers here")
case .child(let fromChild):
snap.deleteItems([.child(fromChild)])
if let toChild = dataSource.itemIdentifier(for: destIndexPath) {
switch toChild {
case .header(_):
print("cannot reorder headers here")
case .child(let toChild):
let isAfter = destIndexPath.item > sourceIndexPath.item
if isAfter {
if header == sourceHeader {
snap.insertItems([.child(fromChild)], afterItem: .child(toChild))
} else {
snap.insertItems([.child(fromChild)], afterItem: .child(toChild))
}
} else {
snap.insertItems([.child(fromChild)], beforeItem: .child(toChild))
}
}
} else {
snap.appendItems([.child(fromChild)], toSection: header)
}
}
var upper: Double
var lower: Double
let headers = fromItem.headers?.allObjects as? [Header] ?? []
let children = headers[destIndexPath.section].children!.allObjects as? [Child] ?? []
let sortedChildren = children.sorted{$0.order > $1.order}
if destIndexPath.item == sortedChildren.count {
print("Appending to the end of the list")
lower = sortedChildren.last?.order ?? 0
upper = sortedChildren.last?.order ?? 0 100.0
} else if destIndexPath.item == 0 {
print("Inserting into the beginning")
lower = 0.0
upper = sortedChildren.first?.order ?? 100.0
} else {
print("Inserting into the middle of the list")
upper = sortedChildren[destIndexPath.item - 2].order
lower = sortedChildren[destIndexPath.item - 1].order
}
print(upper)
print(lower)
if header != sourceHeader {
sortedChildren[sourceIndexPath.item].header = header
}
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
appDelegate.saveContext()
dataSource.apply(snap, animatingDifferences: false)
for header in headers.sorted(by: {$0.order < $1.order}) {
let children = header.children!.allObjects as? [Child] ?? []
let sortedChildren = children.sorted{$0.order > $1.order}
var dataSourceSnapshot = NSDiffableDataSourceSectionSnapshot<ListData>()
let headerItem = ListData.header(header)
dataSourceSnapshot.append([headerItem])
for child in sortedChildren {
dataSourceSnapshot.append([.child(child)], to: headerItem)
}
dataSourceSnapshot.expand([headerItem])
dataSource.apply(dataSourceSnapshot, to: header, animatingDifferences: false)
}
}
}
private func reorderHeaders(coordinator: UICollectionViewDropCoordinator, destinationIndexPath: IndexPath, collectionView: UICollectionView) {
let items = coordinator.items
if items.count == 1, let item = items.first, let sourceIndexPath = item.sourceIndexPath {
guard let fromHeader = dataSource.sectionIdentifier(for: sourceIndexPath.section),
sourceIndexPath != destinationIndexPath else { return }
var snap = dataSource.snapshot()
let unsortedHeaders = fromItem.headers?.allObjects as? [Header] ?? []
let headers = unsortedHeaders.sorted{$0.order > $1.order}
let children = fromHeader.children!.allObjects as? [Child] ?? []
let sortedChildren = children.sorted{$0.order > $1.order}
for i in sortedChildren {
snap.deleteItems([.child(i)])
}
snap.deleteSections([fromHeader])
if let toHeader = dataSource.sectionIdentifier(for: destinationIndexPath.section) {
let isAfter = destinationIndexPath.section > sourceIndexPath.section
if isAfter {
snap.insertSections([fromHeader], afterSection: toHeader)
} else {
snap.insertSections([fromHeader], beforeSection: toHeader)
}
} else {
snap.appendSections([fromHeader])
}
var upper: Double
var lower: Double
if destinationIndexPath.item == headers.count {
print("Appending to the end of the list")
lower = headers.last!.order
upper = headers.last!.order 100.0
} else if destinationIndexPath.item == 0 {
print("Inserting into the beginning")
lower = 0.0
upper = headers.first?.order ?? 100.0
} else {
print("Inserting into the middle of the list")
upper = headers[destinationIndexPath.section - 1].order
lower = headers[destinationIndexPath.section].order
}
headers[sourceIndexPath.section].order = stride(from: lower, to: upper, by: (upper - lower) / Double(2)).map{ $0 }[1]
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
appDelegate.saveContext()
for header in headers.sorted(by: {$0.order < $1.order}) {
let children = header.children!.allObjects as? [Child] ?? []
let sortedChildren = children.sorted{$0.order > $1.order}
var dataSourceSnapshot = NSDiffableDataSourceSectionSnapshot<ListData>()
let headerItem = ListData.header(header)
dataSourceSnapshot.append([headerItem])
for child in sortedChildren {
dataSourceSnapshot.append([.child(child)], to: headerItem)
}
dataSourceSnapshot.expand([headerItem])
dataSource.apply(dataSourceSnapshot, to: header, animatingDifferences: false)
}
}
}
func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {
let destinationIndexPath: IndexPath
if let indexPath = coordinator.destinationIndexPath {
destinationIndexPath = indexPath
} else {
let section = collectionView.numberOfSections - 1
let row = collectionView.numberOfItems(inSection: section)
destinationIndexPath = IndexPath(item: row, section: section)
}
switch coordinator.proposal.operation {
case .move:
for item in coordinator.items {
if item.dragItem.localObject as! String == "child" {
coordinator.session.loadObjects(ofClass: NSString.self) { items in
DispatchQueue.main.async {
print("reordering the children")
self.reorderItems(coordinator: coordinator, destinationIndexPath: destinationIndexPath, collectionView: collectionView)
}
}
} else {
coordinator.session.loadObjects(ofClass: NSURL.self) { items in
DispatchQueue.main.async {
print("reordering the headers")
self.reorderHeaders(coordinator: coordinator, destinationIndexPath: destinationIndexPath, collectionView: collectionView)
}
}
}
}
break
case .copy:
return
default:
return
}
}
func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal {
return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
}
}
Моя Основная Модель Данных:
Если у вас есть еще какие-либо вопросы, пожалуйста, не стесняйтесь задавать их.
Если бы это было проще, я мог бы отправить вам весь свой проект Xcode.
Ответ №1:
В своих приложениях я делаю это следующим образом:
- получить текущий список объектов, упорядоченных по основной записи данных
- удалите объект с выражением из
UICollectionViewDragDelegate
(CollectionView(_ CollectionView: UICollectionView, элементы для начальной сессии: UIDragSession, в indexPath: indexPath)) - вставьте этот объект обратно в список в пункте назначения indexPath из
UICollectionViewDropDelegate
(CollectionView(_ CollectionView: UICollectionView, координатор выполнения: UICollectionViewDropCoordinator)) - обновите индексы значений основных данных с помощью обновленных индексов сортировки
Пример кода:
// MARK: - UICollectionViewDragDelegate
extension CombinedMainVC: UICollectionViewDragDelegate {
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
guard let cell = collectionView.cellForItem(at: indexPath) as? CalendarCC, let item = cell.calendar, let name = item.name else { return [UIDragItem]() }
startingIndexPath = indexPath
let itemProvider = NSItemProvider(object: name as NSString)
let dragItem = UIDragItem(itemProvider: itemProvider)
dragItem.localObject = item
return [dragItem]
}
}
// MARK: - UICollectionViewDropDelegate
extension CombinedMainVC: UICollectionViewDropDelegate {
func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal {
let forbidden = UICollectionViewDropProposal(operation: .forbidden)
guard destinationIndexPath?.section == startingIndexPath.section else { return forbidden }
return collectionView.hasActiveDrag ? UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath) : forbidden
}
func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {
guard let destinationIndexPath = coordinator.destinationIndexPath else { return }
var calendars = fetchedRC.fetchedObjects!
let list = calendars.remove(at: startingIndexPath.row)
calendars.insert(list, at: destinationIndexPath.row)
for cal in fetchedRC.fetchedObjects! {
let sortIndex = calendars.firstIndex(of: cal)!
cal.secondData = Int32(sortIndex)
}
updated = false
appDelegate.saveContext()
postNotification("dateChanged")
}
}
(принудительное разворачивание выполняется, потому что сеанс удаления не может быть запущен, если нет календарей)
После этого моя модель будет обновлена, и я смогу применить новый снимок к представлению коллекции. Я думаю, что вы должны упорядочить свои объекты в одном месте (например, при применении моментальных снимков), а в сеансе перетаскивания вам нужно просто изменить модель данных и не трогать снимок представления коллекции.
Вы могли бы использовать тот же подход для заголовков и дочерних элементов — разница только в списке объектов, которые вы будете использовать в пункте 1
Комментарии:
1. Спасибо тебе, Виктор. Не могли бы вы взглянуть на мой код выше, чтобы узнать, верна ли моя логика при добавлении в конец, вставке в середине или вставке в начале списка просмотра моей коллекции. Если это поможет, я мог бы отправить вам весь свой проект Xcode.
2. @Найтхаук Уверен
3. Спасибо, Виктор. Если вам нужна копия моего проекта Xcode, есть ли у вас электронное письмо, на которое я могу его отправить? Моя главная проблема, похоже, заключается в получении правильного элемента с использованием пути назначения и исходного индекса. Я не совсем понимаю, что я делаю не так.
4. mail@viktorgavrilov.com
5. только что отправил тебе электронное письмо.