-
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
214: Building a Simple Journaling App with CloudKit Sync
Up until now, we've been keeping our data local. That's fine for a prototype, but for a journaling app, it's a non-starter. If a user writes a heartfelt entry on their iPad and then opens their iPhone to find a blank screen, they're going to delete your app. Today, we're building a minimal sync engine for a journaling app I'm calling "MindDump."
We'll be using CloudKit. I prefer it for these types of apps because it handles the authentication via the user's iCloud account, meaning we don't have to build a custom login system. Let's dive in.
Connecting to the Private Database
First, we need to decide where the data lives. CloudKit gives us public, private, and shared databases. Since a journal is deeply personal, we're going exclusively with the privateCloudDatabase. The user is the only one who can see this data, and it doesn't count against your app's developer storage quota—it counts against theirs.
import CloudKit
class CloudKitManager {
let container = CKContainer.default()
let privateDB = CKContainer.default().privateCloudDatabase
// We'll use this to identify our record type in the dashboard
let recordType = "JournalEntry"
}
I'm wrapping this in a manager class. It keeps the view controllers from getting cluttered with CloudKit boilerplate, which can get messy quickly.
Turning a Journal Entry into a CKRecord
CloudKit doesn't store Swift structs directly. It uses CKRecord, which is essentially a dictionary of keys and values. I've already defined a JournalEntry struct in a previous lesson, so now we just need a way to convert it.
extension CloudKitManager {
func saveEntry(_ entry: JournalEntry, completion: @escaping (Result<Void, Error>) -> Void) {
let record = CKRecord(recordType: recordType)
record["content"] = entry.text as CKRecordValue
record["timestamp"] = entry.date as CKRecordValue
privateDB.save(record) { _, error in
if let error = error {
completion(.failure(error))
} else {
completion(.success(()))
}
}
}
}
The "Why is this crashing?" Moment
At this point, I usually hit "Run," try to save an entry, and the app immediately throws a permission error or crashes. I did it again this morning. I wrote the code perfectly, but I forgot the most annoying part of CloudKit: the Capabilities.
If you're seeing an error about "container not found" or "permission denied," it's because you haven't told Xcode that this app is allowed to talk to iCloud. You have to go to your Target settings, hit + Capability, add iCloud, and then check the CloudKit box. You also need to create a container ID (usually iCloud.com.yourname.MindDump). I always forget this because it's a project setting, not a code setting. Don't be like me.
Bringing the Journal Entries Home
Saving is the easy part. The real work is fetching the entries back down and sorting them so the newest journal entry is at the top. We'll use a CKQuery for this.
func fetchEntries(completion: @escaping (Result<[JournalEntry], Error>) -> Void) {
let predicate = NSPredicate(value: true) // We want everything
let query = CKQuery(recordType: recordType, predicate: predicate)
query.sortDescriptors = [NSSortDescriptor(key: "timestamp", ascending: false)]
privateDB.perform(query, inZoneWith: nil) { records, error in
if let error = error {
completion(.failure(error))
return
}
let entries = records?.compactMap { record in
guard let text = record["content"] as? String,
let date = record["timestamp"] as? Date else { return nil }
return JournalEntry(text: text, date: date)
} ?? []
completion(.success(entries))
}
}
One thing to keep in mind: perform(query:) is fine for a small journal, but if your user writes ten thousand entries, this will eventually time out or hit a limit. For a production app, you'd want to use CKQueryOperation and cursor to paginate the results. For MindDump, this is plenty.
📋 Practical Task
Exercise: Implementing the "Sync Now" Manual Refresh
Currently, our CloudKitManager has the logic to fetch and save, but the UI doesn't trigger it. Your task is to integrate this manager into a SwiftUI View.
- Create a
JournalViewModelthat holds an array ofJournalEntryobjects. - Add a function to the ViewModel called
syncWithCloud()that callsfetchEntriesand updates the published array on the main thread. - In your View, add a
Buttonlabeled "Sync with iCloud" that triggers this function. - Add a simple
ProgressView(spinner) that shows up while the fetch is in progress and disappears once thecompletionhandler returns.
There are no comments for now.