# Shoebox/Library Apps Apps with internal database and sidebar navigation (like Notes, Photos, Music). Use shoebox pattern when: - Single library of items (not separate files) - No explicit save (auto-save everything) - Import/export rather than open/save - Sidebar navigation (folders, tags, smart folders) - iCloud sync across devices Do NOT use when: - Users need to manage individual files - Files shared with other apps directly ```swift @main struct LibraryApp: App { var body: some Scene { WindowGroup { ContentView() } .modelContainer(for: [Note.self, Folder.self, Tag.self]) .commands { LibraryCommands() } } } struct ContentView: View { @State private var selectedFolder: Folder? @State private var selectedNote: Note? @State private var searchText = "" var body: some View { NavigationSplitView { SidebarView(selection: $selectedFolder) } content: { NoteListView(folder: selectedFolder, selection: $selectedNote) } detail: { if let note = selectedNote { NoteEditorView(note: note) } else { ContentUnavailableView("Select a Note", systemImage: "note.text") } } .searchable(text: $searchText) } } ``` ```swift import SwiftData @Model class Note { var title: String var content: String var createdAt: Date var modifiedAt: Date var isPinned: Bool @Relationship(inverse: \Folder.notes) var folder: Folder? @Relationship var tags: [Tag] init(title: String = "New Note") { self.title = title self.content = "" self.createdAt = Date() self.modifiedAt = Date() self.isPinned = false self.tags = [] } } @Model class Folder { var name: String var icon: String var sortOrder: Int @Relationship(deleteRule: .cascade) var notes: [Note] var isSmartFolder: Bool var predicate: String? // For smart folders init(name: String, icon: String = "folder") { self.name = name self.icon = icon self.sortOrder = 0 self.notes = [] self.isSmartFolder = false } } @Model class Tag { var name: String var color: String @Relationship(inverse: \Note.tags) var notes: [Note] init(name: String, color: String = "blue") { self.name = name self.color = color self.notes = [] } } ``` ```swift struct SidebarView: View { @Environment(\.modelContext) private var context @Query(sort: \Folder.sortOrder) private var folders: [Folder] @Binding var selection: Folder? var body: some View { List(selection: $selection) { Section("Library") { Label("All Notes", systemImage: "note.text") .tag(nil as Folder?) Label("Recently Deleted", systemImage: "trash") } Section("Folders") { ForEach(folders.filter { !$0.isSmartFolder }) { folder in Label(folder.name, systemImage: folder.icon) .tag(folder as Folder?) .contextMenu { Button("Rename") { renameFolder(folder) } Button("Delete", role: .destructive) { deleteFolder(folder) } } } .onMove(perform: moveFolders) } Section("Smart Folders") { ForEach(folders.filter { $0.isSmartFolder }) { folder in Label(folder.name, systemImage: "folder.badge.gearshape") .tag(folder as Folder?) } } Section("Tags") { TagsSection() } } .listStyle(.sidebar) .toolbar { ToolbarItem { Button(action: addFolder) { Label("New Folder", systemImage: "folder.badge.plus") } } } } private func addFolder() { let folder = Folder(name: "New Folder") folder.sortOrder = folders.count context.insert(folder) } private func deleteFolder(_ folder: Folder) { context.delete(folder) } private func moveFolders(from source: IndexSet, to destination: Int) { var reordered = folders.filter { !$0.isSmartFolder } reordered.move(fromOffsets: source, toOffset: destination) for (index, folder) in reordered.enumerated() { folder.sortOrder = index } } } ``` ```swift struct NoteListView: View { let folder: Folder? @Binding var selection: Note? @Environment(\.modelContext) private var context @Query private var allNotes: [Note] var filteredNotes: [Note] { let sorted = allNotes.sorted { if $0.isPinned != $1.isPinned { return $0.isPinned } return $0.modifiedAt > $1.modifiedAt } if let folder = folder { return sorted.filter { $0.folder == folder } } return sorted } var body: some View { List(filteredNotes, selection: $selection) { note in NoteRow(note: note) .tag(note) .contextMenu { Button(note.isPinned ? "Unpin" : "Pin") { note.isPinned.toggle() } Divider() Button("Delete", role: .destructive) { context.delete(note) } } } .toolbar { ToolbarItem { Button(action: addNote) { Label("New Note", systemImage: "square.and.pencil") } } } } private func addNote() { let note = Note() note.folder = folder context.insert(note) selection = note } } struct NoteRow: View { let note: Note var body: some View { VStack(alignment: .leading, spacing: 4) { HStack { if note.isPinned { Image(systemName: "pin.fill") .foregroundStyle(.orange) .font(.caption) } Text(note.title.isEmpty ? "New Note" : note.title) .fontWeight(.medium) } Text(note.modifiedAt.formatted(date: .abbreviated, time: .shortened)) .font(.caption) .foregroundStyle(.secondary) Text(note.content.prefix(100)) .font(.caption) .foregroundStyle(.secondary) .lineLimit(2) } .padding(.vertical, 4) } } ``` ```swift struct NoteEditorView: View { @Bindable var note: Note @FocusState private var isFocused: Bool var body: some View { VStack(spacing: 0) { // Title TextField("Title", text: $note.title) .textFieldStyle(.plain) .font(.title) .padding() Divider() // Content TextEditor(text: $note.content) .font(.body) .focused($isFocused) .padding() } .onChange(of: note.title) { _, _ in note.modifiedAt = Date() } .onChange(of: note.content) { _, _ in note.modifiedAt = Date() } .toolbar { ToolbarItem { Menu { TagPickerMenu(note: note) } label: { Label("Tags", systemImage: "tag") } } ToolbarItem { ShareLink(item: note.content) } } } } ``` ```swift struct SmartFolderSetup { static func createDefaultSmartFolders(context: ModelContext) { // Today let today = Folder(name: "Today", icon: "calendar") today.isSmartFolder = true today.predicate = "modifiedAt >= startOfToday" context.insert(today) // This Week let week = Folder(name: "This Week", icon: "calendar.badge.clock") week.isSmartFolder = true week.predicate = "modifiedAt >= startOfWeek" context.insert(week) // Pinned let pinned = Folder(name: "Pinned", icon: "pin") pinned.isSmartFolder = true pinned.predicate = "isPinned == true" context.insert(pinned) } } // Query based on smart folder predicate func notesForSmartFolder(_ folder: Folder) -> [Note] { switch folder.predicate { case "isPinned == true": return allNotes.filter { $0.isPinned } case "modifiedAt >= startOfToday": let start = Calendar.current.startOfDay(for: Date()) return allNotes.filter { $0.modifiedAt >= start } default: return [] } } ``` ```swift struct LibraryCommands: Commands { @Environment(\.modelContext) private var context var body: some Commands { CommandGroup(after: .importExport) { Button("Import Notes...") { importNotes() } .keyboardShortcut("i", modifiers: [.command, .shift]) Button("Export All Notes...") { exportNotes() } .keyboardShortcut("e", modifiers: [.command, .shift]) } } private func importNotes() { let panel = NSOpenPanel() panel.allowedContentTypes = [.json, .plainText] panel.allowsMultipleSelection = true if panel.runModal() == .OK { for url in panel.urls { importFile(url) } } } private func exportNotes() { let panel = NSSavePanel() panel.allowedContentTypes = [.json] panel.nameFieldStringValue = "Notes Export.json" if panel.runModal() == .OK, let url = panel.url { let descriptor = FetchDescriptor() if let notes = try? context.fetch(descriptor) { let exportData = notes.map { NoteExport(note: $0) } if let data = try? JSONEncoder().encode(exportData) { try? data.write(to: url) } } } } } struct NoteExport: Codable { let title: String let content: String let createdAt: Date let modifiedAt: Date init(note: Note) { self.title = note.title self.content = note.content self.createdAt = note.createdAt self.modifiedAt = note.modifiedAt } } ``` ```swift struct ContentView: View { @State private var searchText = "" @Query private var allNotes: [Note] var searchResults: [Note] { if searchText.isEmpty { return [] } return allNotes.filter { note in note.title.localizedCaseInsensitiveContains(searchText) || note.content.localizedCaseInsensitiveContains(searchText) } } var body: some View { NavigationSplitView { // ... } .searchable(text: $searchText, placement: .toolbar) .searchSuggestions { if !searchText.isEmpty { ForEach(searchResults.prefix(5)) { note in Button { selectedNote = note } label: { VStack(alignment: .leading) { Text(note.title) Text(note.modifiedAt.formatted()) .font(.caption) .foregroundStyle(.secondary) } } } } } } } ``` ```swift // Configure container for iCloud @main struct LibraryApp: App { let container: ModelContainer init() { let schema = Schema([Note.self, Folder.self, Tag.self]) let config = ModelConfiguration( "Library", schema: schema, cloudKitDatabase: .automatic ) do { container = try ModelContainer(for: schema, configurations: config) } catch { fatalError("Failed to create container: \(error)") } } var body: some Scene { WindowGroup { ContentView() } .modelContainer(container) } } // Handle sync status struct SyncStatusIndicator: View { @State private var isSyncing = false var body: some View { if isSyncing { ProgressView() .scaleEffect(0.5) } else { Image(systemName: "checkmark.icloud") .foregroundStyle(.green) } } } ``` - Auto-save on every change (no explicit save) - Provide import/export for data portability - Use sidebar for navigation (folders, tags, smart folders) - Support search across all content - Show modification dates, not explicit "save" - Use SwiftData with iCloud for seamless sync - Provide trash/restore instead of permanent delete