Skip to content

muhittincamdali/iOS-Offline-First-Framework

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

71 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

iOS Offline-First Framework

   ____  ______ ______ __    _____   __ ______
  / __ \/ ____// ____// /   /  _/ | / // ____/
 / / / / /_   / /_   / /    / / /  |/ // __/   
/ /_/ / __/  / __/  / /____/ / / /|  // /___   
\____/_/    /_/    /_____/___//_/ |_//_____/   
                                               
    β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—
    β–ˆβ–ˆβ•”β•β•β•β•β•β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β•β•β•β•šβ•β•β–ˆβ–ˆβ•”β•β•β•
    β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—  β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—   β–ˆβ–ˆβ•‘   
    β–ˆβ–ˆβ•”β•β•β•  β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β•šβ•β•β•β•β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘   
    β–ˆβ–ˆβ•‘     β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘  β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘   
    β•šβ•β•     β•šβ•β•β•šβ•β•  β•šβ•β•β•šβ•β•β•β•β•β•β•   β•šβ•β•   
         Work Offline. Sync Later.

Swift iOS SPM License Version

The most comprehensive offline-first framework for iOS. Production-ready with CRDT support, real encryption, delta sync, and multi-device collaboration.

Features β€’ Installation β€’ Quick Start β€’ Architecture β€’ Docs


πŸš€ What's New in 2.0

  • CRDT Support - Conflict-free replicated data types for automatic conflict resolution
  • Real Encryption - AES-256-GCM & ChaCha20-Poly1305 with secure key management
  • Delta Sync - Only sync what changed, save bandwidth
  • Multi-Device Sync - Real-time collaboration across all your devices
  • Bandwidth Optimization - Adaptive chunking and smart queue management
  • Background Sync - BGTaskScheduler integration for reliable background updates
  • Optimistic UI - Instant UI updates with automatic rollback on failure

πŸ“‹ Table of Contents


Why Offline-First?

Users expect apps to work everywhere β€” in subways, airplanes, rural areas, or during network outages. Offline-first isn't just a feature; it's a user expectation.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  "The best user experience is one that works, period."      β”‚
β”‚                                        β€” Every User Ever    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

✨ Features

Core Capabilities

Feature Description Status
πŸ’Ύ Local Storage Encrypted file-based persistence βœ…
πŸ”„ Auto Sync Background synchronization with BGTaskScheduler βœ…
βš”οΈ Conflict Resolution CRDT-based automatic resolution βœ…
πŸ“‹ Retry Queue Exponential backoff with persistence βœ…
πŸ“‘ Network Monitor Real-time connectivity & quality detection βœ…
πŸ” Encryption AES-256-GCM at-rest encryption βœ…
πŸ—œοΈ Compression LZ4/ZLIB/LZMA/LZFSE compression βœ…
πŸ“Š Delta Sync Efficient change-only synchronization βœ…
πŸ“± Multi-Device Real-time cross-device sync βœ…
⚑ Optimistic UI Instant updates with auto-rollback βœ…
πŸ“Ά Bandwidth Optimizer Adaptive transfer management βœ…

Conflict Resolution Strategies

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  Conflict Resolution                         β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Last Write     β”‚  Most recent timestamp wins               β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Server Wins    β”‚  Remote data always takes precedence      β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Client Wins    β”‚  Local changes always take precedence     β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Custom Merge   β”‚  Your logic decides the outcome           β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  CRDT           β”‚  Automatic conflict-free merging          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Architecture

graph TD
    A[πŸ‘€ User Action] --> B{πŸ“‘ Network?}
    B -->|Online| C[🌐 Remote API]
    B -->|Offline| D[πŸ’Ύ Local Store]
    C --> E[⬇️ Sync to Local]
    D --> F[πŸ“‹ Queue Operation]
    F --> G{πŸ“‘ Back Online?}
    G -->|Yes| H[⬆️ Process Queue]
    H --> I[πŸ”„ Delta Sync]
    I --> C
    E --> J[βœ… Data Available]
    D --> J
    
    style A fill:#4CAF50,color:#fff
    style B fill:#FF9800,color:#fff
    style C fill:#2196F3,color:#fff
    style D fill:#9C27B0,color:#fff
    style J fill:#4CAF50,color:#fff
