Core Data Relationship in Swift 5— made simple

Megi Sila
7 min readMay 3, 2022

--

Persistent storage has become an essential part of the majority of iOS apps nowadays. Core Data is a persistence and/or in-memory cache framework that consists of a very powerful set of other tools that can be used within the app. While researching for my personal ongoing project I have faced a lack of updated content on this topic so I decided to make an article myself in Swift 5.

Firstly let’s start fresh and create a new project where the Core Data module is selected.

Now we can see that there are two notable changes in this project: CoreDataRelationship.xcdatamodeld and AppDelegate.swift file with Core Data Stack code.

We must create at least two entities to add a relationship between them. In this example we are going to implement a one-to-many relationship between two entities: Singer and Song with their respective attributes as shown below.

Let’s select each entity and add a relationship between them. In the Relationships sections of the first entity Singer we are going to create a new relationship called songs by clicking the plus button and choose Song as destination. The inverse field is going to be left empty at the moment. In the attributes section of this relationship the type must be changed to To Many.

In the same way we are going to create a new relationship in the Song entity named singer with Singer as destination. Now you can see that in the inverse dropdown appears the relationship that we previously created: songs.

If we jump back to songs relationship we can see that the inverse field has been updated automatically. Aldo make sure to set the Codegen to Manually/None in each entity’s attribute section.

Let’s go and regenerate our NSManagedObject Subclasses in the Editor menu now. Click Next, choose both entities and then Create.

Build the project after creating these classes to make sure everything is okay (⌘ + B). If it does not build successfully try to quit Xcode and reopen it again.

To manage data creation, fetching and deletion we are going to create a class called DataManager by firstly creating the NSPersistentContainer and saving the context.

class DataManager {
static let shared = DataManager()
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "CoreDataRelationship")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if
let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
//Core Data Saving support
func
save () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
}

Methods responsible for Singer object and Song object creation implemented simply below. addToSongs(_value: Song) method relates the song to its Singer object as a parameter of the song method.

func singer(name: String) -> Singer {
let singer = Singer(context: persistentContainer.viewContext)
singer.name = name
return singer
}
func song(title: String, releaseDate: String, singer: Singer) -> Song {
let song = Song(context: persistentContainer.viewContext)
song.title = title
song.releaseDate = releaseDate
singer.addToSongs(song)
return song
}

Now we are going to implement methods responsible for fetching data of each type. To fetch the songs we must add a predicate to the NSFetchRequest so Core Data will ensure that only the matching objects, songs that belong to the Singer object, will get returned.

func singers() -> [Singer] {
let request: NSFetchRequest<Singer> = Singer.fetchRequest()
var fetchedSingers: [Singer] = []
do {
fetchedSingers = try persistentContainer.viewContext.fetch(request)
} catch let error {
print("Error fetching singers \(error)")
}
return fetchedSingers
}
func songs(singer: Singer) -> [Song] {
let request: NSFetchRequest<Song> = Song.fetchRequest()
request.predicate = NSPredicate(format: "singer = %@", singer)
request.sortDescriptors = [NSSortDescriptor(key: "releaseDate", ascending: false)]
var fetchedSongs: [Song] = []
do {
fetchedSongs = try persistentContainer.viewContext.fetch(request)
} catch let error {
print("Error fetching songs \(error)")
}
return fetchedSongs
}

Methods responsible for deletion of each data type. As simple as that.

func deleteSong(song: Song) {
let context = persistentContainer.viewContext
context.delete(song)
save()
}
func deleteSinger(singer: Singer) {
let context = persistentContainer.viewContext
context.delete(singer)
save()
}

The data will be displayed in two UITableViews in different UIViewControllers. After creating SingerVC that is going to display the list of singers and SongVC that is going to display the list of songs I have set up a simple user interface as below.

The logic is simple: if we select a cell of SingerVC tableView the list of songs of this singer is going to display. Now straight to implementing the logic.

Let’s declare an array of Singer type globally because we are going to use it in different classes.

var singers = [Singer]()

Let’s start with SingerVC and create a new Singer. We can create a new Singer by tapping the navigationItem.rightBarButtonItem this way:

  • Create the object using singer method of DataManager class
  • Appending the object to singers array
  • Save the context and reload tableView
let singer = DataManager.shared.singer(name: textfield.text ?? "")
singers.append(singer)
DataManager.shared.save()
self.tableView.reloadData()

