// // BookmarksBench.swift // import SwiftUI import UniformTypeIdentifiers final class Model: ObservableObject { enum Outcome { case info(String) case error(String, Error) } @Published var creationOption_minimalBookmark = false @Published var creationOption_suitableForBookmarkFile = false @Published var creationOption_withSecurityScope = false @Published var creationOption_securityScopeAllowOnlyReadAccess = false @Published var creationOption_withoutImplicitSecurityScope = false @Published var resolutionOption_withoutUI = false @Published var resolutionOption_withoutMounting = false @Published var resolutionOption_withSecurityScope = false @Published var resolutionOption_withoutImplicitStartAccessing = false @Published var workUrls: [URL?] = .init(repeating: nil, count: 10) @Published var outcome: Outcome = .info("") { didSet { switch self.outcome { case .info(let info): print("info: \(info)") case .error(let message, let error): print("error: \(message) \(error)") } } } @Published var selectedSlot: Int = UserDefaults.standard.integer(forKey: "selectedSlot") { didSet { UserDefaults.standard.set(selectedSlot, forKey: "selectedSlot") pickedUrl = UserDefaults.standard.url(forKey: "PickedUrl-\(selectedSlot)") workUrl = pickedUrl bookmark = UserDefaults.standard.data(forKey: "Bookmark-\(selectedSlot)") } } @Published var bookmark: Data? { didSet { UserDefaults.standard.set(bookmark, forKey: "Bookmark-\(selectedSlot)") } } @Published var pickedUrl: URL? { didSet { UserDefaults.standard.set(pickedUrl, forKey: "PickedUrl-\(selectedSlot)") } } init() { let old = selectedSlot for i in 0 ..< 10 { selectedSlot = i } selectedSlot = old } var workUrl: URL? { get { workUrls[selectedSlot] } set { workUrls[selectedSlot] = newValue } } var bookmarkCreationOptions: URL.BookmarkCreationOptions { [ creationOption_minimalBookmark ? .minimalBookmark : [], creationOption_suitableForBookmarkFile ? .suitableForBookmarkFile : [], creationOption_withSecurityScope ? .withSecurityScope : [], creationOption_securityScopeAllowOnlyReadAccess ? .securityScopeAllowOnlyReadAccess : [], creationOption_withoutImplicitSecurityScope ? .withoutImplicitSecurityScope : [] ] } var bookmarkResolutionOptions: URL.BookmarkResolutionOptions { [ resolutionOption_withoutUI ? .withoutUI : [], resolutionOption_withoutMounting ? .withoutMounting : [], resolutionOption_withSecurityScope ? .withSecurityScope : [], resolutionOption_withoutImplicitStartAccessing ? .withoutImplicitStartAccessing : [], ] } } extension URL.BookmarkCreationOptions { #if !os(macOS) static var withSecurityScope: Self = [] static var securityScopeAllowOnlyReadAccess: Self = [] #endif } extension URL.BookmarkResolutionOptions { #if !os(macOS) static var withSecurityScope: Self = [] #endif } extension Data { func hexString(withDetails: Bool = onMac) -> String { let bytesPerLine = 32 var result = "data size: \(count)\n" if withDetails { for lineStart in stride(from: 0, to: self.count, by: bytesPerLine) { let lineEnd = Swift.min(lineStart + bytesPerLine, self.count) let lineBytes = self[lineStart.. String in let scalar = UnicodeScalar(byte) return scalar.isASCII && scalar.value >= 0x20 && scalar.value <= 0x7E ? String(scalar) : "." }.joined() result += hexPart + " |" + asciiPart + "|\n" } } return result } } extension Optional { var descriptionOrNil: String { if let value = self { "\(value)" } else { "nil" } } } #Preview { ContentView() } extension Optional { func unwrap(_ message: String = "") -> Wrapped { guard let value = self else { preconditionFailure(message) } return value } } var onMac: Bool { #if os(macOS) true #else false #endif } struct ContentView: View { @StateObject private var model = Model() var body: some View { let view = HStack { VStack(alignment: .leading, spacing: 2) { HStack { Menu("Selected slot: \(model.selectedSlot)") { ForEach(0 ..< 10) { i in Button("\(i) " + (model.workUrls[i]?.nameEnd ?? "")) { model.selectedSlot = i } } } .menuStyle(.button) .buttonStyle(.bordered) Spacer() } GroupBox { VStack(alignment: .leading) { Text("Picked: " + model.pickedUrl.descriptionOrNil).font(.caption2) .textSelection(.enabled) .lineLimit(1) .truncationMode(.middle) .padding(2) Text("Work: " + model.workUrl.descriptionOrNil).font(.caption2) .textSelection(.enabled) .lineLimit(1) .truncationMode(.middle) .padding(2) } } GroupBox { VStack(alignment: .leading) { HStack { Button("Pick File") { pickFile() } .buttonStyle(.bordered) Button("Pick Folder") { pickFile(folder: true) } .buttonStyle(.bordered) Spacer() } HStack { Button("Work with Picked") { model.workUrl = model.pickedUrl model.outcome = .info("workURL is set to: " + model.pickedUrl.descriptionOrNil) } .buttonStyle(.bordered) Button("Set work to nil") { model.workUrl = nil model.outcome = .info("workURL is set to: nil") } .buttonStyle(.bordered) .disabled(model.workUrl == nil) Spacer() } HStack { Button("Read") { let url = model.workUrl.unwrap("check button disabling condition") var isFolder: ObjCBool = false FileManager.default.fileExists(atPath: url.path, isDirectory: &isFolder) do { if isFolder.boolValue { let contents = try FileManager.default.contentsOfDirectory(atPath: url.path) model.outcome = .info("Reading from folder succeeded, file count: \(contents.count), \(contents.map { $0 })") } else { let data = try Data(contentsOf: url) model.outcome = .info("Reading from file succeeded, \(data.count) bytes read") } } catch { model.outcome = .error("Reading from file/folder failed with error: ", error) } } .buttonStyle(.bordered) .disabled(model.workUrl == nil) Button("Write") { let url = model.workUrl.unwrap("check button disabling condition") var isFolder: ObjCBool = false FileManager.default.fileExists(atPath: url.path, isDirectory: &isFolder) do { if isFolder.boolValue { let url = url.appendingPathComponent(String(Int.random(in: 0 ..< .max))) try FileManager.default.createDirectory(at: url, withIntermediateDirectories: false) try? FileManager.default.removeItem(at: url) model.outcome = .info("Writing to folder succeeded") } else { let data = try Data(contentsOf: url) try data.write(to: url) model.outcome = .info("Writing to file succeeded") } } catch { model.outcome = .error("Writing to file/folder failed with error:", error) } } .buttonStyle(.bordered) .disabled(model.workUrl == nil) Button("StartAccess") { let url = model.workUrl.unwrap("check button disabling condition") let result = url.startAccessingSecurityScopedResource() model.outcome = .info("start succeeded with: \(result)") } .buttonStyle(.bordered) .disabled(model.workUrl == nil) Button("StopAccess") { let url = model.workUrl.unwrap("check button disabling condition") url.stopAccessingSecurityScopedResource() model.outcome = .info("stop succeeded (it can't fail)") } .buttonStyle(.bordered) .disabled(model.workUrl == nil) Spacer() } } } GroupBox { VStack(alignment: .leading) { Toggle("minimalBookmark", isOn: $model.creationOption_minimalBookmark) Toggle("suitableForBookmarkFile", isOn: $model.creationOption_suitableForBookmarkFile) Toggle("withoutImplicitSecurityScope", isOn: $model.creationOption_withoutImplicitSecurityScope) Toggle("withSecurityScope (mac only)", isOn: $model.creationOption_withSecurityScope) .disabled(!onMac) .foregroundStyle(onMac ? Color.primary : Color.secondary) Toggle("securityScopeAllowOnlyReadAccess (mac only)", isOn: $model.creationOption_securityScopeAllowOnlyReadAccess) .disabled(!onMac) .foregroundStyle(onMac ? Color.primary : Color.secondary) Button("Create bookmark") { let url = model.workUrl.unwrap("check button disabling condition") do { model.bookmark = try url.bookmarkData(options: model.bookmarkCreationOptions, includingResourceValuesForKeys: [], relativeTo: nil) model.outcome = .info("Bookmark created") } catch { model.bookmark = nil model.outcome = .error("Creating bookmark failed with error:", error) } } .buttonStyle(.bordered) .disabled(model.workUrl == nil) } } GroupBox { VStack(alignment: .leading) { Toggle("withoutUI", isOn: $model.resolutionOption_withoutUI) Toggle("withoutMounting", isOn: $model.resolutionOption_withoutMounting) Toggle("withoutImplicitStartAccessing", isOn: $model.resolutionOption_withoutImplicitStartAccessing) Toggle("withSecurityScope (mac only)", isOn: $model.resolutionOption_withSecurityScope) .disabled(!onMac) .foregroundStyle(onMac ? Color.primary : Color.secondary) Button("Resolve bookmark") { if let data = model.bookmark { var isStale = false do { model.workUrl = try URL(resolvingBookmarkData: data, options: model.bookmarkResolutionOptions, relativeTo: nil, bookmarkDataIsStale: &isStale) model.outcome = .info("Resolving bookmark succeeded: " + model.workUrl.descriptionOrNil + " isStale: \(isStale)") } catch { model.workUrl = nil model.outcome = .error("Resolving bookmark failed with error:", error) } } } .buttonStyle(.bordered) .disabled(model.bookmark == nil) } } switch model.outcome { case .info(let info): Text(info.isEmpty ? "Info area here" : info) .foregroundStyle(info.isEmpty ? Color.secondary : Color.green) .textSelection(.enabled) .lineLimit(6) .padding() case .error(let string, let error): Text("\(string) \(error)") .foregroundStyle(Color.red) .textSelection(.enabled) .lineLimit(6) .padding() } Text("bookmark: " + (model.bookmark?.hexString()).descriptionOrNil).font(.caption2).monospaced() .textSelection(.enabled) Spacer() } Spacer() } .padding() .controlSize(.small) .font(.caption2) #if os(macOS) return view #else return view .sheet(isPresented: $showFilePicker) { FilePicker(isFolder: false) { url in model.outcome = .info("Picked file: " + url.descriptionOrNil) model.pickedUrl = url model.workUrl = url } } .sheet(isPresented: $showFolderPicker) { FilePicker(isFolder: true) { url in model.outcome = .info("Picked folder: " + url.descriptionOrNil) model.pickedUrl = url model.workUrl = url } } #endif } #if os(macOS) func pickFile(folder: Bool = false) { let panel = NSOpenPanel() panel.allowsMultipleSelection = false panel.canChooseDirectories = folder panel.canChooseFiles = !folder panel.allowedContentTypes = [.item] if panel.runModal() == .OK { model.pickedUrl = panel.url model.workUrl = panel.url model.outcome = .info("Picked file or folder: " + panel.url.descriptionOrNil) } else { model.pickedUrl = nil model.workUrl = nil model.outcome = .info("Picked nil") } } #else @State private var showFilePicker: Bool = false @State private var showFolderPicker: Bool = false func pickFile(folder: Bool = false) { if folder { showFolderPicker = true } else { showFilePicker = true } } #endif } extension URL { var nameEnd: String { let s = lastPathComponent return if s.count > 20 { "“…" + String(s.dropFirst(s.count - 20) + "”") } else { "“" + s + "”" } } } #if !os(macOS) struct FilePicker: UIViewControllerRepresentable { let isFolder: Bool var onPick: (URL?) -> Void func makeUIViewController(context: Context) -> UIDocumentPickerViewController { let picker = UIDocumentPickerViewController(forOpeningContentTypes: [isFolder ? .folder : .item], asCopy: false) picker.delegate = context.coordinator picker.allowsMultipleSelection = false return picker } func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: Context) {} func makeCoordinator() -> Coordinator { Coordinator(onPick: onPick) } class Coordinator: NSObject, UIDocumentPickerDelegate { var onPick: (URL?) -> Void init(onPick: @escaping (URL?) -> Void) { self.onPick = onPick } func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { onPick(urls.first.unwrap("should not be possible to have an empty url array here!")) } func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { onPick(nil) } } } #endif @main struct MacTestApp: App { var body: some Scene { WindowGroup { ContentView() } } }