Categories: iOSSwiftTutorials

Understanding Relationships in Core Data

Greetings!

It’s been a while I haven’t written anything, I was out of topics to write on.
Lately I have been working on app at my work place which strongly depends on Core Data. Which gave me an idea to write on Relationships in Core Data.
Without any further delay, Let’s get started.

You can clone the Starter Project app called BookBase, so it may help you to follow along. After cloning the app, let’s open the BookBase.xcdatamodeld.

We are going to create two Entities here, Books and an entity named Authors, then first we will understand how One-Many Relationship can be established between the two entities, then we will look into Many-Many Relationship. Cool thing about Xcode is that now it generates classes for Entities on its own.

One-Many Relationship

Click on Add Entity button and create two entities one Books and the other one named Authors. Now select the entity Books and add an attribute called bookTitle of  type String and set the destination to Authors. After click on + on the Relationships section and create a relationship with named hasAuthors, uncheck the optional check because a Book will always has Author, then select the relationship and from the File inspector select the last icon and set the Type of relationship from To One to To Many.  As shown in the picture below,


You have just created your first One-Many relation. We’ll go into implementations later, let’s move on to making it a Many-Many relationship.

Many-Many Relationship

A Book can have multiple Authors and an Author can have multiple Books. Inside your Authors’ entity create an attribute authorName. In your Relationship section create a new relation with name hasBooks and set the destination to Books. Change the Inverse Relationship to hasAuthors from No Inverse, and at last make the type of the relationship To Many from the File Inspector. Inverse relation ensures that the entity from which a to Many relation has been made towards the other entity also acknowledges that it has a relation to that particular entity to maintain data integrity.

You have successfully created a Many-Many Relationship between the two entities. Let’s dive in to some code now.

We’ll create a class that will manage the adding and searching the Books from Core Data, let’s name it CoreDataManager. We’ll also create properties that will hold the fetched Books and Authors, a property that manage the context and a function that saves the context as displayed in the class below.

class CoreDataManager: NSObject {
    
    //Container Context
    fileprivate lazy var context: NSManagedObjectContext = {
        let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
        return context
    }()
    
    //Contains Fetched Tasks
    var books: [Books] = []
    var authors: [Authors] = []
    
    
    //MARK: init
    override init() {
        super.init()
          
    }
    
    //CoreData Save Context
    private func saveContext(){
     (UIApplication.shared.delegate as! AppDelegate).saveContext()   
    }
    
}

First we’ll create a function which will fetch all the records from CoreData and call it on the time when object of CoreDataManager is being created.

class CoreDataManager: NSObject {
    
    //Container Context
    fileprivate lazy var context: NSManagedObjectContext = {
        let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
        return context
    }()
    
    //Contains Fetched Tasks
    var books: [Books] = []
    var authors: [Authors] = []
    
    
    //MARK: init
    override init() {
        super.init()
       //Fetches all the data from the CoreData into the arrays declared above
       getAllEntities()   
    }
        //Fetch from CoreData
    private func getAllEntities(){
        do {
            books = try context.fetch(Books.fetchRequest())
            authors = try context.fetch(Authors.fetchRequest())
            
        }
        catch {
            debugPrint("Fetching Failed")
        }
    }
    //CoreData Save Context
    private func saveContext(){
     (UIApplication.shared.delegate as! AppDelegate).saveContext()   
    }
    
}

After setting these things up, we’ll create a function which will save Book title and Author name to the Core Data. But first let’s have a look at the UI.

The UI consists of two TextFields for entering Book’s title and  the second one is for the Author name, and three buttons to Add, to Search, and to Refresh.

Insertion in Core Data

So we’ve seen how the UI is, now we’ll get back to writing that function which will help us to insert data in Core Data.

  func saveBookToCoreData(bookTitle: String, authorName: String, completion:(_ isSaved: Bool) ->()){
        guard (self.books.filter{ $0.bookTitle == bookTitle}).first != nil else {
            let books = Books(context: context)
            books.bookTitle = bookTitle
            
            
            let authorNameArray = authorName.split(separator: ",")
            var authorArray: [Authors] = []
            for authorName in authorNameArray{
                
                let filteredAuthors = authors.filter{$0.authorName!.lowercased() == String(authorName).lowercased()}.first
                if filteredAuthors == nil{
                    let author = Authors(context: context)
                    author.authorName = String(authorName)
                    author.addToHasBooks(books)
                    authorArray.append(author)
                    self.authors.append(author)
                }else{
                    filteredAuthors!.addToHasBooks(books)
                    authorArray.append(filteredAuthors!)
                }
            }
            let authorSet = NSSet(array: authorArray)
            books.addToHasAuthors(authorSet)
            self.books.append(books)
            
            self.saveContext()
            completion(true)
            return
        }
        completion(false)
        
    }

So how this function works?, First it checks if the passed Book already exist or not, if Yes, it doesn’t do anything, if No, then it creates an object of type Books,  then it checks if the passed in Author name is already in the CoreData if No? then creates an Authors object, if user passes in multiple Authors separated by a “,” then that logic is also handled there, if an Author is already in the CoreData than the same object of Authors is used to create a new object of Books, a completion returns true if Book is saved successfully otherwise a false.

Fetching from Core Data

