Swift
UITableView
Core Data
Asynchronous Programming
iOS Development

Refreshing UITableView Asynchronously after Core Data Loaded Swift

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

If a UITableView depends on Core Data content, the UI should stay on the main thread while the fetch work happens in a background context. The correct pattern is to fetch in the background, pass safe identifiers back to the main context, update the data source, and then reload the table on the main thread. The part that usually goes wrong is crossing Core Data thread boundaries with live managed objects.

Why the Table View Freezes

Core Data fetches can be expensive when the store is large or when the app performs extra mapping work after the fetch. If you do all of that in the view controller on the main thread, scrolling, taps, and layout updates stall until the work finishes.

That is why the fetch should move to a background context while the UI stays responsive.

Use a Background Context Correctly

The NSPersistentContainer API gives you a safe background context through performBackgroundTask.

swift
1import CoreData
2import UIKit
3
4final class PeopleViewController: UITableViewController {
5    var persistentContainer: NSPersistentContainer!
6    private var people: [Person] = []
7
8    func loadPeople() {
9        persistentContainer.performBackgroundTask { backgroundContext in
10            let request: NSFetchRequest<Person> = Person.fetchRequest()
11            request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
12
13            do {
14                let results = try backgroundContext.fetch(request)
15                let objectIDs = results.map(\.objectID)
16
17                DispatchQueue.main.async {
18                    let viewContext = self.persistentContainer.viewContext
19                    self.people = objectIDs.compactMap {
20                        try? viewContext.existingObject(with: $0) as? Person
21                    }
22                    self.tableView.reloadData()
23                }
24            } catch {
25                DispatchQueue.main.async {
26                    print("Fetch failed: \(error)")
27                }
28            }
29        }
30    }
31}

The important detail is returning objectID values, not NSManagedObject instances created in the background context.

Do Not Pass Managed Objects Across Threads

A managed object belongs to the context that created or fetched it. Passing that object directly into the main thread is a Core Data concurrency bug.

The safe transfer choices are usually:

  • pass NSManagedObjectID values
  • refetch on the destination context
  • convert the data into plain value types before crossing threads

If the table view only needs display data, a plain Swift struct can be even simpler than moving object IDs around.

Always Reload on the Main Thread

UITableView is a UIKit type, so reloadData() must be called on the main thread.

A common mistake is fetching in the background and then calling reloadData() from the same background queue. That may appear to work during testing and then crash or behave unpredictably later.

NSFetchedResultsController Is Often Better

If the table is directly backed by Core Data, NSFetchedResultsController is usually a stronger design than manually reloading arrays after each fetch.

swift
1import CoreData
2
3let request: NSFetchRequest<Person> = Person.fetchRequest()
4request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
5
6let frc = NSFetchedResultsController(
7    fetchRequest: request,
8    managedObjectContext: persistentContainer.viewContext,
9    sectionNameKeyPath: nil,
10    cacheName: nil
11)

With an NSFetchedResultsController, inserts, deletes, and updates can be reflected automatically in the table view through delegate callbacks.

When Manual Reloading Still Makes Sense

Manual reloads are fine when:

  • the fetch is occasional rather than continuous
  • the data source is a transformed view model rather than raw managed objects
  • you want one explicit reload after a background import completes

The point is not that reloadData() is wrong. The point is that it should happen after the background work completes and after the result is safely bridged into the main-context world.

Common Pitfalls

  • Fetching Core Data objects on a background context and using those same objects directly on the main thread.
  • Calling tableView.reloadData() from a background queue instead of the main thread.
  • Doing large fetches synchronously in the view controller and freezing the UI.
  • Forgetting that NSManagedObjectID is the safe cross-thread handoff mechanism.
  • Rebuilding table data manually for every change when NSFetchedResultsController would be a better fit.

Summary

  • Fetch Core Data data in a background context to keep the UI responsive.
  • Pass object IDs or plain value types back to the main thread, not live managed objects.
  • Update the table view only on the main thread.
  • Use NSFetchedResultsController when the table is tightly coupled to Core Data changes.
  • The key rule is not just asynchronous loading, but correct Core Data concurrency.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.