Apakah mungkin untuk mengimplementasikan fungsionalitas.onDelete dan .onMove ke .listStyle(GroupedListStyle()) yang didukung Data Inti di SwiftUI?

Saya dapat membuat daftar datar yang didukung Data Inti berfungsi (tanpa pengubah .listStyle) dengan fungsi hapus dan pindah.

Tetapi ketika saya mencoba membuat daftar dikelompokkan

}.listStyle(GroupedListStyle())

roda jatuh secara konseptual. Parameter pengubah onDelete memiliki tanda tangan fungsi IndexSet? -> Batal. Jadi saya tidak bisa meneruskan objek yang akan dihapus.

onMove pada dasarnya adalah masalah yang sama, hanya saja lebih buruk. Kedua pengubah bergantung pada sumber data yang diasumsikan berupa array datar nilai berurutan yang dapat diakses oleh langganan IndexSet. Namun saya tidak dapat memikirkan cara membuat daftar yang dikelompokkan menggunakan sumber data datar.

masukkan deskripsi gambar di sini

Badan tampilan saya terlihat seperti ini:

//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 sumber


Jawaban (3)


Tidak bisakah Anda menyimpan hasil filter dan meneruskannya di dalam .onDelete ke metode penghapusan khusus Anda? Maka hapus berarti menghapus item di dalam IndexSet. Apakah perpindahan antar bagian mungkin dilakukan? Atau maksud Anda hanya di dalam setiap folder? Jika hanya di dalam setiap folder Anda dapat menggunakan trik yang sama, gunakan proyek yang disimpan dan terapkan pemindahan secara manual bagaimanapun Anda menentukan posisi di CoreData.

Ide umumnya adalah sebagai berikut:

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
Terima kasih atas respon cepatnya. Biarkan aku bermain-main dengannya. - person Small Talk; 19.08.2019
comment
Semoga beruntung. Satu tip: Pindahkan semua yang ada di dalam bagian ke dalam tampilannya sendiri, lalu Anda dapat menahan let projects = <filtering> di metode var body: some View tingkat atas dan mengaksesnya di dalam .onDelete dan .onMove untuk digunakan dan diteruskan. Untuk ini, Anda memerlukan akses toko di dalam subview untuk memanggil metode pindah dan hapus. - person Fabian; 19.08.2019
comment
Saya menambahkan bagaimana menurut saya ini bisa berhasil. Apakah itu memperbaiki masalah Anda, atau apakah saya salah membaca beberapa persyaratan? :D .onDelete menggunakan indeks yang menunjuk ke array yang saat ini disediakan, itulah sebabnya ia dihitung pada setiap gambar ulang dan selalu memiliki proyek yang dapat direferensikan oleh .onDelete, itulah hal yang menarik tentangnya. Mengisolasi objek kemudian hanya memetakan indeks ke array sementara dan memberikan proyek ke penyimpanan untuk dihapus. - person Fabian; 19.08.2019
comment
Senang itu berhasil untuk Anda! Satu-satunya masalah adalah ia menyaring ulang proyek setiap kali (atau mungkin hanya jika mereka berubah, siapa yang tahu keajaiban SwiftUI, tapi saya meragukannya), itulah yang lebih saya sukai dari jawaban Chuck :-) - person Fabian; 21.08.2019
comment
Setelah beberapa upaya, saya akan mencoba pendekatan lain. Data yang tertanam di setiap ForEach FolderView berfungsi dengan baik untuk menyediakan objek yang diperlukan untuk .onMove dan .onDelete. Namun menambahkan FolderView di loop ForEach bagian dalam tampaknya mengganggu pembaruan tampilan dinamis SwiftUI, dan segala sesuatunya menjadi bermasalah secara visual. Saya sudah mencoba membangun kembali dari awal, tetapi tidak bisa mendapatkan pembaruan tampilan yang dapat diandalkan. stackoverflow.com/questions/57627666/ Saya akan bermain dengan pendekatan frc di bawah ini dan melihat apakah saya lebih beruntung. Anda menawarkan solusi yang solid. - person Small Talk; 24.08.2019
comment
Solusi ini benar-benar berhasil. Tapi itu memicu bug di Xcode 11, beta 6. Memperkenalkan struct FolderView ke dalam loop ForEach bagian dalam akan merusak pembaruan UI saat pengguna di List EditMode = .active. Mudah-mudahan pada saat GM keluar, bug itu sudah diperbaiki, dan ini akan menjadi jawaban yang diterima. - person Small Talk; 25.08.2019

Anda benar bahwa kunci untuk melakukan apa yang Anda inginkan adalah mendapatkan satu array objek dan mengelompokkannya dengan tepat. Dalam kasus Anda, ini adalah Proyek Anda. Anda tidak menampilkan skema CoreData Anda, tetapi saya berharap Anda memiliki entitas "Proyek" dan entitas "Folder" serta hubungan satu-ke-banyak di antara keduanya. Tujuan Anda adalah membuat kueri CoreData yang membuat rangkaian Proyek tersebut dan mengelompokkannya berdasarkan Folder. Maka kunci sebenarnya adalah menggunakan NSFetchedResultsController CoreData untuk membuat grup menggunakan sectionNameKeyPath.

Tidak praktis bagi saya untuk mengirimkan seluruh proyek saya kepada Anda, jadi saya akan mencoba memberi Anda cukup banyak kode kerja saya untuk mengarahkan Anda ke arah yang benar. Jika ada kesempatan, saya akan menambahkan konsep ini ke dalam contoh program yang baru saja saya terbitkan di GitHub. https://github.com/Whiffer/SwiftUI-Core-Data-Test

Inilah inti dari Daftar Anda:

@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())
    }

Bagian dari 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) Struktur entitas seperti yang Anda gambarkan (satu-ke-banyak antara Folder dan Proyek) 2) Penggunaan FetchedResultsController dengan objek yang dikelompokkan berdasarkan parameter sectionNameKeyPath adalah pola yang sudah dikenal. 3) Penggunaan obat generik dalam contoh CoreDataDataSource Anda adalah ace. Meskipun terasa seperti monster Frankenstein untuk memasang FetchedResultsController ke SwiftUI, frc dibuat khusus untuk tabel yang dikelompokkan dari Data Inti. Terima kasih kepada Fabian dan Chuck H yang telah melepaskan saya dari kebuntuan. - person Small Talk; 20.08.2019
comment
Saya sangat menyempurnakan proyek sampel GitHub saya untuk menunjukkan kepada Anda cara mengimplementasikan .onDelete dan .onMove saat menggunakan loop ForEach bersarang. Solusinya tidak sepenuhnya mudah. Lihatlah AttributesGroupedView dan CoreDataDataSource saya yang sangat ditingkatkan. - person Chuck H; 21.08.2019
comment
terima kasih atas tautan ke GitHub CoreData + SwiftUI Anda. Saya telah mereferensikannya untuk saya gunakan, dan sejauh ini, ini berfungsi dengan sangat baik. - person Small Talk; 27.08.2019

Jika Anda ingin dengan mudah menghapus sesuatu dari suatu bagian (tidak harus dikelompokkan!) List, Anda perlu memanfaatkan sarang Anda. Anggaplah Anda memiliki yang berikut ini:

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

Sekarang Anda ingin menyiapkan .onDelete. Jadi mari kita perbesar deklarasi 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
Terima kasih, terjebak dalam hal ini, dan pindah untuk sementara. Saya akan memeriksanya lagi ketika saya kembali ke sini. - person Small Talk; 22.12.2019