Loading

System Components

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        OfflineFirst Framework                            β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚    CRDT     β”‚  β”‚  Encryption β”‚  β”‚ Compression β”‚  β”‚ Delta Sync  β”‚   β”‚
β”‚  β”‚   Engine    β”‚  β”‚   Engine    β”‚  β”‚   Engine    β”‚  β”‚   Engine    β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚   Retry     β”‚  β”‚  Background β”‚  β”‚  Bandwidth  β”‚  β”‚ Multi-Deviceβ”‚   β”‚
β”‚  β”‚   Queue     β”‚  β”‚    Sync     β”‚  β”‚  Optimizer  β”‚  β”‚    Sync     β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚  Optimistic β”‚  β”‚   Network   β”‚  β”‚   Storage   β”‚  β”‚  Analytics  β”‚   β”‚
β”‚  β”‚     UI      β”‚  β”‚   Monitor   β”‚  β”‚   Manager   β”‚  β”‚   Manager   β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Installation

Swift Package Manager

dependencies: [
    .package(
        url: "https://github.com/Nahodan/iOS-Offline-First-Framework.git",
        from: "2.0.0"
    )
]

Then import:

import OfflineFirstFramework

Quick Start

1. Initialize the Framework

import OfflineFirstFramework

// Initialize with default configuration
let offlineManager = OfflineFirstManager.shared
offlineManager.initialize(with: OfflineFirstConfiguration())

2. Define Your Model

struct Task: Syncable {
    let id: String
    var title: String
    var isCompleted: Bool
    var lastModified: Date
    var version: Int64
    var isDirty: Bool
    
    var syncId: String { id }
}

3. Save Data (Works Offline!)

let task = Task(
    id: UUID().uuidString,
    title: "Buy groceries",
    isCompleted: false,
    lastModified: Date(),
    version: 1,
    isDirty: true
)

// Saves locally, queues for sync if offline
offlineManager.save(task)
    .subscribe(onNext: { result in
        print("Saved: \(result)")
    })
    .disposed(by: disposeBag)

4. Monitor Sync Status

offlineManager.syncStatus
    .subscribe(onNext: { status in
        switch status {
        case .idle:
            print("Ready to sync")
        case .inProgress:
            print("Syncing...")
        case .completed:
            print("Sync complete!")
        case .failed(let error):
            print("Sync failed: \(error)")
        }
    })
    .disposed(by: disposeBag)

πŸ”§ Core Components

CRDT Types

Conflict-free Replicated Data Types for automatic merge:

// Last-Writer-Wins Register
var counter = LWWRegister(value: "Hello", nodeId: deviceId)
counter = counter.update("World")

// OR-Set (add/remove support)
var set = ORSet<String>(nodeId: deviceId)
set = set.add("item1")
set = set.remove("item1")

// PN-Counter (increment/decrement)
var visits = PNCounter(nodeId: deviceId)
visits = visits.increment()
visits = visits.decrement()

// Merge with remote
let merged = counter.merge(with: remoteCounter)

Encryption

Production-ready encryption with CryptoKit:

let encryption = EncryptionEngine()

// Encrypt with master key
let encrypted = try encryption.encrypt(data)

// Decrypt
let decrypted = try encryption.decrypt(encrypted)

// Password-based encryption
let encryptedWithPassword = try encryption.encrypt(data, password: "secret")

Compression

Multiple algorithms with adaptive selection:

let compression = CompressionEngine(configuration: .default)

// Compress
let compressed = try compression.compress(data)
print("Ratio: \(compressed.compressionRatio)")

// Decompress
let original = try compression.decompress(compressed)

Delta Sync

Only sync what changed:

let deltaEngine = DeltaSyncEngine()

