Можно ли реализовать функциональность .onDelete и .onMove для .listStyle с поддержкой Core Data (GroupedListStyle ()) в SwiftUI?

Я могу заставить работать плоский список с поддержкой Core Data (без модификатора .listStyle) с функциями удаления и перемещения.

Но когда я попытался сгруппировать список

}.listStyle(GroupedListStyle())

колеса концептуально отваливаются. Параметр модификатора onDelete имеет сигнатуру функции IndexSet? -> Пустота. Поэтому я не могу передать объект, который нужно удалить.

onMove - это, по сути, та же проблема, только хуже. Оба модификатора полагаются на источник данных, который, как предполагается, представляет собой плоский массив последовательных значений, доступ к которому может получить подписка IndexSet. Но я не могу придумать, как построить сгруппированный список, используя плоский источник данных.

введите здесь описание изображения

Мое тело просмотра выглядит так:

//I'm building the list using two independent arrays. This makes onDelete impossible to implement as recommended

ForEach(folders, id: \.self) { folder in 
                    Section(header: Text(folder.title) ) {
                        ForEach(self.allProjects.filter{$0.folder == folder}, id: \.self){ project in
                            Text(project.title)
//this modifier is where the confusion starts:
                        }.onDelete(perform: self.delete) 
                    }
                }

            }.listStyle(GroupedListStyle())
    func delete (at offsets: IndexSet) {
        //        ??.remove(atOffsets: offsets)
        //Since I use two arrays to construct group list, I can't use generic remove at Offsets call. And I can't figure out a way to pass in the managed object.

    }

      func move (from source: IndexSet, to destination: Int) {
    ////same problem here. a grouped list has Dynamic Views produced by multiple arrays, instead of the single array the move function is looking for.
        } 

person Small Talk    schedule 19.08.2019    source источник


Ответы (3)


Разве вы не можете сохранить результат фильтра и передать его внутри .onDelete в свой собственный метод удаления? Тогда удаление будет означать удаление элементов внутри IndexSet. Возможно ли перемещение между разделами? Или вы просто имеете в виду внутри каждой папки? Если только внутри каждой папки вы можете использовать один и тот же трюк, использовать сохраненные проекты и реализовать перемещение вручную, но вы определяете положение в CoreData.

Общая идея такова:

import SwiftUI

class FoldersStore: ObservableObject {
    @Published var folders: [MyFolder] = [

    ]

    @Published var allProjects: [Project] = [

    ]

    func delete(projects: [Project]) {

    }
    func move(projects: [Project], set: IndexSet, to: Int) {

    }
}

struct MyFolder: Identifiable {
    let id = UUID()
    var title: String
}

struct Project: Identifiable {
    let id = UUID()
    var title: String
    var folder: UUID
}

struct FoldersAndFilesView: View {
    var body: some View {
        FoldersAndFilesView_NeedsEnv().environmentObject(FoldersStore())
    }
}

struct FoldersAndFilesView_NeedsEnv: View {
    @EnvironmentObject var store: FoldersStore

    var body: some View {
        return ForEach(store.folders) { (folder: MyFolder) in
            Section(header: Text(folder.title) ) {
                FolderView(folder: folder)
            }
        }.listStyle(GroupedListStyle())
    }
}

struct FolderView: View {
    var folder: MyFolder
    @EnvironmentObject var store: FoldersStore

    func projects(for folder: MyFolder) -> [Project] {
        return self.store.allProjects.filter{ project in project.folder == folder.id}
    }

    var body: some View {
        let projects: [Project] = self.projects(for: folder)

        return ForEach(projects) { (project: Project) in
            Text(project.title)
        }.onDelete {
            self.store.delete(projects: $0.map{
                return projects[$0]
            })
        }.onMove {
            self.store.move(projects: projects, set: $0, to: $1)
        }
    }
}
person Fabian    schedule 19.08.2019
comment
Спасибо за быстрый ответ. Позвольте мне поиграть с этим. - person Small Talk; 19.08.2019
comment
Удачи. Единственный совет: переместите все внутри section в собственное представление, затем вы можете удерживать let projects = <filtering> на верхнем уровне var body: some View-method и получить к нему доступ внутри .onDelete и .onMove для использования и передачи. Для этого вам нужен доступ к хранилищу внутри подпредставления, хотя для вызова методов перемещения и удаления. - person Fabian; 19.08.2019
comment
Я добавил, как я думал, что это может сработать. Это исправит ситуацию, или я неправильно прочитал некоторые требования? : D .onDelete использует индексы, указывающие на предоставленный в настоящее время массив, поэтому он вычисляется при каждой перерисовке и всегда имеет проекты, на которые может ссылаться .onDelete, что в этом очень здорово. Таким образом, изоляция объектов - это просто отображение индексов во временный массив и передача проектов в хранилище для удаления. - person Fabian; 19.08.2019
comment
Приятно, что у вас все получилось! Единственная проблема в том, что он каждый раз повторно фильтрует проекты (или, возможно, только если они меняются, кто знает магию SwiftUI, но я сомневаюсь в этом), это то, что мне больше нравится в ответе Чака :-) - person Fabian; 21.08.2019
comment
После некоторых усилий я попробую другой подход. Данные, встроенные в каждый ForEach FolderView, прекрасно работают для предоставления объектов, необходимых для .onMove и .onDelete. Но добавление FolderView во внутренний цикл ForEach, похоже, мешает обновлению динамического представления SwiftUI, и все становится визуально ошибочно. Я пробовал перестроить с нуля, но не могу получить надежные обновления представления. Строка stackoverflow.com/questions/57627666/ Я собираюсь поиграть с подходом frc ниже и посмотреть, повезет ли мне больше. Вам предложили твердое решение. - person Small Talk; 24.08.2019
comment
Это решение абсолютно работает. Но это вызывает ошибку в Xcode 11, beta 6. Введение структуры FolderView во внутренний цикл ForEach прерывает обновления пользовательского интерфейса, когда пользователь в List EditMode = .active. Надеюсь, к тому времени, когда GM выйдет, эта ошибка будет исправлена, и это будет принятый ответ. - person Small Talk; 25.08.2019

