Как быстро обновить массив рекурсивных элементов?

Я пытаюсь создать простое дерево файлов в SwiftUI, однако обнаружил проблему, для решения которой моих навыков программирования недостаточно. По сути, дерево состоит из представления FileView, которое вызывается рекурсивно и содержит группу раскрытия для папок. Однако когда я удаляю файл, он удаляется, но представление не обновляется. Я не могу просто снова вызвать функцию загрузки, потому что это приведет к сбросу состояния всех групп раскрытия информации, и мне придется заново открывать каждую из них. Чего я также не могу сделать, так это, например, что файл удаляется другим приложением или пользователь делает это вне приложения. тогда приложение должно обнаружить это, а также обновиться. Приложение также не требует песочницы.

Если у вас есть какие-либо вопросы о моем коде, просто напишите комментарий, и я постараюсь ответить на него как можно лучше.

Помощь очень ценится.

Вот мой код, который должен быть готов к запуску, однако для работы вам необходимо отключить песочницу.

import SwiftUI

struct FileModel: Identifiable {
    let id = UUID()
    var parentDirectory: URL?
    var fileURL: URL
    var subFiles: [FileModel]?
    
    var systemImage: String {
        if let subFiles {
            if fileURL.pathExtension == "app" {
                return "app.badge.fill"
            }
            if subFiles.isEmpty {
                return "folder"
            } else {
                return "folder.fill"
            }
        } else {
            switch fileURL.pathExtension {
            case "app": return "app.fill"
            case "png", "jpg", "tiff": return "photo.fill"
            case "pdf": return "doc.richtext"
            case "csv", "json": return "list.bullet.indent"
            case "bom": return "doc.richtext"
            case "stl", "obj", "usdz": return "cube.transparent.fill"
            case "mp3", "wav": return "waveform"
            default: return "doc.fill"
            }
        }
    }
}

func deleteFile(url: URL) {
    let fileManager = FileManager.default
    do {
        try fileManager.removeItem(at: url)
    } catch {
        print(error)
    }
}

struct FileView: View {
    @State var file: FileModel
    @State private var isExpanded = false
    
    var body: some View {
        Group {
            if let subFiles = file.subFiles {
                DisclosureGroup(isExpanded: $isExpanded) {
                    ForEach(subFiles) { subFile in
                        FileView(file: subFile)
                    }
                } label: {
                    Label(file.fileURL.lastPathComponent, systemImage: file.systemImage)
                }
            } else {
                HStack {
                    Image(systemName: file.systemImage)
                    Text(file.fileURL.lastPathComponent)
                }
            }
        } .tag(file.fileURL)
            .contextMenu {
                Button("Show in finder") {
                    NSWorkspace.shared.activateFileViewerSelecting([file.fileURL])
                }
                Button("Open with external editor") {
                    NSWorkspace.shared.open(file.fileURL)
                }
                Button("Delete") {
                    deleteFile(url: file.fileURL)
                }
            }
    }
}

struct ContentView: View {
    
    @State private var rootFiles: [FileModel] = []
    @State private var selection: Set<URL>?
    
    @State private var showPicker = false
    
    var body: some View {
        NavigationSplitView {
            List(selection: $selection) {
                ForEach(rootFiles) { file in
                    FileView(file: file)
                }
            }
            .toolbar {
                ToolbarItem {
                    Button("Load Directory") {
                        showPicker.toggle()
                    }
                }
            }
        } detail: {
            
        }
        .fileImporter(isPresented: $showPicker, allowedContentTypes: [.folder], onCompletion: { result in
            do {
                let url = try result.get()
                Task {
                    rootFiles = await loadDirectory(url: url)
                }
            } catch {
                print(error)
            }
        })
    }
}

func loadDirectory(url: URL) async -> [FileModel] {
    var fileModels = [FileModel]()
    
    do {
        let fileManager = FileManager.default
        let contents = try fileManager.contentsOfDirectory(at: url, includingPropertiesForKeys: [.isDirectoryKey], options: [.skipsHiddenFiles])
        
        await withTaskGroup(of: FileModel?.self) { group in
            for content in contents {
                group.addTask {
                    var isDirectory: ObjCBool = false
                    let localFileManager = FileManager() // Create a new FileManager instance
                    localFileManager.fileExists(atPath: content.path, isDirectory: &isDirectory)
                    
                    if isDirectory.boolValue {
                        // Recursively load subdirectory
                        let subFiles = await loadDirectory(url: content)
                        return FileModel(parentDirectory: url, fileURL: content, subFiles: subFiles)
                    } else {
                        // It's a file
                        return FileModel(parentDirectory: url, fileURL: content, subFiles: nil)
                    }
                }
            }
            
            for await fileModel in group {
                if let fileModel = fileModel {
                    fileModels.append(fileModel)
                }
            }
        }
    } catch {
        print("Error loading directory: \(error)")
    }
    
    return fileModels
}

Ваша функция deleteFile(url: URL) удаляет их из FileManager, но это не влияет на массивы файлов, поскольку вы никогда не выполняете повторную выборку/проверку из FileManager. Вам нужно будет обновить сохраненные массивы, чтобы увидеть любые изменения в пользовательском интерфейсе, либо вызовя loadDirectory(), либо вручную обновив массивы состояний (или используя наблюдение и побочные эффекты для того и другого).

pilchard 15.06.2024 12:38

Да, я знаю это, но это приведет к сбросу состояния расширения всех дисклогрупп на ложное.

Karl Ehrlich 15.06.2024 12:50

