Overview
UploadHistoryStore manages the upload history by persisting completed uploads to UserDefaults. It provides an observable list of upload items that automatically syncs to disk, allowing users to track their upload activity across app launches.
Located at: Fiaxe/Services/UploadHistoryStore.swift:6
Type Definition
@Observable
final class UploadHistoryStore
An observable class that automatically notifies SwiftUI views when the history changes.
Properties
items
The current list of upload history items.
var items: [UploadItem] = []
Array of completed uploads, sorted with newest first. Automatically persisted to UserDefaults.
Methods
add()
Adds a new upload item to the history.
func add(_ item: UploadItem)
The upload item to add to history. Contains file name, size, R2 key, upload date, and public URL.
Implementation Details
// Example from UploadHistoryStore.swift:15-18
func add(_ item: UploadItem) {
items.insert(item, at: 0) // newest first
save()
}
The method:
- Inserts the new item at index 0 (beginning of array)
- Maintains reverse chronological order (newest first)
- Automatically persists to UserDefaults
remove()
Removes upload items at the specified indices.
func remove(at offsets: IndexSet)
Indices of items to remove. Typically provided by SwiftUI’s onDelete modifier.
Implementation Details
// Example from UploadHistoryStore.swift:20-23
func remove(at offsets: IndexSet) {
items.remove(atOffsets: offsets)
save()
}
Used with SwiftUI’s List deletion:
List {
ForEach(store.items) { item in
UploadHistoryRow(item: item)
}
.onDelete { offsets in
store.remove(at: offsets)
}
}
clearAll()
Removes all items from the history.
Useful for implementing a “Clear History” button.
Implementation Details
// Example from UploadHistoryStore.swift:25-28
func clearAll() {
items.removeAll()
save()
}
Persistence Implementation
The store uses UserDefaults with JSON encoding:
private static let storageKey = "fiaxe.uploadHistory"
private func save() {
guard let data = try? JSONEncoder().encode(items) else { return }
UserDefaults.standard.set(data, forKey: Self.storageKey)
}
private func load() {
guard let data = UserDefaults.standard.data(forKey: Self.storageKey),
let decoded = try? JSONDecoder().decode([UploadItem].self, from: data)
else { return }
items = decoded
}
UserDefaults key: "fiaxe.uploadHistory"
Initialization
The store automatically loads saved history on initialization:
History is restored when the app launches.
History is stored as JSON-encoded array of UploadItem objects:
[
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"fileName": "photo.jpg",
"fileSize": 2048576,
"r2Key": "abc12345-photo.jpg",
"uploadDate": "2024-03-15T12:30:45Z",
"publicURL": "https://pub-bucket.example.com/abc12345-photo.jpg"
},
// ... more items
]
Usage Example
import SwiftUI
struct UploadHistoryView: View {
@State private var store = UploadHistoryStore()
var body: some View {
VStack {
List {
ForEach(store.items) { item in
HStack {
VStack(alignment: .leading) {
Text(item.fileName)
.font(.headline)
Text(item.formattedFileSize)
.font(.caption)
.foregroundColor(.secondary)
Text(item.uploadDate.formatted())
.font(.caption2)
.foregroundColor(.secondary)
}
Spacer()
Button("Copy URL") {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(
item.publicURL.absoluteString,
forType: .string
)
}
}
}
.onDelete { offsets in
store.remove(at: offsets)
}
}
if !store.items.isEmpty {
Button("Clear All History") {
store.clearAll()
}
.foregroundColor(.red)
}
}
}
}
Adding Items After Upload
// After successful upload
let uploadItem = UploadItem(
fileName: fileURL.lastPathComponent,
fileSize: fileSize,
r2Key: generatedKey,
publicURL: publicURL
)
uploadHistoryStore.add(uploadItem)
Automatic Persistence
Zero-Effort PersistenceThe store automatically saves to UserDefaults after every modification:
add() saves immediately
remove() saves immediately
clearAll() saves immediately
No manual save calls are needed.
Observable Behavior
The @Observable macro enables automatic SwiftUI updates:
@Observable
final class UploadHistoryStore {
var items: [UploadItem] = [] // Changes automatically trigger view updates
}
Views automatically refresh when:
- Items are added
- Items are removed
- All items are cleared
Integration with r2Vault
The upload history is integrated into the menu bar popover:
struct MenuBarView: View {
@Environment(AppViewModel.self) private var viewModel
@State private var historyStore = UploadHistoryStore()
var body: some View {
TabView {
UploadQueueView()
.tabItem { Label("Queue", systemImage: "arrow.up") }
UploadHistoryView(store: historyStore)
.tabItem { Label("History", systemImage: "clock") }
}
}
}
Users can:
- View recent uploads
- Copy public URLs
- Delete individual items
- Clear entire history
Data Retention
Unlimited HistoryThe current implementation stores all uploads indefinitely. For production use, consider:
- Limiting to the most recent N uploads
- Auto-deleting items older than X days
- Implementing search/filter functionality
- Adding pagination for large histories
Privacy Considerations
Stored in Plain TextUpload history is stored in UserDefaults without encryption:
- File names are visible
- Public URLs are visible
- Upload dates are visible
- R2 keys are visible
For sensitive uploads, consider:
- Adding encryption for stored data
- Providing a “Clear History on Quit” option
- Implementing automatic cleanup
- Storing less identifying information
Error Handling
The store gracefully handles encoding/decoding errors:
private func save() {
guard let data = try? JSONEncoder().encode(items) else { return }
// Silently fails if encoding fails
UserDefaults.standard.set(data, forKey: Self.storageKey)
}
private func load() {
guard let data = UserDefaults.standard.data(forKey: Self.storageKey),
let decoded = try? JSONDecoder().decode([UploadItem].self, from: data)
else { return } // Silently fails if decoding fails
items = decoded
}
Failures result in:
- Empty history on load failure
- No persistence on save failure
- No crashes or error messages
Testing Considerations
For testing, use a separate UserDefaults suite:
// Modify the store to accept a custom UserDefaults
final class UploadHistoryStore {
private let defaults: UserDefaults
private let storageKey: String
init(defaults: UserDefaults = .standard, storageKey: String = "fiaxe.uploadHistory") {
self.defaults = defaults
self.storageKey = storageKey
load()
}
private func save() {
guard let data = try? JSONEncoder().encode(items) else { return }
defaults.set(data, forKey: storageKey)
}
}
// In tests
let testDefaults = UserDefaults(suiteName: "com.example.tests")!
let store = UploadHistoryStore(defaults: testDefaults)