// Detect changes
let change = try await deltaEngine.detectChanges(
    old: oldTask,
    new: newTask,
    entityType: "Task"
)

// Apply patch
let patched = try await deltaEngine.applyPatch(change.patch!, to: oldTask)

Retry Queue

Persistent queue with exponential backoff:

let queue = RetryQueue(configuration: .default)

// Queue operation
let operation = OperationBuilder()
    .type(.update)
    .entity(id: "123", type: "Task")
    .payload(try JSONEncoder().encode(task))
    .priority(.high)
    .build()

await queue.enqueue(operation)

// Monitor progress
queue.statusPublisher
    .sink { status in
        print("Pending: \(status.pendingCount)")
    }
    .store(in: &cancellables)

Background Sync

Reliable background updates with BGTaskScheduler:

// In AppDelegate
let scheduler = BackgroundSyncScheduler()
scheduler.registerBackgroundTask()
scheduler.setSyncHandler(DefaultSyncHandler(retryQueue: queue))
scheduler.enable()

// Check last sync
if let result = scheduler.lastSyncResult {
    print("Last sync: \(result.itemsSynced) items")
}

Optimistic UI

Instant updates with auto-rollback:

let store = ObservableEntityStore<Task>()
let optimistic = OptimisticUpdateManager(store: store)

// Update with instant UI feedback
optimistic.optimisticUpdate(updatedTask) {
    try await api.update(updatedTask)
}

// SwiftUI support
TaskRow(task: task)
    .pendingOverlay(optimistic.isPending(entityId: task.id))

Multi-Device Sync

Real-time collaboration:

let multiDevice = MultiDeviceSyncManager(
    configuration: .default(deviceName: UIDevice.current.name)
)

multiDevice.start()

// Send changes to all devices
try await multiDevice.sendDelta(changes)

// Handle incoming changes
multiDevice.setMessageHandler { message in
    // Process sync message
}

// Monitor connected devices
multiDevice.devicesPublisher
    .sink { devices in
        print("Connected: \(devices.count) devices")
    }
    .store(in: &cancellables)

Bandwidth Optimizer

Smart transfer management:

let bandwidth = BandwidthOptimizer(configuration: .default)

// Queue transfer
let taskId = await bandwidth.queueTransfer(
    TransferTask(type: .upload, dataSize: fileSize, priority: .normal)
)

// Monitor network quality
bandwidth.qualityPublisher
    .sink { quality in
        if quality.isPoor {
            print("Poor connection - deferring large syncs")
        }
    }
    .store(in: &cancellables)

// Check if should defer
if await bandwidth.shouldDeferSync(priority: .low) {
    // Wait for better connection
}

πŸ“š Advanced Features

Custom Conflict Resolution

let engine = ConflictResolutionManager()

// Detect conflicts
let conflicts = engine.detectConflicts(local: localTask, remote: remoteTask)

// Custom resolution
engine.autoResolveConflicts(conflicts, strategy: .custom { local, remote in
    // Your merge logic
    var merged = remote
    merged.localNotes = local.localNotes
    return merged
})

Network Quality Monitoring

let networkMonitor = NetworkStateManager()

networkMonitor.isOnline
    .distinctUntilChanged()
    .subscribe(onNext: { isOnline in
        if isOnline {
            syncEngine.syncNow()
        }
    })
    .disposed(by: disposeBag)

Requirements

Requirement Version
iOS 15.0+
macOS 12.0+
tvOS 15.0+
watchOS 8.0+
Xcode 15.0+
Swift 5.9+

Documentation

Detailed documentation available in Documentation/:


Contributing

Contributions are welcome! Please read the Contributing Guide first.

  1. Fork the repo
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'feat: add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

License

This project is licensed under the MIT License β€” see the LICENSE file for details.


Author

Muhittin Camdali β€” @Nahodan


Built with ❀️ for apps that work everywhere


πŸ“ˆ Star History

Star History Chart

Sponsor this project

Packages

 
 
 

Contributors

Languages