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:
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
Three-column navigation for macOS: NavigationSplitView {
// Sidebar
} detail : {
// Main content
}
Native drag-and-drop with onDrop modifier: . onDrop ( of : [. fileURL ], isTargeted : $isDragging) { providers in
handleDrop (providers)
return true
}
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
}
}
}
TaskGroup (Parallel Execution)
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.
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>).
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 dependencies r2Vault 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
Category Technologies Language Swift 6 UI SwiftUI, AppKit (menu bar) Networking URLSession, S3 API Crypto CryptoKit (SHA-256, HMAC-SHA256) Persistence UserDefaults, FileManager Concurrency async/await, TaskGroup, Actor Media AVFoundation, UniformTypeIdentifiers, QuickLook Parsing XMLParser (Foundation) Platform macOS 14.0+
Next Steps