Skip to main content

Language & Platform

Swift 6

Modern Swift with strict concurrency checking

macOS 15.0+

Leverages latest macOS APIs and SwiftUI features

Xcode 16+

Built with the latest Xcode toolchain

~4,700 LOC

Pure Swift codebase, no Objective-C

UI Frameworks

SwiftUI (Primary)

r2Vault’s entire interface is built with SwiftUI:
ContentView.swift
struct ContentView: View {
    @Environment(AppViewModel.self) private var viewModel
    
    var body: some View {
        NavigationSplitView {
            // Sidebar with bucket list
            List(selection: $selection) {
                Section("Buckets") {
                    ForEach(viewModel.credentialsList) { creds in
                        Label(creds.bucketName, systemImage: "externaldrive.fill")
                    }
                }
            }
        } detail: {
            // Main browser view
            BrowserView()
        }
    }
}
Why SwiftUI?
  • Declarative UI with automatic updates
  • Native macOS look and feel
  • Excellent performance with @Observable
  • Built-in drag-and-drop, context menus, and more

Key SwiftUI Features Used

Swift 6’s new observation system replaces ObservableObject:
@Observable
final class AppViewModel {
    var uploadTasks: [FileUploadTask] = []
    var currentPrefix: String = ""
}
  • No manual @Published wrappers
  • Better performance (fine-grained tracking)
  • Cleaner code
View model is injected at the root and accessed throughout the hierarchy:
WindowGroup {
    ContentView()
        .environment(viewModel)  // Inject
}

// In any child view:
@Environment(AppViewModel.self) private var viewModel
Native drag-and-drop with onDrop modifier:
.onDrop(of: [.fileURL], isTargeted: $isDragging) { providers in
    handleDrop(providers)
    return true
}
Right-click menus on objects:
.contextMenu {
    Button("Download", systemImage: "arrow.down") { /* ... */ }
    Button("Delete", systemImage: "trash", role: .destructive) { /* ... */ }
}

AppKit (Menu Bar)

While SwiftUI handles the main interface, AppKit is used for menu bar functionality:
Services/MenuBarManager.swift
import AppKit

@MainActor
final class MenuBarManager: NSObject {
    private var statusItem: NSStatusItem!
    private var popover: NSPopover!
    
    private func setupStatusItem() {
        statusItem = NSStatusBar.system.statusItem(
            withLength: NSStatusItem.squareLength
        )
        if let button = statusItem.button {
            button.image = NSImage(
                systemSymbolName: "arrow.up.to.line.compact",
                accessibilityDescription: "R2 Vault"
            )
            button.action = #selector(togglePopover)
            button.target = self
        }
    }
    
    private func setupPopover() {
        popover = NSPopover()
        popover.contentSize = NSSize(width: 300, height: 400)
        popover.behavior = .applicationDefined  // Never auto-dismiss
        
        let hostingController = NSHostingController(
            rootView: MenuBarView().environment(viewModel)
        )
        popover.contentViewController = hostingController
    }
}
AppKit APIs used:
  • NSStatusBar / NSStatusItem - Menu bar icon
  • NSPopover - Floating panel
  • NSHostingController - Bridge to SwiftUI
  • NSOpenPanel - File picker
  • NSPasteboard - Clipboard operations
  • NSCache - Memory caching

Networking

URLSession

All HTTP requests use Foundation’s URLSession with async/await:
Services/R2UploadService.swift
static func upload(
    fileURL: URL,
    credentials: R2Credentials,
    key: String,
    contentType: String,
    onProgress: @MainActor @escaping @Sendable (Int64, Int64) -> Void
) async throws -> UploadResult {
    var request = URLRequest(url: objectURL)
    request.httpMethod = "PUT"
    request.setValue(contentType, forHTTPHeaderField: "Content-Type")
    
    let signedRequest = AWSV4Signer.sign(
        request: request, 
        credentials: credentials
    )
    
    let delegate = UploadProgressDelegate(onProgress: onProgress)
    let session = URLSession(
        configuration: .default, 
        delegate: delegate, 
        delegateQueue: nil
    )
    
    let (data, response) = try await session.upload(
        for: signedRequest, 
        fromFile: fileURL
    )
    
    let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0
    return UploadResult(httpStatusCode: statusCode, responseBody: data)
}

async/await

Modern concurrency instead of callbacks

Progress Tracking

URLSessionTaskDelegate for upload progress

File Uploads

upload(for:fromFile:) for efficient streaming

Custom Delegates

Per-session delegates for progress callbacks

S3-Compatible API