Вы правы, что ключ к тому, что вы хотите, - это получить один массив объектов и соответствующим образом сгруппировать его. В вашем случае это ваши проекты. Вы не показываете свою схему CoreData, но я ожидаю, что у вас есть сущность «Проекты» и сущность «Папки» и связь «один ко многим» между ними. Ваша цель - создать запрос CoreData, который создает этот массив проектов и группирует их по папкам. Тогда реальный ключ - использовать NSFetchedResultsController CoreData для создания групп с помощью sectionNameKeyPath.

Для меня непрактично отправлять вам весь мой проект, поэтому я постараюсь дать вам достаточно частей моего рабочего кода, чтобы указать вам в правильном направлении. Когда у меня будет возможность, я добавлю эту концепцию в пример программы, которую я только что опубликовал на GitHub. https://github.com/Whiffer/SwiftUI-Core-Data-Test

В этом суть вашего списка:

@ObservedObject var dataSource =
        CoreDataDataSource<Project>(sortKey1: "folder.order",
                                              sortKey2: "order",
                                              sectionNameKeyPath: "folderName")

    var body: some View {

        List() {

            ForEach(self.dataSource.sections, id: \.name) { section in

                Section(header: Text(section.name.uppercased()))
                {
                    ForEach(self.dataSource.objects(forSection: section)) { project in

                        ListCell(project: project)
                    }
                }
            }
        }
        .listStyle(GroupedListStyle())
    }

Части CoreDataDataSource:

let frc = NSFetchedResultsController(
            fetchRequest: fetchRequest,
            managedObjectContext: McDataModel.stack.context,
            sectionNameKeyPath: sectionNameKeyPath,
            cacheName: nil)
frc.delegate = self

    public func performFetch() {

        do {
            try self.frc.performFetch()
        } catch {

            let nserror = error as NSError
            fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
        }
    }

    private var fetchedObjects: [T] {

        return frc.fetchedObjects ?? []
    }

    public var sections: [NSFetchedResultsSectionInfo] {

        self.performFetch()
        return self.frc.sections!
    }

    public func objects(forSection: NSFetchedResultsSectionInfo) -> [T] {

        return forSection.objects as! [T]
    }

    public func move(from source: IndexSet, to destination: Int) {

        self.reorder(from: source, to: destination, within: self.fetchedObjects)
    }
person Chuck H    schedule 20.08.2019
comment
1) Структура сущности, как вы описываете (один ко многим между папкой и проектами) 2) Использование FetchedResultsController с объектами, сгруппированными по параметру sectionNameKeyPath, является знакомым шаблоном. 3) Использование дженериков в вашем примере CoreDataDataSource - это тузы. Прикрепить FetchedResultsController к SwiftUI похоже на монстра Франкенштейна, однако frc специально создан для сгруппированных таблиц из Core Data. Спасибо и Фабиану, и Чаку Х за то, что заставили меня оторваться. - person Small Talk; 20.08.2019
comment
Я значительно улучшил свой пример проекта GitHub, чтобы показать вам, как реализовать .onDelete и .onMove при использовании вложенных циклов ForEach. Решение было не совсем простым. Посмотрите на AttributesGroupedView и мой значительно улучшенный CoreDataDataSource. - person Chuck H; 21.08.2019
comment
спасибо за ссылку на ваш GitHub CoreData + SwiftUI. Я ссылался на него для своего использования, и до сих пор он работает очень хорошо. - person Small Talk; 27.08.2019

Если вы хотите легко удалять вещи из секционированных (необязательно сгруппированных!) List, вам нужно воспользоваться преимуществами вложенности. Считайте, что у вас есть следующее:

List {
  ForEach(self.folders) { folder in
    Section(header: folder.title) {
      ForEach(folder.items) { project in
        ProjectCell(project)
      }
    }
  }
}

Теперь вы хотите настроить .onDelete. Итак, давайте увеличим масштаб объявления Section:

Section(header: Text(...)) {
  ...
}
.onDelete { deletions in
  // you have access to the current `Folder` at this level of nesting
  // this is confirmed to work with singular deletion, not multi-select deletion
  // I would hope that this actually gets called once per section that contains a deletion
  // but that is _not_ confirmed
  guard !deletions.isEmpty else { return }

  self.delete(deletions, in: folder)
}

func delete(_ indexes: IndexSet, in folder: Folder) {
  // you can now delete this bc you have your managed object type and indexes into the project structure
}
person Procrastin8    schedule 18.12.2019
comment
Спасибо, застрял на этом и временно переехал. Я еще раз хорошенько посмотрю, когда вернусь к этому. - person Small Talk; 22.12.2019