Skip to main content

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] = []
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)
item
UploadItem
required
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:
  1. Inserts the new item at index 0 (beginning of array)
  2. Maintains reverse chronological order (newest first)
  3. Automatically persists to UserDefaults

remove()

Removes upload items at the specified indices.
func remove(at offsets: IndexSet)
offsets
IndexSet
required
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.
func clearAll()
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
}
storageKey
String
UserDefaults key: "fiaxe.uploadHistory"

Initialization

The store automatically loads saved history on initialization:
init() {
    load()
}
History is restored when the app launches.

Data Format

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)