Cloudflare R2 implements the AWS S3 API:
Upload a file:
PUT /{bucket}/{key}
Content-Type: image/jpeg
Content-Length: 123456
Authorization: AWS4-HMAC-SHA256 Credential=...
List files and folders:
GET /{bucket}?list-type=2&prefix=photos/&delimiter=/
Authorization: AWS4-HMAC-SHA256 Credential=...
Returns XML with <Contents> (files) and <CommonPrefixes> (folders).
Delete a file:
DELETE /{bucket}/{key}
Authorization: AWS4-HMAC-SHA256 Credential=...
Test connection:
HEAD /{bucket}
Authorization: AWS4-HMAC-SHA256 Credential=...

Cryptography

CryptoKit

Apple’s native cryptography framework handles AWS Signature V4:
Services/AWSV4Signer.swift
import CryptoKit

nonisolated enum AWSV4Signer {
    static func sha256Hex(_ string: String) -> String {
        let digest = SHA256.hash(data: Data(string.utf8))
        return digest.map { String(format: "%02x", $0) }.joined()
    }
    
    private static func deriveSigningKey(
        secret: String, 
        date: String, 
        region: String, 
        service: String
    ) -> SymmetricKey {
        let kSecret = SymmetricKey(data: Data(("AWS4" + secret).utf8))
        let kDate = hmac(key: kSecret, data: Data(date.utf8))
        let kRegion = hmac(key: kDate, data: Data(region.utf8))
        let kService = hmac(key: kRegion, data: Data(service.utf8))
        return hmac(key: kService, data: Data("aws4_request".utf8))
    }
    
    private static func hmac(key: SymmetricKey, data: Data) -> SymmetricKey {
        SymmetricKey(data: Data(
            HMAC<SHA256>.authenticationCode(for: data, using: key)
        ))
    }
}

SHA-256

For payload hashing and canonical request digests

HMAC-SHA256

For signature derivation (AWS Signature V4)

SymmetricKey

Type-safe key handling

Zero Dependencies

No third-party crypto libraries

Data Persistence

UserDefaults

Simple key-value storage for credentials and history:
Services/KeychainService.swift
enum KeychainService {
    private static let storageKey = "fiaxe.r2credentials"
    
    static func saveAll(_ credentials: [R2Credentials]) throws {
        let data = try JSONEncoder().encode(credentials)
        UserDefaults.standard.set(data, forKey: storageKey)
    }
    
    static func loadAll() throws -> [R2Credentials] {
        guard let data = UserDefaults.standard.data(forKey: storageKey) 
        else { return [] }
        return try JSONDecoder().decode([R2Credentials].self, from: data)
    }
}
Services/UploadHistoryStore.swift
@Observable
final class UploadHistoryStore {
    private static let storageKey = "fiaxe.uploadHistory"
    var items: [UploadItem] = []
    
    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
    }
}
Security consideration: Credentials are stored in plain text in UserDefaults. For a personal tool this is acceptable, but production apps should use the macOS Keychain.

File System (Disk Cache)

Thumbnails are cached to disk in the user’s cache directory:
Services/ThumbnailCache.swift
actor ThumbnailCache {
    private let diskCacheURL: URL = {
        let caches = FileManager.default.urls(
            for: .cachesDirectory, 
            in: .userDomainMask
        ).first!
        let dir = caches.appendingPathComponent(
            "R2VaultThumbnails", 
            isDirectory: true
        )
        try? FileManager.default.createDirectory(
            at: dir, 
            withIntermediateDirectories: true
        )
        return dir
    }()
    
    private func saveToDisk(_ image: NSImage, key: String) {
        guard let tiff = image.tiffRepresentation,
              let rep = NSBitmapImageRep(data: tiff),
              let png = rep.representation(using: .png, properties: [:])
        else { return }
        try? png.write(to: diskURL(for: key))
    }
}

Concurrency

Structured Concurrency

All async work uses Swift 6’s structured concurrency:
Every I/O operation is async:
func loadCurrentFolder() {
    Task {
        do {
            let result = try await R2BrowseService.listObjects(
                credentials: credentials,
                prefix: currentPrefix
            )
            browserObjects = result.objects
        } catch {
            browserError = error.localizedDescription
        }
    }
}
Multiple uploads/deletes run concurrently:
await withTaskGroup(of: Void.self) { group in
    for uploadTask in pending {
        group.addTask {
            await self.uploadSingleFile(uploadTask, credentials: credentials)
        }
    }
}
Uploads can be cancelled:
@Observable
final class FileUploadTask {
    var uploadTask: Task<Void, Never>?
    
