-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Optionals
-
Section 4: Object-Oriented and Value Types
-
Section 5: Memory Management
-
Section 6: Generics and Error Handling
-
Section 7: Concurrency
-
Section 8: Working with Collections
-
Section 9: Codable and Data Handling
-
Section 10: Protocol-Oriented Programming
-
Section 11: Testing and Tooling
-
Section 12: Practical Projects
-
Section 13: Interview Practice
-
Section 14: More Practice Exercises
-
Section 15: More Standard Library
-
Section 16: Advanced Concurrency
-
Section 17: Foundation Framework Deep Dive
-
Section 18: URLSession and Networking Deep Dive
-
Section 19: Combine Framework
-
Section 20: SwiftUI Fundamentals for Swift Developers
-
Section 21: Server-Side Swift with Vapor
-
Section 22: Swift Package Manager Deep Dive
-
Section 23: Swift Concurrency Deep Dive
-
Section 24: More Language Features
-
Section 25: Error Handling Deep Dive
-
Section 26: Testing Deep Dive
-
Section 27: Data Structures and Algorithms in Swift
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Swift Macros (Swift 5.9+)
-
Section 31: Property Wrappers Ecosystem
-
Section 32: Swift Interop Deep Dive
-
Section 33: iOS App Architecture Patterns
-
Section 34: Performance and Debugging
-
Section 35: App Distribution and CI/CD
-
Section 36: More Practical Projects
-
Section 37: SwiftData and Persistence
-
Section 38: More Design Patterns
-
Section 39: More Review and Practice
-
Section 40: More Foundation Deep Dive
-
Section 41: Advanced Collections in Swift
-
Section 42: Advanced Generics Practice
-
Section 43: UIKit for Legacy and Hybrid Apps
-
Section 44: watchOS and visionOS Development Basics
-
Section 45: More Networking Patterns
-
Section 46: More Testing Practice
-
Section 47: Accessibility in Swift Apps
-
Section 48: Localization
-
Section 49: More Practical Projects Round 2
-
Section 50: Swift Charts Framework
-
Section 51: More Interview and Algorithm Practice
-
Section 52: Final Practice and Mastery
-
Section 53: Swift Compiler and Build System
-
Section 54: More Concurrency Practice
-
Section 55: App Store Guidelines and Review
-
Section 56: More Design and Architecture
127: Practice Exercise: Building an Offline-First Data Sync Layer
I've seen a lot of developers approach offline-first architecture with a dangerous assumption: that "offline-first" is just a fancy way of saying "cache the data and upload it when the connection returns." It sounds simple, right? You save a record to a local database, and once the NWPathMonitor tells you the internet is back, you fire off a POST request. But if you build it this way, you aren't building a sync layer—you're building a data-loss engine.
The "Last Write Wins" Fallacy
Here is exactly where that logic falls apart. Imagine you're building a collaborative Travel Itinerary app. You and a partner are both editing the same "Day 1: Paris" itinerary. You go into a subway tunnel (offline) and change the hotel name to "Hotel Ritz." Simultaneously, your partner (online) changes the hotel name to "Hotel Lumière."
If your logic is simply "upload when connected," the moment you exit that tunnel, your app will send the "Hotel Ritz" update to the server. The server, seeing a newer request, overwrites the partner's "Hotel Lumière" change. Your partner's work is gone, and they have no idea why. This is the "Last Write Wins" problem, and in a professional production environment, it's a bug that will get you a lot of angry Jira tickets.
Tracking Intent with Sync States and Timestamps
To do this right, we have to stop thinking about "saving data" and start thinking about "tracking intent." We don't just save the current state of an object; we save the fact that a change was requested. I usually implement this by adding a syncStatus enum and a lastModified timestamp to every synchronizable entity.
enum SyncStatus: String, Codable {
case synced // Matches the server
case pendingCreate // New record, not yet on server
case pendingUpdate // Modified locally, needs upload
case deleted // Marked for deletion locally
}
By tagging your data this way, your sync layer doesn't just blindly upload everything. It queries for anything that isn't .synced. More importantly, when the server responds, you compare timestamps. If the server's updatedAt is newer than the local timestamp from when the user started their edit, you've found a conflict. Now you can actually handle it—either by merging the changes, prompting the user, or using a deterministic rule—instead of just wiping out data.
The Synchronization Loop
The actual architecture I recommend is a unidirectional loop. You never let your UI talk directly to the API; the UI talks to the Local Store, and a separate SyncManager manages the bridge to the API. This keeps your app snappy because the UI is always reading from the local disk, never waiting on a network request.
- Pull: Fetch changes from the server. If a remote record is newer than the local one, update the local store.
- Resolve: Identify local records marked as
.pendingUpdatethat also have newer versions on the server. This is where your conflict logic lives. - Push: Send all
.pendingCreateand.pendingUpdaterecords to the server. - Acknowledge: Once the server confirms receipt, flip the local status back to
.synced.
It's a bit more boilerplate up front, but it's the only way to ensure that your users don't lose their data the moment they walk into an elevator.
📋 Practical Task
Exercise: Implementing a Conflict-Aware Sync Manager for a Recipe Book
You are tasked with building the sync logic for a "Recipe Book" app. The app allows users to edit ingredients offline. Your goal is to implement a SyncManager that prevents accidental data overwrites using timestamps.
Requirements:
- Create a
Recipestruct that includes anid,title,lastModified(Date), and aSyncStatus(synced, pendingUpdate, pendingCreate). - Implement a
SyncManagerclass with a methodprocessSync(localRecipe: Recipe, serverRecipe: Recipe) -> Recipe. - Conflict Logic:
- If the
localRecipeis.synced, always take theserverRecipe. - If the
localRecipeis.pendingUpdatebut theserverRecipe.lastModifiedis newer than thelocalRecipe.lastModified, the server wins (simulate a conflict resolution by returning the server version). - If the
localRecipeis.pendingUpdateand is newer than the server version, return the local version and mark it as ready for upload.
- If the
- Write a small test suite in a
main.swiftfile that simulates three scenarios: a clean update, a server-wins conflict, and a local-wins conflict.
Tip: Use Date().addingTimeInterval() to easily create timestamps that are precisely in the past or future for your test cases.
There are no comments for now.