Swift Combine Series: Part 5 — Combine with Core Data (Persistence Made Easy!)
Welcome back to our Swift Combine journey! 🚀 If you’ve made it this far, give yourself a high-five! 🙌 Today, we’re diving into Combine with Core Data, because what’s the point of all this reactive magic if we can’t persist our data?
📌 Previous Parts:
Why Combine and Core Data?
“A persistent app is a happy app.” — Some wise developer (probably you, after reading this)
Core Data is Apple’s powerful framework for managing local databases. Traditionally, saving and fetching data required callbacks and boilerplate code. But Combine simplifies all of that with publishers, making everything declarative and reactive! 🎉
Setting Up Core Data with Combine
Before we get to the fun stuff, let’s set up Core Data in our project.
Step 1: Create a Core Data Model
Define an Entity called Task
with the following attributes:
title
(String)isCompleted
(Boolean)
Now, generate the NSManagedObject subclass for Task
.
Step 2: Create the Core Data Stack
We need a persistent container to interact with our database.
import CoreData
import Combine
class CoreDataManager {
static let shared = CoreDataManager()
let persistentContainer: NSPersistentContainer
private init() {
persistentContainer = NSPersistentContainer(name: "TaskModel")
persistentContainer.loadPersistentStores { (_, error) in
if let error = error {
fatalError("Failed to load Core Data stack: \(error)")
}
}
}
var context: NSManagedObjectContext {
return persistentContainer.viewContext
}
}
Fetching Data with Combine
“Fetching data shouldn’t feel like fetching coffee — no waiting in long lines!” ☕️
Here’s how we reactively fetch data from Core Data:
class TaskRepository {
private var cancellables = Set<AnyCancellable>()
private let context = CoreDataManager.shared.context
func fetchTasks() -> AnyPublisher<[Task], Never> {
let fetchRequest: NSFetchRequest<Task> = Task.fetchRequest()
return Future { promise in
do {
let tasks = try self.context.fetch(fetchRequest)
promise(.success(tasks))
} catch {
print("Failed to fetch tasks: \(error)")
promise(.success([]))
}
}
.eraseToAnyPublisher()
}
}
Using the Fetch Function
let taskRepo = TaskRepository()
taskRepo.fetchTasks()
.sink(receiveValue: { tasks in
tasks.forEach { print($0.title ?? "No Title") }
})
.store(in: &cancellables)
Boom! 💥 We just fetched data reactively without breaking a sweat. 😎
Saving Data with Combine
“Persistence is key — in apps and in life!” 🔑
extension TaskRepository {
func saveTask(title: String) -> AnyPublisher<Void, Never> {
Future { promise in
let task = Task(context: self.context)
task.title = title
task.isCompleted = false
do {
try self.context.save()
promise(.success(()))
} catch {
print("Failed to save task: \(error)")
promise(.success(()))
}
}
.eraseToAnyPublisher()
}
}
Using the Save Function
let taskRepo = TaskRepository()
taskRepo.saveTask(title: "Write Swift Combine Blog")
.sink {
print("Task saved successfully! 🎉")
}
.store(in: &cancellables)
Easy, right? No more callbacks, just Combine goodness! ✨
Updating Data with Combine
“Change is inevitable, even for Core Data records!”
extension TaskRepository {
func updateTask(_ task: Task, isCompleted: Bool) -> AnyPublisher<Void, Never> {
Future { promise in
task.isCompleted = isCompleted
do {
try self.context.save()
promise(.success(()))
} catch {
print("Failed to update task: \(error)")
promise(.success(()))
}
}
.eraseToAnyPublisher()
}
}
Updating a Task
let taskRepo = TaskRepository()
taskRepo.updateTask(someTask, isCompleted: true)
.sink {
print("Task updated successfully! ✅")
}
.store(in: &cancellables)
Deleting Data with Combine
“Sometimes, we just need to let things go… like unused data!” 🗑
extension TaskRepository {
func deleteTask(_ task: Task) -> AnyPublisher<Void, Never> {
Future { promise in
self.context.delete(task)
do {
try self.context.save()
promise(.success(()))
} catch {
print("Failed to delete task: \(error)")
promise(.success(()))
}
}
.eraseToAnyPublisher()
}
}
Deleting a Task
let taskRepo = TaskRepository()
taskRepo.deleteTask(someTask)
.sink {
print("Task deleted successfully! 🗑")
}
.store(in: &cancellables)
Complete Code in One Place
For convenience, here’s the full TaskRepository implementation:
import CoreData
import Combine
// MARK: CoreData Entity
@objc(Task)
public class Task: NSManagedObject {
}
extension Task {
@nonobjc public class func fetchRequest() -> NSFetchRequest<Task> {
return NSFetchRequest<Task>(entityName: "Task")
}
@NSManaged public var title: String?
@NSManaged public var isCompleted: Bool
}
extension Task : Identifiable {
}
// MARK: - CoreData Manager
class CoreDataManager {
static let shared = CoreDataManager()
let persistentContainer: NSPersistentContainer
private init() {
persistentContainer = NSPersistentContainer(name: "TaskModel")
persistentContainer.loadPersistentStores { (_, error) in
if let error = error {
fatalError("Failed to load Core Data stack: \(error)")
}
}
}
var context: NSManagedObjectContext {
return persistentContainer.viewContext
}
}
// MARK: - Task Repository
class TaskRepository {
private var cancellables = Set<AnyCancellable>()
private let context = CoreDataManager.shared.context
func fetchTasks() -> AnyPublisher<[Task], Never> {
let fetchRequest: NSFetchRequest<Task> = Task.fetchRequest()
return Future { promise in
do {
let tasks = try self.context.fetch(fetchRequest)
promise(.success(tasks))
} catch {
print("Failed to fetch tasks: \(error)")
promise(.success([]))
}
}
.eraseToAnyPublisher()
}
}
extension TaskRepository {
func saveTask(title: String) -> AnyPublisher<Void, Never> {
Future { promise in
let task = Task(context: self.context)
task.title = title
task.isCompleted = false
do {
try self.context.save()
promise(.success(()))
} catch {
print("Failed to save task: \(error)")
promise(.success(()))
}
}
.eraseToAnyPublisher()
}
}
extension TaskRepository {
func updateTask(_ task: Task, isCompleted: Bool) -> AnyPublisher<Void, Never> {
Future { promise in
task.isCompleted = isCompleted
do {
try self.context.save()
promise(.success(()))
} catch {
print("Failed to update task: \(error)")
promise(.success(()))
}
}
.eraseToAnyPublisher()
}
}
extension TaskRepository {
func deleteTask(_ task: Task) -> AnyPublisher<Void, Never> {
Future { promise in
self.context.delete(task)
do {
try self.context.save()
promise(.success(()))
} catch {
print("Failed to delete task: \(error)")
promise(.success(()))
}
}
.eraseToAnyPublisher()
}
}
Final Thoughts
With Combine and Core Data, we’ve turned our database operations into elegant, reactive workflows. Fetching, saving, updating, and deleting data is now effortless!
“Reactive programming isn’t just a trend — it’s the future!”