Swift Combine Series: Part 5 — Combine with Core Data (Persistence Made Easy!)

Vikram Kumar
5 min readFeb 10, 2025

--

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?

Photo by Nikita Kachanovsky on Unsplash

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!”

--

--

Vikram Kumar
Vikram Kumar

Written by Vikram Kumar

I am Vikram, a Senior iOS Developer at Matellio Inc. focused on writing clean and efficient code. Complex problem-solver with an analytical and driven mindset.

No responses yet