    func cancel() {
        uploadTask?.cancel()
        status = .cancelled
    }
}
All UI updates run on the main thread:
@Observable  // Implicitly @MainActor
final class AppViewModel {
    var uploadTasks: [FileUploadTask] = []
}
Thread-safe caching:
actor ThumbnailCache {
    private var inFlight: [String: Task<NSImage?, Never>] = [:]
    
    func thumbnail(for key: String) async -> NSImage? {
        // Safe concurrent access
    }
}

Sendable Types

All types crossing actor boundaries are Sendable:
struct R2Credentials: Sendable, Codable { /* ... */ }
struct R2Object: Sendable { /* ... */ }
struct UploadItem: Sendable { /* ... */ }

enum R2UploadService { /* nonisolated */ }
enum AWSV4Signer { /* nonisolated */ }
nonisolated enum services can be called from any actor without await.

Media Handling

UniformTypeIdentifiers

Determines MIME types for uploads:
ViewModels/AppViewModel.swift
import UniformTypeIdentifiers

private func mimeType(for url: URL) -> String {
    if let type = UTType(filenameExtension: url.pathExtension) {
        return type.preferredMIMEType ?? "application/octet-stream"
    }
    return "application/octet-stream"
}

AVFoundation

Generates video thumbnails:
Services/ThumbnailCache.swift
import AVFoundation

private func videoThumbnail(url: URL) async -> NSImage? {
    let asset = AVURLAsset(url: url)
    let gen = AVAssetImageGenerator(asset: asset)
    gen.appliesPreferredTrackTransform = true
    gen.maximumSize = CGSize(width: 120, height: 120)
    guard let cgImage = try? await gen.image(at: .zero).image 
    else { return nil }
    return NSImage(cgImage: cgImage, size: NSSize(width: 120, height: 120))
}

QuickLook

Prevews files directly from R2:
Services/QuickLookCoordinator.swift
import Quartz

final class QuickLookCoordinator: NSObject, QLPreviewPanelDataSource {
    var previewURL: URL?
    
    func numberOfPreviewItems(in panel: QLPreviewPanel!) -> Int {
        previewURL != nil ? 1 : 0
    }
    
    func previewPanel(
        _ panel: QLPreviewPanel!, 
        previewItemAt index: Int
    ) -> QLPreviewItem! {
        previewURL as QLPreviewItem?
    }
}

XML Parsing

XMLParser (Foundation)

Custom delegate-based parser for S3 XML responses:
Services/R2BrowseService.swift
private final class ListBucketResultParser: NSObject, XMLParserDelegate {
    private var objects: [R2Object] = []
    private var folders: [R2Object] = []
    private var currentElement = ""
    private var currentText = ""
    
    func parser(
        _ parser: XMLParser, 
        didStartElement elementName: String, 
        namespaceURI: String?, 
        qualifiedName qName: String?, 
        attributes attributeDict: [String: String] = [:]
    ) {
        currentElement = elementName
        currentText = ""
        if elementName == "Contents" { inContents = true }
    }
    
    func parser(_ parser: XMLParser, foundCharacters string: String) {
        currentText += string
    }
    
    func parser(
        _ parser: XMLParser, 
        didEndElement elementName: String, 
        namespaceURI: String?, 
        qualifiedName qName: String?
    ) {
        switch elementName {
        case "Key": currentKey = currentText
        case "Size": currentSize = Int64(currentText) ?? 0
        case "LastModified": currentLastModified = iso8601.date(from: currentText)
        default: break
        }
    }
}
S3’s ListObjectsV2 returns XML, not JSON. A custom parser extracts files (<Contents>) and folders (<CommonPrefixes>).

Developer Tools

Git

Version control with GitHub

Xcode

IDE with Swift 6 support

GitHub Actions

CI/CD for automated releases

sparkle-project

Auto-update framework (future)

Notable Absences

Zero third-party dependenciesr2Vault is built entirely with native Apple frameworks:
  • No CocoaPods
  • No Swift Package Manager dependencies
  • No external SDKs
This keeps the codebase simple, secure, and maintainable.

Summary

CategoryTechnologies
LanguageSwift 6
UISwiftUI, AppKit (menu bar)
NetworkingURLSession, S3 API
CryptoCryptoKit (SHA-256, HMAC-SHA256)
PersistenceUserDefaults, FileManager
Concurrencyasync/await, TaskGroup, Actor
MediaAVFoundation, UniformTypeIdentifiers, QuickLook
ParsingXMLParser (Foundation)
PlatformmacOS 14.0+

Next Steps