Implementing Create, Read, Update, and Delete (CRUD) Operations in iOS
Course: iOS App Development
Topic: Implementing Create, Read, Update, and Delete (CRUD) Operations
Course Link: iOS App Development Training
Demo Link: Register for a Free Course Demo
JustAcademy Homepage: Visit JustAcademy
1. Introduction to CRUD Operations
CRUD stands for Create, Read, Update, and Delete. These four operations form the foundation of data management in most iOS applications. Whenever an application allows users to add a task, view saved information, edit a profile, or remove an item, it is performing one or more CRUD operations.
In iOS development, CRUD functionality can be implemented using several storage approaches, including Core Data, SwiftData, SQLite, files, UserDefaults for simple preferences, or a remote API. The correct choice depends on the type of information, its complexity, and whether the data must be shared with a server.
Learning Objectives
- Understand the purpose of Create, Read, Update, and Delete operations.
- Choose an appropriate data-storage method for an iOS application.
- Implement CRUD functionality with Swift and Core Data.
- Connect CRUD operations to SwiftUI interfaces.
- Handle validation, errors, persistence, and UI updates.
- Apply best practices for testing and maintaining data.
2. Understanding the Four CRUD Operations
| Operation |
Purpose |
Example in an iOS App |
| Create |
Add a new record. |
Save a new task or contact. |
| Read |
Retrieve and display saved records. |
Show a list of saved tasks. |
| Update |
Modify an existing record. |
Change a task title or mark it complete. |
| Delete |
Remove a record. |
Delete a completed task. |
CRUD operations are often connected. For example, a task-management application creates a task, reads it into a list, updates its completion status, and deletes it when it is no longer needed.
3. Selecting a Data Storage Approach
Before implementing CRUD, decide where the application's data should live. The storage approach affects performance, complexity, offline behavior, and data persistence.
| Technology |
Best Use |
Important Consideration |
| UserDefaults |
Small preferences such as theme selection. |
Not intended for complex collections or large datasets. |
| FileManager |
Files, documents, images, and exported data. |
Requires a suitable file format and file-management strategy. |
| Core Data |
Structured local data, relationships, and larger object graphs. |
Requires a managed object model and context management. |
| SwiftData |
Model-based persistence in supported modern Apple platforms. |
Availability depends on the deployment target and SDK. |
| SQLite |
Relational data and direct SQL-based storage. |
Requires schema and query management, or an appropriate library. |
| Remote API |
Data shared with a backend or across devices and users. |
Requires network handling, authentication, and synchronization logic. |
In this tutorial, Core Data is used for the main CRUD example because it is commonly used to manage structured local data in iOS applications. The same CRUD concepts apply to other storage technologies.
4. Preparing the iOS Project
To implement CRUD with Core Data, create an iOS project in Xcode and enable Core Data when creating the project if that option is available in your Xcode version. You can also add a Core Data model to an existing project and configure a persistent container yourself.
Step 1: Create a Data Model
- Open the project's
.xcdatamodeld file.
- Create an entity named
TaskItem.
- Add an attribute named
id with type UUID.
- Add an attribute named
title with type String.
- Add an attribute named
isCompleted with type Boolean.
- Add an attribute named
createdAt with type Date.
- Configure the entity's code-generation settings to match how the project will define or generate its managed object class.
The model represents the structure of each saved task. Keep attribute names and types consistent with the code that accesses the entity.
Example Data Structure
| Attribute |
Type |
Purpose |
| id |
UUID |
Uniquely identifies a task. |
| title |
String |
Stores the task description. |
| isCompleted |
Boolean |
Indicates whether the task is complete. |
| createdAt |
Date |
Records when the task was created. |
5. Understanding the Core Data Components
- NSPersistentContainer: Sets up the Core Data stack and coordinates the managed object model, persistent store, and contexts.
- NSManagedObjectContext: Tracks inserted, changed, and deleted objects before changes are saved.
- NSManagedObject: Represents an entity instance managed by Core Data.
- NSFetchRequest: Describes which objects should be retrieved.
- Persistent Store: Holds saved data, often in a SQLite-backed store.
A typical data flow is: the user interacts with the interface, application code changes objects through a context, the context saves changes to persistent storage, and the interface displays the current data.
6. Setting Up a Persistence Controller
For an existing project, a small persistence controller can provide a shared persistent container. If Xcode has already generated a Core Data stack, use that implementation instead of creating a duplicate container.
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "TaskModel")
if inMemory {
container.persistentStoreDescriptions.first?
.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores { _, error in
if let error = error {
fatalError(
"Failed to load Core Data store: \(error)"
)
}
}
container.viewContext
.automaticallyMergesChangesFromParent = true
}
}
Important: Replace TaskModel with the actual name of your Core Data model. The in-memory configuration is useful for tests because its data does not persist between app launches.
7. Create Operation: Adding New Records
The Create operation inserts a new object into the managed object context and saves the context so the record persists beyond the current editing session.
Steps to Create a Record
- Receive input from the user.
- Validate the input, such as checking that a task title is not empty.
- Create an instance of the managed object.
- Assign values to its attributes.
- Save the context.
- Update the interface or show a confirmation.
import CoreData
func createTask(
title: String,
context: NSManagedObjectContext
) throws {
let cleanedTitle = title.trimmingCharacters(
in: .whitespacesAndNewlines
)
guard !cleanedTitle.isEmpty else {
throw NSError(
domain: "TaskValidation",
code: 1,
userInfo: [
NSLocalizedDescriptionKey:
"Task title cannot be empty."
]
)
}
let task = TaskItem(context: context)
task.id = UUID()
task.title = cleanedTitle
task.isCompleted = false
task.createdAt = Date()
if context.hasChanges {
try context.save()
}
}
This example assumes Xcode generates a TaskItem subclass for the Core Data entity. If your project uses a different generated class or manual subclass, adapt the code accordingly.
Handling Create Errors
do {
try createTask(
title: "Learn Core Data",
context: context
)
print("Task saved successfully.")
} catch {
print("Unable to save task: \(error.localizedDescription)")
}
In a production application, show a user-friendly message rather than relying only on console output. Avoid displaying sensitive implementation details from internal errors.
8. Read Operation: Fetching Saved Records
The Read operation retrieves saved records from Core Data. A fetch request can filter, sort, and limit the returned objects.
func fetchTasks(
context: NSManagedObjectContext
) throws -> [TaskItem] {
let request: NSFetchRequest<TaskItem> =
TaskItem.fetchRequest()
request.sortDescriptors = [
NSSortDescriptor(
key: "createdAt",
ascending: false
)
]
return try context.fetch(request)
}
This request sorts tasks by creation date, placing newer tasks first. The generated fetch request must return the correct managed object type for your project.
Filtering Records with a Predicate
func fetchIncompleteTasks(
context: NSManagedObjectContext
) throws -> [TaskItem] {
let request: NSFetchRequest<TaskItem> =
TaskItem.fetchRequest()
request.predicate = NSPredicate(
format: "isCompleted == NO"
)
request.sortDescriptors = [
NSSortDescriptor(
key: "createdAt",
ascending: false
)
]
return try context.fetch(request)
}
Predicates allow the application to retrieve only matching records. Common filters include completion status, dates, categories, or a search term. For user-entered search strings, use predicate substitution rather than concatenating raw input into a predicate format.
Sorting and Limiting Results
let request: NSFetchRequest<TaskItem> =
TaskItem.fetchRequest()
request.sortDescriptors = [
NSSortDescriptor(key: "title", ascending: true)
]
request.fetchLimit = 50
let tasks = try context.fetch(request)
Sorting improves readability, while fetch limits can reduce unnecessary work when the interface needs only a portion of a large dataset.
9. Update Operation: Modifying Existing Records
The Update operation modifies an existing record without creating a duplicate. In Core Data, fetch the required object, update its attributes, and save the context.
Example: Update a Task Title
func updateTaskTitle(
task: TaskItem,
newTitle: String,
context: NSManagedObjectContext
) throws {
let cleanedTitle = newTitle.trimmingCharacters(
in: .whitespacesAndNewlines
)
guard !cleanedTitle.isEmpty else {
throw NSError(
domain: "TaskValidation",
code: 2,
userInfo: [
NSLocalizedDescriptionKey:
"Updated title cannot be empty."
]
)
}
task.title = cleanedTitle
if context.hasChanges {
try context.save()
}
}
The function accepts an existing TaskItem, validates the new title, changes the object's value, and saves the modification. It does not create another record.
Example: Update Completion Status
func toggleTaskCompletion(
task: TaskItem,
context: NSManagedObjectContext
) throws {
task.isCompleted.toggle()
if context.hasChanges {
try context.save()
}
}
This function changes a task from incomplete to complete or from complete to incomplete. The context tracks the changed property until it is saved.
Update Workflow
- Identify the record the user wants to edit.
- Load the record from the correct context.
- Validate the new values.
- Update only the intended attributes.
- Save the context and handle possible errors.
- Refresh the UI or rely on its data-binding mechanism to reflect the change.
10. Delete Operation: Removing Existing Records
The Delete operation removes an existing object from the managed object context. The deletion becomes persistent when the relevant context is saved successfully.
func deleteTask(
task: TaskItem,
context: NSManagedObjectContext
) throws {
context.delete(task)
if context.hasChanges {
try context.save()
}
}
Deleting an object from the context and saving the context are separate steps. If saving fails, handle the error appropriately instead of assuming the deletion was completed successfully.
Deleting a Record Safely
- Confirm that the user selected the correct record.
- Use a confirmation dialog when deletion could cause meaningful data loss.
- Check whether related entities depend on the record.
- Configure relationship deletion rules carefully in the Core Data model.
- Handle save failures and keep the interface consistent with the actual data state.
Example: Delete by Unique Identifier
func deleteTask(
id: UUID,
context: NSManagedObjectContext
) throws {
let request: NSFetchRequest<TaskItem> =
TaskItem.fetchRequest()
request.predicate = NSPredicate(
format: "id == %@",
id as CVarArg
)
request.fetchLimit = 1
guard let task = try context.fetch(request).first else {
return
}
context.delete(task)
if context.hasChanges {
try context.save()
}
}
Using a unique identifier can be useful when a view or service stores an ID rather than passing a managed object directly. Ensure the identifier is unique and that the fetched object belongs to the context performing the deletion.
11. Connecting CRUD Operations to SwiftUI
SwiftUI can work with Core Data through an environment-managed context and property wrappers such as @FetchRequest. These features help keep the interface synchronized with persisted records.
Providing the Managed Object Context
In the application's entry point, provide the persistent container's view context to the SwiftUI environment.
import SwiftUI
import CoreData
@main
struct TaskApp: App {
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
TaskListView()
.environment(
\.managedObjectContext,
persistenceController.container.viewContext
)
}
}
}
If your project already provides the context through a generated app entry point, use that existing setup and avoid creating a second persistent container.
Displaying Records with a Fetch Request
import SwiftUI
import CoreData
struct TaskListView: View {
@Environment(\.managedObjectContext)
private var context
@FetchRequest(
sortDescriptors: [
NSSortDescriptor(
keyPath: \TaskItem.createdAt,
ascending: false
)
],
animation: .default
)
private var tasks: FetchedResults<TaskItem>
@State private var newTitle = ""
var body: some View {
NavigationStack {
VStack {
HStack {
TextField(
"Enter a task",
text: $newTitle
)
.textFieldStyle(.roundedBorder)
Button("Add") {
addTask()
}
.disabled(
newTitle.trimmingCharacters(
in: .whitespacesAndNewlines
).isEmpty
)
}
.padding()
List {
ForEach(tasks, id: \.objectID) { task in
HStack {
Button {
toggleCompletion(task)
} label: {
Image(
systemName: task.isCompleted
? "checkmark.circle.fill"
: "circle"
)
}
.buttonStyle(.plain)
Text(task.title ?? "Untitled Task")
.strikethrough(task.isCompleted)
Spacer()
Button(role: .destructive) {
delete(task)
} label: {
Image(systemName: "trash")
}
}
}
}
}
.navigationTitle("My Tasks")
}
}
private func addTask() {
let title = newTitle.trimmingCharacters(
in: .whitespacesAndNewlines
)
guard !title.isEmpty else { return }
let task = TaskItem(context: context)
task.id = UUID()
task.title = title
task.isCompleted = false
task.createdAt = Date()
saveContext()
newTitle = ""
}
private func toggleCompletion(_ task: TaskItem) {
task.isCompleted.toggle()
saveContext()
}
private func delete(_ task: TaskItem) {
context.delete(task)
saveContext()
}
private func saveContext() {
guard context.hasChanges else { return }
do {
try context.save()
} catch {
context.rollback()
print("Unable to save changes: \(error)")
}
}
}
Model assumptions: This example expects a generated TaskItem managed object with the attributes id, title, isCompleted, and createdAt. If your model defines title as non-optional, use task.title directly instead of providing a nil fallback.
This view demonstrates Create through the Add button, Read through the fetched list, Update through the completion button, and Delete through the trash button. For a larger application, move persistence logic into a dedicated service or view model and present save failures to the user rather than only printing them.
12. Implementing an Edit Screen
Many applications use a separate editing screen instead of changing data directly in a list. The edit screen loads the current values, lets the user modify them, and saves the updated values.
struct EditTaskView: View {
@Environment(\.managedObjectContext)
private var context
@Environment(\.dismiss)
private var dismiss
@ObservedObject var task: TaskItem
@State private var title = ""
var body: some View {
Form {
TextField("Task title", text: $title)
Toggle(
"Completed",
isOn: $task.isCompleted
)
Button("Save Changes") {
saveChanges()
}
}
.navigationTitle("Edit Task")
.onAppear {
title = task.title ?? ""
}
}
private func saveChanges() {
let cleanedTitle = title.trimmingCharacters(
in: .whitespacesAndNewlines
)
guard !cleanedTitle.isEmpty else { return }
task.title = cleanedTitle
do {
if context.hasChanges {
try context.save()
}
dismiss()
} catch {
context.rollback()
print("Update failed: \(error)")
}
}
}
For a production edit form, use a separate local state variable for each editable field, including the completion status. This avoids modifying the managed object before the user presses Save. If the user cancels, discard the local edits without saving them.
13. Validation and Data Integrity
Validation prevents invalid or incomplete records from entering persistent storage. Validation can be implemented in the interface, a service layer, and the Core Data model.
- Required values: Reject empty titles and missing required fields.
- Length limits: Prevent values from exceeding the application's expected limits.
- Uniqueness: Use unique identifiers and, where appropriate, model-level uniqueness constraints.
- Type validation: Ensure dates, numbers, Boolean values, and text use the expected types.
- Relationship validation: Confirm that related records exist when required.
- Business rules: Prevent invalid state changes, such as completing an order that has not been approved.
Interface validation improves the user experience, but it should not be the only protection for important data. Centralize business rules so they are consistently applied by every entry point.
14. Error Handling and Unsaved Changes
Core Data operations can fail because of validation errors, store problems, migration issues, or other persistence failures. Use Swift's error-handling mechanisms to manage these situations.
do {
if context.hasChanges {
try context.save()
}
} catch {
print("Save failed: \(error.localizedDescription)")
}
The save() method can throw an error. Checking hasChanges avoids unnecessary save calls when the context has no pending modifications. Apple's Core Data save documentation explains the behavior of saving a managed object context.
Choose an error strategy that fits the operation:
- For an edit form, preserve the user's input and show an error if saving fails.
- For a background import, record failures and decide whether to retry or discard the failed batch.
- For a destructive operation, do not report success until the save has completed.
- Use
rollback() carefully because it discards pending context changes, which may include unrelated edits.
15. Using a Service Layer for CRUD
As an application grows, it is helpful to move database operations out of SwiftUI views. A service layer can centralize validation, fetching, insertion, updating, deletion, and error handling.
import CoreData
struct TaskRepository {
let context: NSManagedObjectContext
func allTasks() throws -> [TaskItem] {
let request: NSFetchRequest<TaskItem> =
TaskItem.fetchRequest()
request.sortDescriptors = [
NSSortDescriptor(
key: "createdAt",
ascending: false
)
]
return try context.fetch(request)
}
func addTask(title: String) throws {
let cleaned = title.trimmingCharacters(
in: .whitespacesAndNewlines
)
guard !cleaned.isEmpty else { return }
let task = TaskItem(context: context)
task.id = UUID()
task.title = cleaned
task.isCompleted = false
task.createdAt = Date()
try saveIfNeeded()
}
func renameTask(
_ task: TaskItem,
to newTitle: String
) throws {
let cleaned = newTitle.trimmingCharacters(
in: .whitespacesAndNewlines
)
guard !cleaned.isEmpty else { return }
task.title = cleaned
try saveIfNeeded()
}
func removeTask(_ task: TaskItem) throws {
context.delete(task)
try saveIfNeeded()
}
private func saveIfNeeded() throws {
if context.hasChanges {
try context.save()
}
}
}
This repository is a starting point. In a larger project, consider returning explicit validation errors instead of silently ignoring invalid input, and define whether the repository owns its context or receives one from another component.
16. CRUD with Remote APIs
Some applications store records on a server rather than exclusively on the device. In that situation, CRUD operations typically map to HTTP methods, although the actual API contract determines the precise behavior.
| CRUD Action |
Common HTTP Method |
Example Endpoint |
| Create |
POST |
POST /tasks |
| Read |
GET |
GET /tasks |
| Update |
PUT or PATCH |
PATCH /tasks/123 |
| Delete |
DELETE |
DELETE /tasks/123 |
When using a remote API, handle network errors, HTTP status codes, authentication, response decoding, and request timeouts. A successful local change does not automatically mean that the server has accepted the change.
For offline-capable applications, Core Data or SwiftData can store a local copy while a synchronization layer reconciles changes with the backend. Such a design must account for duplicate requests, conflicting updates, deleted records, and retry behavior.
17. Performance Optimization
- Use fetch predicates: Retrieve only records required by the current screen.
- Limit fetches: Use fetch limits or pagination patterns for large datasets.
- Batch carefully: For large imports or deletions, investigate Core Data batch operations and understand their effect on registered objects.
- Use background contexts: Perform expensive imports and processing away from the main UI context.
- Respect context queues: Access managed objects on the queue associated with their context. Do not pass managed object instances directly between unrelated contexts; pass object IDs or transfer plain values.
- Avoid repeated saves: Group related changes into sensible save operations.
- Monitor memory: Avoid retaining large numbers of managed objects unnecessarily.
Apple's Core Data documentation covers fetching, background tasks, model migration, and other techniques for managing persisted data efficiently.
18. Common CRUD Problems and Solutions
| Problem |
Possible Cause |
Solution |
| New record disappears after relaunch. |
The context was not saved, or the persistent store was not configured correctly. |
Verify the save operation and persistent container setup. |
| List does not show new data. |
The view is not observing the correct context or fetch results. |
Check environment injection and fetch configuration. |
| Update creates duplicate records. |
The code inserts a new object instead of editing the existing one. |
Fetch or retain the correct record and update its attributes. |
| Deleted record returns after relaunch. |
The deletion was not saved or another context restored the record. |
Inspect context saves, synchronization, and merge behavior. |
| Fetch returns no results. |
Incorrect entity name, predicate, identifier, or store setup. |
Check the model, query conditions, and stored data. |
| Save fails. |
Validation, model, or persistent-store error. |
Inspect the error, validate values, and verify the model configuration. |
| UI becomes unresponsive. |
Heavy processing or large fetches on the main context. |
Optimize queries and move suitable work to a background context. |
| Context concurrency errors occur. |
A managed object is accessed on the wrong queue. |
Use the context's supported queue APIs and pass object IDs across contexts. |
19. Testing CRUD Operations
Testing ensures each operation works independently and that the complete workflow remains reliable.
Create Tests
- Verify that a valid title creates a record.
- Verify that whitespace-only input is rejected.
- Confirm that required attributes receive expected values.
Read Tests
- Verify that saved records can be fetched.
- Confirm that predicates return the expected subset.
- Check sort order and behavior when the store is empty.
Update Tests
- Verify that the correct record changes.
- Ensure updating does not create a duplicate.
- Confirm that invalid replacement values are rejected.
Delete Tests
- Verify that a deleted record no longer appears in subsequent fetches.
- Confirm that deletion persists after saving and reopening the store.
- Test cancellation and failure handling for destructive actions.
Use an in-memory persistent store for isolated Core Data tests where appropriate. This helps tests avoid changing a developer's normal local database.
20. Practical Project: Task Management Application
Build a small task manager that uses all four CRUD operations.
Functional Requirements
- Allow the user to enter and create a task.
- Display saved tasks in a sorted list.
- Allow users to rename a task.
- Allow users to mark tasks as complete or incomplete.
- Allow users to delete a task.
- Preserve data between application launches.
- Display a helpful empty state when no tasks exist.
- Show clear error messages if saving fails.
Suggested Development Steps
- Create the Xcode project and configure Core Data.
- Define the
TaskItem entity and its attributes.
- Set up the persistent container and managed object context.
- Implement create and read functions.
- Build a SwiftUI list with an input field.
- Add editing and completion controls.
- Implement deletion with an appropriate confirmation step.
- Test validation, persistence, and error scenarios.
- Improve the layout and accessibility labels.
21. Best Practices for CRUD in iOS
- Choose a storage technology that matches the data and platform requirements.
- Keep persistence logic separate from view layout when the application becomes complex.
- Validate user input before attempting to save.
- Use stable identifiers to identify records reliably.
- Save context changes deliberately and handle errors.
- Keep Core Data managed objects within their context's concurrency rules.
- Use predicates, sorting, and fetch limits for appropriate read operations.
- Consider confirmation and undo options for destructive operations.
- Test empty states, invalid inputs, persistence, and failure paths.
- For server-backed data, handle authentication, connectivity, and synchronization explicitly.
22. Practice Exercises
- Create a contact entity with name, email, and phone number.
- Implement a form to add contacts and display them in a list.
- Add a search field to filter contacts by name.
- Create an edit screen to update contact details.
- Add a delete confirmation dialog before removing a contact.
- Prevent duplicate email addresses where the application requires unique emails.
- Add a filter to display only completed tasks.
- Write tests that verify create, read, update, and delete behavior.
23. Interview Questions and Answers
Q1. What does CRUD stand for?
CRUD stands for Create, Read, Update, and Delete. These operations represent the basic lifecycle of stored records.
Q2. What is the difference between Create and Update?
Create inserts a new record, while Update modifies an existing record. Updating the wrong way can accidentally produce duplicate records.
Q3. What is an NSManagedObjectContext?
It is an object-management environment that tracks inserted, updated, and deleted managed objects and coordinates fetching and saving changes.
Q4. Why must a Core Data context be saved?
Changes made in a context are tracked as pending changes. Saving commits those changes to the context's parent store; the root context must ultimately save for changes to reach the persistent store.
Q5. How does an NSFetchRequest work?
It describes which objects should be retrieved and can include a predicate, sort descriptors, and a fetch limit.
Q6. How can you filter Core Data records?
Set an NSPredicate on the fetch request to specify conditions that returned objects must satisfy.
Q7. What happens when a managed object is deleted?
The context marks the object for deletion. Saving the context commits that deletion to the persistent store.
Q8. What is the purpose of error handling in CRUD?
Error handling lets the application respond safely to validation failures, storage issues, and other operation failures instead of assuming that every operation succeeds.
Q9. How can CRUD data be synchronized with a server?
A synchronization layer can exchange changes through a remote API, track local and remote versions, and resolve conflicts. Offline synchronization requires additional design beyond basic local CRUD.
Q10. How do you improve CRUD performance?
Use suitable predicates, fetch limits, background contexts for expensive work, efficient save patterns, and careful management of large result sets.
24. Quick Revision Checklist
- Create: validate input, insert a new record, and save.
- Read: fetch records using appropriate predicates and sorting.
- Update: identify the correct existing object, change its values, and save.
- Delete: remove the intended object, save, and handle errors.
- Core Data: understand entities, managed objects, contexts, fetch requests, and persistent stores.
- SwiftUI: provide the managed object context and observe fetched results.
- Reliability: validate data, respect context concurrency, and test failure scenarios.
25. Conclusion
Create, Read, Update, and Delete operations are essential to building interactive, data-driven iOS applications. With Swift and Core Data, developers can insert new records, fetch and display stored data, modify existing objects, and remove unwanted records. A well-designed CRUD implementation combines appropriate data modeling, validation, context management, error handling, testing, and a responsive interface.
To explore iOS App Development training, visit JustAcademy's iOS Training Course, register for a course demo, or browse the JustAcademy homepage.