SyncVault-Swift is a pure, native Swift offline-first "Store-and-Forward" sync engine for Apple platforms. Zero external dependencies. Drop it in.
- π Offline-First β Requests automatically queue when offline
- π Auto-Sync β Queued requests sync when connectivity returns
- β±οΈ Exponential Backoff β Smart retry logic with configurable delays
- π Thread-Safe β Built with Swift Actors for safe concurrent access
- π‘ Observable β AsyncStream events for SwiftUI integration
- π± Multi-Platform β iOS, macOS, watchOS, tvOS support
- π« Zero Dependencies β Pure native Swift with URLSession & NWPathMonitor
- π§ Auto Headers β Automatically sets
Content-Type,Content-Length, andAcceptheaders
Add SyncVault to your Package.swift:
dependencies: [
.package(url: "https://github.com/brownboycodes/SyncVault-Swift.git", from: "1.0.0")
]Or in Xcode:
- File β Add Package Dependencies...
- Enter:
https://github.com/brownboycodes/SyncVault-Swift.git - Select version and add to your target
Add to your Podfile:
pod 'SyncVault', '~> 1.0'Then run:
pod installimport SyncVault
// Make a request (automatically queues if offline)
do {
let response = try await SyncVault.shared.request(
url: URL(string: "https://api.example.com/data")!,
method: "POST",
body: jsonData,
headers: ["Authorization": "Bearer \(token)"]
)
// Handle response
} catch SyncVaultError.queued(let id) {
// Request was queued for later sync
print("Queued for sync: \(id)")
}// GET request
let (data, response) = try await SyncVault.shared.get(
URL(string: "https://api.example.com/users")!
)
// POST request with body
// Use JSONSerialization for maximum server compatibility
let bodyDict: [String: Any] = ["name": "John", "email": "john@example.com"]
let bodyData = try JSONSerialization.data(withJSONObject: bodyDict)
let (data, response) = try await SyncVault.shared.post(
URL(string: "https://api.example.com/users")!,
body: bodyData
// Headers are automatically set:
// - Content-Type: application/json; charset=utf-8
// - Content-Length: <body size>
// - Accept: application/json
)
// PUT, PATCH, DELETE also available
try await SyncVault.shared.put(url, body: data)
try await SyncVault.shared.patch(url, body: data)
try await SyncVault.shared.delete(url)struct User: Codable {
let name: String
let email: String
}
// Option 1: Using JSONSerialization (recommended for maximum server compatibility)
let userDict: [String: Any] = ["name": user.name, "email": user.email]
let bodyData = try JSONSerialization.data(withJSONObject: userDict)
let (data, response) = try await SyncVault.shared.post(
URL(string: "https://api.example.com/users")!,
body: bodyData
)
// Option 2: Using JSONEncoder to encode your model, then pass Data to post()
let bodyData = try JSONEncoder().encode(user)
let (data, response) = try await SyncVault.shared.post(
URL(string: "https://api.example.com/users")!,
body: bodyData
)// Listen for all sync events
Task {
for await event in SyncVault.shared.events {
switch event {
case .queued(let action):
print("Queued: \(action)")
case .success(let action, _, let statusCode):
print("Success (\(statusCode)): \(action)")
case .retrying(let action, _, let attempt):
print("Retrying (\(attempt)): \(action)")
case .deadLetter(let action, _):
print("Failed permanently: \(action)")
case .connectivityChanged(let isOnline):
print("Online: \(isOnline)")
case .processingStarted(let count):
print("Processing \(count) items...")
case .processingCompleted(let success, let failed):
print("Done: \(success) success, \(failed) failed")
default:
break
}
}
}import SwiftUI
import SyncVault
struct ContentView: View {
@State private var pendingCount = 0
@State private var isOnline = true
@State private var items: [Item] = []
var body: some View {
VStack {
// Status indicator
HStack {
Circle()
.fill(isOnline ? .green : .red)
.frame(width: 10, height: 10)
Text(isOnline ? "Online" : "Offline")
Spacer()
if pendingCount > 0 {
Text("\(pendingCount) pending")
.foregroundColor(.orange)
}
}
.padding()
// Action buttons
Button("Create Item") {
Task {
await createItem()
}
}
Button("Sync Now") {
Task { await SyncVault.shared.sync() }
}
}
.task {
// Observe events
for await event in SyncVault.shared.events {
if case .connectivityChanged(let online) = event {
isOnline = online
}
pendingCount = await SyncVault.shared.pendingCount()
}
}
}
func createItem() async {
do {
// Create JSON body using JSONSerialization
let itemDict: [String: Any] = [
"title": "New Item",
"description": "Created from iOS"
]
let bodyData = try JSONSerialization.data(withJSONObject: itemDict)
let (data, _) = try await SyncVault.shared.post(
URL(string: "https://api.example.com/items")!,
body: bodyData
)
// Parse response
if let data = data {
let item = try JSONDecoder().decode(Item.self, from: data)
items.append(item)
}
} catch SyncVaultError.queued(let id) {
print("Queued for later: \(id)")
} catch {
print("Error: \(error)")
}
}
}// Get pending count
let count = await SyncVault.shared.pendingCount()
// Get all pending actions
let actions = await SyncVault.shared.pendingActions()
// Remove a specific action
await SyncVault.shared.removeAction(actionId)
// Clear entire queue
await SyncVault.shared.clearQueue()
// Manually trigger sync
let result = await SyncVault.shared.sync()
print("Processed: \(result.succeeded) success, \(result.failed) failed")// Disable auto-sync when coming online
SyncVault.shared.disableAutoSync()
// Re-enable auto-sync
SyncVault.shared.enableAutoSync()
// Check current online status
if SyncVault.shared.isOnline {
// Device is connected
}let config = SyncClient.Configuration(
workerConfiguration: QueueWorker.Configuration(
baseRetryDelay: 2.0, // Initial retry delay
maxRetryDelay: 60.0, // Maximum retry delay
requestTimeout: 30.0, // Request timeout
stopOnError: true // Stop queue processing on error
),
autoSyncOnConnectivity: true, // Auto-sync when coming online
logLevel: .debug // Log level
)
let client = try SyncClient(configuration: config)// Implement StorageAdapter protocol for custom backends
class CoreDataStorageAdapter: StorageAdapter {
func save(_ action: SyncAction) async throws { /* ... */ }
func getAll() async throws -> [SyncAction] { /* ... */ }
func remove(_ id: UUID) async throws { /* ... */ }
func update(_ action: SyncAction) async throws { /* ... */ }
func count() async throws -> Int { /* ... */ }
func clear() async throws { /* ... */ }
}
let client = SyncClient(storage: CoreDataStorageAdapter())SyncVault automatically logs failed requests with a cURL command for easy debugging:
// When a request fails, check the console for output like:
// ============================================================
// π§ FAILED REQUEST - Copy this cURL command to debug:
// ------------------------------------------------------------
// curl -v \
// -X POST \
// -H 'Accept: application/json' \
// -H 'Content-Length: 42' \
// -H 'Content-Type: application/json; charset=utf-8' \
// -d '{"title":"Test","body":"Hello"}' \
// 'https://api.example.com/posts'
// ============================================================
// You can also generate the cURL command manually:
let action = SyncAction(
url: URL(string: "https://api.example.com/posts")!,
httpMethod: "POST",
body: jsonData
)
print(action.toCurlCommand())SyncVault is perfect for Apple Watch apps that frequently go offline:
// In your watchOS app
struct WorkoutView: View {
var body: some View {
Button("Save Workout") {
Task {
do {
// Create workout data
let workoutDict: [String: Any] = [
"type": "Running",
"duration": 1800,
"calories": 250
]
let bodyData = try JSONSerialization.data(withJSONObject: workoutDict)
try await SyncVault.shared.post(
URL(string: "https://api.example.com/workouts")!,
body: bodyData
)
} catch SyncVaultError.queued {
// Workout saved locally, will sync when watch reconnects
}
}
}
}
}βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β SyncClient β
β (Public API - Singleton Entry Point) β
βββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββΌββββββββββββββ
βΌ βΌ βΌ
βββββββββββββ ββββββββββββββ ββββββββββββββββ
β Network β β Queue β β Storage β
β Monitor β β Worker β β Adapter β
β β β (Actor) β β (Protocol) β
βββββββββββββ ββββββββββββββ ββββββββββββββββ
β β β
β β βΌ
β β ββββββββββββββββ
β β β File β
β β β Storage β
β β β (Default) β
β β ββββββββββββββββ
β β
βΌ βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β URLSession β
β (Native Apple API) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- Capture: User calls
SyncVault.request(...) - Check: If online β execute via
URLSession - Queue: If offline β persist to local storage (JSON files)
- Sync: When connectivity returns β process queue (FIFO) with exponential backoff
SyncVault automatically sets the following headers for requests with a body:
| Header | Value | Purpose |
|---|---|---|
Content-Type |
application/json; charset=utf-8 |
Tells server the body format |
Content-Length |
<body size> |
Required by some servers |
Accept |
application/json |
Requests JSON response |
You can override these by passing custom headers.
Sources/SyncVault/
βββ Core/
β βββ SyncClient.swift # Main entry point (singleton)
β βββ QueueWorker.swift # Background processing (actor)
β βββ NetworkMonitor.swift # NWPathMonitor wrapper
βββ Storage/
β βββ StorageAdapter.swift # Protocol for storage backends
β βββ FileStorageAdapter.swift # Default JSON file storage
βββ Models/
β βββ SyncAction.swift # Request model (Codable)
β βββ SyncEvent.swift # Observable events
β βββ SyncVaultError.swift # Custom errors
βββ Utils/
β βββ Logger.swift # Logging utility
βββ SyncVault.swift # Public exports
| Platform | Minimum Version |
|---|---|
| iOS | 15.0 |
| macOS | 12.0 |
| watchOS | 8.0 |
| tvOS | 15.0 |
| Swift | 5.9+ |
If you're getting 500 errors, check:
-
Use JSONSerialization for creating request bodies (most compatible):
let dict: [String: Any] = ["key": "value"] let body = try JSONSerialization.data(withJSONObject: dict)
-
Verify JSON is valid before sending:
guard QueueWorker.isValidJSON(bodyData) else { print("Invalid JSON!") return }
-
Check the cURL output in console logs to debug manually
- Ensure the device has network connectivity:
SyncVault.shared.isOnline - Check if auto-sync is enabled
- Manually trigger sync:
await SyncVault.shared.sync()
- Nabhodipta Garai
- GitHub: @brownboycodes
- LinkedIn: linkedin.com/in/brownboycodes
- Ajay Verma
- GitHub: @ajayverma050698
- LinkedIn: linkedin.com/in/ajay-verma-7921b01b1
SyncVault-Swift is available under the MIT License. See the LICENSE file for details.
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
"Don't bloat your app with Alamofire just for offline sync. SyncVault-Swift is pure, native Swift. Zero dependencies. Drop it in."
- For iOS/macOS devs: Native API integration, no third-party risk
- For watchOS devs: Critical for health/fitness apps that operate offline
- For enterprise teams: Minimal dependencies = minimal audit surface
- For everyone: Modern Swift Concurrency (async/await, Actors)
Made with β€οΈ for the Apple Developer Community
