I am trying to make a simple file tree in SwiftUI however I have found an issue for which my coding skills aren’t good enough. Basically the tree consists of a view called ‘FileView’, which is called recursively and contains a disclosuregroup, for folders. However when I delete a file, it gets deleted, but the view doesn’t update. I can’t just call the loading function again, because that would reset the state of all discloregroups, and I would have to reopen every single one. What I also can’t do, is for example a file gets deleted by another application, or the user does it outside of the app. then the app should detect that, and also update itself.
The app also doesnt require sandbox.
If you have any questions about my code, please just write a comment and i’ll try to answer it as best as I can.
Help is greatly appreciated.
Here is my code, which should be ready to run, however in order to work you have to disable sandbox.
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
}