Fetching singers in viewDidLoad()

override func viewDidLoad() {
super.viewDidLoad()
setupUI()
singers = DataManager.shared.singers()
}

Displaying singers in tableView. The number of rows is going to be equal to the number of singers and each row is going to display the Singer object’s name at given index in singers array.

extension SingerVC: UITableViewDelegate, UITableViewDataSource {  func numberOfSections(in tableView: UITableView) -> Int {
1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return singers.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let singer = singers[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "singerCell", for: indexPath) as UITableViewCell
cell.textLabel?.text = singer.name
cell.accessoryType = .disclosureIndicator
tableView.deselectRow(at: indexPath, animated: true)

return cell
}
}

I have implemented the editing and deletion of the Singer objects as actions in the leadingSwipeActionsConfigurationForRowAt method.

The editing is done via setValue(_ value: Any?, forKey key: String) method that sets the specified property of the managed object to the specified value by using its key.

private func editSingerAction(indexPath: IndexPath) {
let singer = singers[indexPath.row]
var nameTextField = UITextField()
let alert = UIAlertController(title: "Edit singer", message: "", preferredStyle: .alert)
let editAction = UIAlertAction(title: "Edit", style: .default) { (action) in
singer.setValue(nameTextField.text ?? "", forKey: "name")
DataManager.shared.save()
self.tableView.reloadData()
}
alert.addTextField { (alertTextField) in
alertTextField.placeholder = "Ex: Beyonce"
alertTextField.text = singer.name
nameTextField = alertTextField
}
let cancelAction = UIAlertAction(title: "Cancel", style: .destructive) { (action) in
self
.dismiss(animated: true, completion: nil)
}
alert.addAction(editAction)
alert.addAction(cancelAction)
present(alert, animated: true, completion: nil)
}

For the deletion we are going to use the deleteSinger(singer: Singer) method that we created previously in the DataManager class and then delete the row of the tableView.

private func deleteSingerAction(indexPath: IndexPath) {
let singer = singers[indexPath.row]
let areYouSureAlert = UIAlertController(title: "Are you sure you want to delete this singer?", message: "", preferredStyle: .alert)
let yesDeleteAction = UIAlertAction(title: "Yes", style: .destructive) { [self] (action) in
DataManager.shared.deleteSinger(singer: singer)
singers.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
tableView.reloadData()
}
let noDeleteAction = UIAlertAction(title: "No", style: .default) { (action) in
//do nothing
}
areYouSureAlert.addAction(noDeleteAction)
areYouSureAlert.addAction(yesDeleteAction)
self.present(areYouSureAlert, animated: true, completion: nil)
}

Initialization of SongsVC for each singer in didSelectRowAt. The index parameter when initializing SongVC is going to define the singer in the singers array these songs belong to.

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let songVC = SongsVC(index: indexPath.row)
navigationController?.pushViewController(songVC, animated: true)
}

Now let’s jump to SongVC. The implementation is almost the same except the fact that in this class we are going to define the fundamental part of the relationship. Let’s declare a Singer object and attach the element of the given index in the singers array.

var singer: Singer?
...
init(index: Int) {
self.index = index
singer = singers[index]
super.init(nibName: nil, bundle: nil)
}

Now that we know the singer these songs belong to, we can create them the same way we created Singer objects.

let song = DataManager.shared.song(title: titleTextField.text ?? "", releaseDate: releaseDateTextField.text ?? "", singer: singer!)
songs.append(song)
tableView.reloadData()
DataManager.shared.save()

We must use optional binding for the Singer object to make sure if it contains a value or not. If yes, the NSFetchRequest will be executed to fetch the songs in viewDidLoad() on SongVC.

override func viewDidLoad() {
super.viewDidLoad()
setupUI()
if let singer = singer {
songs = DataManager.shared.songs(singer: singer)
}
tableView.reloadData()
}

Songs will be displayed in tableView the same way singers are by adding the proper code to UITableViewDelegate, UITableViewDataSource extension.

Try to implement it by yourself. Do not worry if you can not because you can find it in the source code linked at the end of this article 😄. The implementation of editing and deletion is also the same so give it a try!

And that is it. I hope you find it helpful and enjoy implementing it. You can reuse it in different relationships as you please.

Click here for the source code:

--

--