Я пытаюсь создать простое дерево файлов в 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
}
Да, я знаю это, но это приведет к сбросу состояния расширения всех дисклогрупп на ложное.
Пробовали ли вы Bibding в FileView, чтобы отразить изменение в родительском представлении. Или можно использовать наблюдаемую модель представления файла с опубликованной моделью файла.
хорошая идея. ты мог бы это написать?
Здесь с привязкой (возможно, не лучшее решение, но работает). Важное примечание: это родительский каталог файла, который должен удалить его из дочерних элементов.
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
}
Ваша функция
deleteFile(url: URL)
удаляет их изFileManager
, но это не влияет на массивы файлов, поскольку вы никогда не выполняете повторную выборку/проверку изFileManager
. Вам нужно будет обновить сохраненные массивы, чтобы увидеть любые изменения в пользовательском интерфейсе, либо вызовяloadDirectory()
, либо вручную обновив массивы состояний (или используя наблюдение и побочные эффекты для того и другого).