Пробовали ли вы Bibding в FileView, чтобы отразить изменение в родительском представлении. Или можно использовать наблюдаемую модель представления файла с опубликованной моделью файла.

Ptit Xav 15.06.2024 13:32

хорошая идея. ты мог бы это написать?

Karl Ehrlich 15.06.2024 13:50
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
4
58
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Здесь с привязкой (возможно, не лучшее решение, но работает). Важное примечание: это родительский каталог файла, который должен удалить его из дочерних элементов.

struct FileModel: Identifiable {
    let id = UUID()
    var parentDirectory: URL?
    var fileURL: URL
    var isDirectory: Bool // define if directory or not
    var subFiles: [FileModel] = [] // empty for directiry
    
    var systemImage: String {
        if isDirectory {
            if fileURL.pathExtension == "app" {
                return "app.badge.fill"
            }
            if subFiles.isEmpty {
                return "folder"
            } else {
                return "folder.fill"
            }
        } else {
            switch fileURL.pathExtension {
                case "app": return "app.fill"
                case "png", "jpg", "tiff": return "photo.fill"
                case "pdf": return "doc.richtext"
                case "csv", "json": return "list.bullet.indent"
                case "bom": return "doc.richtext"
                case "stl", "obj", "usdz": return "cube.transparent.fill"
                case "mp3", "wav": return "waveform"
                default: return "doc.fill"
            }
        }
    }
    
    // delete a subfile from the list
    mutating func delete(subFile: FileModel) {
        guard let index = subFiles.firstIndex(where: {$0.id == subFile.id}) else { return }
        subFiles.remove(at: index)
    }
}

func deleteFile(url: URL) {
    let fileManager = FileManager.default
    do {
        try fileManager.removeItem(at: url)
    } catch {
        print(error)
    }
}

struct FileView: View {
    @Binding var file: FileModel // Use a binding to have UI synchronization
    
    @State private var isExpanded = false
    
    let deleteFileCallBack: (FileModel) -> Void
    
    var body: some View {
        Group {
            if file.isDirectory { // use isDirectiry property
                DisclosureGroup(isExpanded: $isExpanded) {
                    ForEach($file.subFiles) { $subFile in // note the syntax for binding property
                        FileView(file: $subFile, deleteFileCallBack: self.delete(subFile:))
                    }
                } label: {
                    Label(file.fileURL.lastPathComponent, systemImage: file.systemImage)
                }
            } else {
                HStack {
                    Image(systemName: file.systemImage)
                    Text(file.fileURL.lastPathComponent)
                }
                .contextMenu { // context menu is associated to the file
                    Button("Show in finder") {
                        NSWorkspace.shared.activateFileViewerSelecting([file.fileURL])
                    }
                    Button("Open with external editor") {
                        NSWorkspace.shared.open(file.fileURL)
                    }
                    Button("Delete") {
                        deleteFile(url: file.fileURL)
                        self.deleteFileCallBack(file) // tell the caller to remove the file from the list
                    }
                }
            }
        } .tag(file.fileURL)
    }
    
    func delete(subFile: FileModel) { // this will be used by subview to indicate a subfile must has been deleted
        guard let index = file.subFiles.firstIndex(where: {$0.id == subFile.id}) else { return }
        file.subFiles.remove(at: index)
    }

}

struct ContentView: View {
    
    @State
    private var rootFiles: [FileModel] = []
    
    @State
    private var selection: Set<URL>?
    
    @State private var showPicker = false
    
    var body: some View {
        NavigationSplitView {
            List(selection: $selection) {
                ForEach($rootFiles) { $file in // Binding loop
                    FileView(file: $file, deleteFileCallBack: self.delete(rootFile:))
                }
            }
            .toolbar {
                ToolbarItem(placement: .principal) {
                    Button("Load Directory") {
                        showPicker.toggle()
                    }
                }
            }
        } detail: {
            
        }
        .fileImporter(isPresented: $showPicker, allowedContentTypes: [.folder], onCompletion: { result in
            do {
                let url = try result.get()
                Task {
                    rootFiles = await loadDirectory(url: url)
                }
            } catch {
                print(error)
            }
        })
    }
    
    // to tell the subview how to delete the file
    func delete(rootFile: FileModel) {
        guard let index = rootFiles.firstIndex(where: {$0.id == rootFile.id}) else { return }
        rootFiles.remove(at: index)
    }
}

func loadDirectory(url: URL) async -> [FileModel] {
    var fileModels = [FileModel]()
    
    do {
        let fileManager = FileManager.default
        let contents = try fileManager.contentsOfDirectory(at: url, includingPropertiesForKeys: [.isDirectoryKey], options: [.skipsHiddenFiles])
        
        await withTaskGroup(of: FileModel?.self) { group in
            for content in contents {
                group.addTask {
                    var isDirectory: ObjCBool = false
                    let localFileManager = FileManager() // Create a new FileManager instance
                    localFileManager.fileExists(atPath: content.path, isDirectory: &isDirectory)
                    
                    if isDirectory.boolValue {
                        // Recursively load subdirectory
                        let subFiles = await loadDirectory(url: content)
                        return FileModel(parentDirectory: url, fileURL: content, isDirectory: true, subFiles: subFiles)
                    } else {
                        // It's a file
                        return FileModel(parentDirectory: url, fileURL: content, isDirectory: false) // no subfiles because directory
                    }
                }
            }
            
            for await fileModel in group {
                if let fileModel = fileModel {
                    fileModels.append(fileModel)
                }
            }
        }
    } catch {
        print("Error loading directory: \(error)")
    }
    
    return fileModels
}

Другие вопросы по теме