Let’s move on to writing functions for searching Books by either Author name or Book title.

    private func searchBookByBookTitle(bookTitle: String) -> (books: [Books]?, authorsName: [String]?){
        
        let filteredBooks = books.filter { (book: Books) -> Bool in
            return book.bookTitle!.lowercased().contains(bookTitle.lowercased())
        }
        let authorsName = filteredBooks.flatMap{($0.hasAuthors?.allObjects as? [Authors]).flatMap{$0.flatMap{$0.authorName}}}.map{$0.joined(separator: ",")}
        
        return (filteredBooks, authorsName)
    }
    
    private func searchBookByAuthorName(authorName: String) -> (books: [Books]?, authorsName: [String]?){
        
        let books = authors.filter({ (author: Authors) -> Bool in
            return author.authorName!.lowercased().contains(authorName.lowercased())
        }).flatMap{$0.hasBooks?.allObjects as? [Books]}.flatMap{$0}
        let authorsName = books.flatMap{($0.hasAuthors?.allObjects as? [Authors]).flatMap{$0.flatMap{$0.authorName}}}.map{$0.joined(separator: ",")}
        return (books, authorsName)
    }

What these function does is filter the ManagedObject ([Books] or [Authors]) w.r.t to arguments provided i.e. (Book title or Author name), after getting the desired Books or Author, it fetches the respective relation, and takes out the required information from it, which would be a Books and Authors. {Please don’t mind the excessive use of closure functions in the search, but I feel more comfortable using them. :)}

Just to make things look tidy.

  func searchBook(bookTitle: String?, authorName: String?) -> (books: [Books]?, authorsName: [String]?){
        
        if let bookName = bookTitle, bookName != ""{
            
            return searchBookByBookTitle(bookTitle: bookName)
            
        }else if let author = authorName, author != ""{
            
            return searchBookByAuthorName(authorName: author)
        }
        return (nil, nil)
    }

Setting up the Controller

Our work in this class is done here. It’s time to move on to your ViewController. In your controller create two properties as:

var coreDataManager = CoreDataManager()
lazy var searchedBooks: (books: [Books], authorsName: [String]) = ([], [])

CoreDataManager object is for manipulating the data and other one is for displaying the searched results inside the tableview.

In your Add button’s @IBAction i.e addToCoreData write the following code:

 guard let bookTitle = bookNameTextField.text, let authorName = authorNameTextField.text, bookTitle != "", authorName != "" else{
           
           showAlert(title: "Cannot Proceed", message: "Both fields are required.")
            return
        }
        coreDataManager.saveBookToCoreData(bookTitle: bookTitle, authorName: authorName) { (isSaved) in
            if !isSaved{
            self.showAlert(title: "Cannot Proceed", message: "This Book is already in the library.")
            }
        }
        clearTextFields()

Inside the @IBAction of Search, write this:

 guard let (searchBooks, authors) = coreDataManager.searchBook(bookTitle: bookNameTextField.text, authorName: authorNameTextField.text)  as? ([Books], [String]) else{return}
 searchedBooks = (searchBooks, authors)
 tableView.reloadData()

Change your TableView’s DataSource to look like this:

extension ViewController: UITableViewDataSource{
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "booksList", for: indexPath)
        cell.textLabel?.text = searchedBooks.books[indexPath.row].bookTitle
        cell.detailTextLabel?.text = searchedBooks.authorsName[indexPath.row]
        return cell
    }
    
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return searchedBooks.books.count
    }
}

In the end, in your Refresh button’s action:

 searchedBooks = ([],[])
 tableView.reloadData()
 clearTextFields()

Build and Run

Now, Its time to build and run the app.

 

And add some books in it. After that try and search. Let’s search by passing in the name of the Author in the textfield.

 

You can try and search with a Book name as well.

Questions?

So this was all there is to it for now, and source code is available here.
If you have any questions please leave a comment below.

Sayonara!

Aaqib Hussain

Aaqib is an enthusiastic programmer with the love of Swift and anything that looks like Swift i.e Kotlin. He loves writing code in Swift, and exploring new technology and platforms. He likes to listen to old music. When he is not writing code, he's probably spend his time watching movies, tv-shows or anime, or either doing some research for writing the next article. He started Kode Snippets in 2015.

View Comments

  • Hi Aqib,
    Fetching all the entities and storing them in local var this way, does it cause to realize all the objects? or will those be faults until an item is fetched from the array.
    And wouldn't it be better to user FetchRequest for searching the books or author? Even if they are faults , don't you think that would be better architecture? as this is a database app.

    • Hi Sajid!
      I think it's based on what your needs are. Let's say if this app showed the list of all the books on start up I would eventually be fetching all the records. So basically its just your practice. I think they will faults until an item is accessed from the array.
      It really depends on how one feels comfortable using something, I feel filters more easy to use and flexible. Since we already have all the fetched records in an array, why create a fetch request and go through all that again? I don't see any point of that in this scenario. :)

Recent Posts

Things to know when moving to Germany

This article covers some important things you must know when you are considering a move…

3 years ago

Unit Testing in Android for Dummies

What is Unit Testing? In its simplest term, unit testing is testing a small piece…

4 years ago

Factory Design Pattern

In this article, you will learn about a type of Creational Design Pattern which is…

5 years ago

Creating Target specific Theme in iOS

In this tutorial, you will go through the use of targets to achieve two separate…

5 years ago

Facade Design Pattern

In this article, you will learn about a type of Structural Design Pattern which is…

5 years ago

Singleton Design Pattern

In this article you will learn about a type of Creational Design Pattern which is…

5 years ago