Better Programming

Advice for programmers.

Follow publication

How To Avoid Creating Duplicate Entries in Core Data in a SwiftUI App

Teresa Bagalà
Better Programming
Published in
7 min readJan 31, 2023

Photo by StockSnap on Pixabay

Before starting, I would like to recommend the book, SwiftUI Essentials iOS 16 edition. I’ve drawn a lot of my knowledge about SwiftUI from this content.

This demo app was built with Xcode 14.2 and iOS 16.2. In the following article, you can find the app video.

I want to create multiple persistent ToDoLists whose elements are not duplicated. Before inserting a new item in a list, I have to be sure it is not already stored in that list. To add a new list or a new item to a list, I use the custom alert implemented in my previous article, Custom alert in SwiftUI.

Start Xcode and create the MoreLists app. After that, create the Core Data model for the demo app, MoreListsModel.xcdatamodeld. Here’s how to do that:

In MoreListsModel, I create two Entities: the first is the Lists Entity; its name attribute is of the String type and represents the generic list name. The second is the Item Entity with a name attribute of String type and an image attribute of Binary Data type. This entity represents a generic list item.

Since each ToDoList can have many items, I define a one-to-many relationship between Lists and Item by calling it toItem, and adding a delete rule Cascade:

I define a second one-to-one relationship between Item and Lists by calling it toLists, then adding the delete rule, Nullify, and inverse toItem. Here’s what that looks like:

Let’s get to the code. Under the Model group, I define a PersistenceContainer structure with the methods to initialize the NSPersistentContainer object, and save or delete a NSManagedObject object. Here’s what the code looks like:

import Foundation
import CoreData

struct PersistenceContainer {
static let shared = PersistenceContainer()
let container: NSPersistentContainer

init() {
container = NSPersistentContainer(name: "MoreListsModel")
container.viewContext.automaticallyMergesChangesFromParent = true
container.loadPersistentStores { (_ , error) in
if let error = error as NSError? {
fatalError("Container load failed: \(error)")
}
}
}
func save() throws{
let context = container.viewContext
guard context.hasChanges else { return }
do {
try context.save()
} catch {
print(error.localizedDescription)
}
}

func delete(_ object: NSManagedObject) throws{
let context = container.viewContext
context.delete(object)
try context.save()

}

}

And here’s how you can share PersistenceContainer through the app:

import SwiftUI

@main
struct MoreListsApp: App {
let persistenceContainer = PersistenceContainer.shared
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext,
persistenceContainer.container.viewContext)
}
}
}

Now, under the Views group, I create the CustomAlert.swift structure. This code allows you to display a TextField alert that does two things: insert the name of the list (or of the item) and show a warning message if you try to enter a name that already exists. Here’s how to create that:

import Foundation
import SwiftUI

struct CustomAlert: View{
@Binding var textFieldValue: String
@Binding var showSimpleAlert: Bool
@Binding var showAlertWithTextField : Bool
@State var showError: Bool = false
var title: String
var message: String
var placeholder: String
var handler : () -> Void
var body: some View {
ZStack(alignment: .top) {
Color.white

if showAlertWithTextField{
AlertWithTextField(textFieldValue: $textFieldValue, showAlertWithTextField: $showAlertWithTextField, showError: $showError, title: title, message: message, placeholder: placeholder, handler: handler)

.padding()

if showError{
ErrorView()
}
}
if showSimpleAlert{
SmpleAlert(showSimpleAlert: $showSimpleAlert, title: title, message: message)

}
}

.frame(width: 300, height: 180)
.cornerRadius(20).shadow(color: .cyan, radius: 8)
.foregroundColor(Color.cyan)

}
}

struct AlertWithTextField: View{
@Binding var textFieldValue: String
@Binding var showAlertWithTextField : Bool
@Binding var showError: Bool
var title: String
var message: String
var placeholder: String
var handler : () -> Void
var body: some View{
VStack {
VStack{
Text(title).padding(5)
TextField(placeholder, text: $textFieldValue)
.textFieldStyle(.roundedBorder)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.cyan, lineWidth: 1)
)
.onChange(of: textFieldValue) { newValue in
showError = false
}

Spacer(minLength: 25)
HStack{
CustomButton(text: "Cancel") {
showAlertWithTextField.toggle()
textFieldValue = ""
}

Spacer()
CustomButton(text: "Done"){
if textFieldValue.count > 0{
handler()
textFieldValue = ""
showAlertWithTextField.toggle()
}else{
showError = true

}
}

}
}
}
}
}

struct ErrorView: View{
var body: some View {
HStack{
Image(systemName: "exclamationmark.triangle")
.foregroundStyle(Color.red)
.padding(4)
Text("Insert text")
.padding(4)
.font(.custom("ArialRoundedMTBold", size: 14))
.foregroundColor(Color.red)
}
}
}

struct SmpleAlert: View{
@Binding var showSimpleAlert: Bool
var title: String
var message: String
var body: some View{
VStack {
Spacer(minLength: 20)
Text(title).padding(5)
Text(message).padding(5)
.multilineTextAlignment(.center)
.lineSpacing(4.0)
Spacer(minLength: 35)
HStack(alignment:.center){
Spacer()
CustomButton(text: "OK") {
withAnimation(.linear(duration: 0.2)){

showSimpleAlert.toggle()
}
}

Spacer()
}
Spacer(minLength: 20)
}
}
}

The code for the CustomButton structure introduced in my previous article is shown below:

struct CustomButton: View {
var text : String
var action: () -> Void
var body: some View {
Button(text, action: {
action()
})
.padding(10)
.background(.white)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.cyan, lineWidth: 1)
)
}
}

The ContentView structure hold the code to add or delete lists. When you tap on the ToolbarItem “Add List,” an alert appears that lets you enter the list's name. For each name typed, the save() method is called. Note that there is a check to avoid duplicate entries — the save() method calls the searchName() method to see if the list name already exists. If so, a simple alert with a warning message is displayed, otherwise, the new name is saved. Here’s the code:

import SwiftUI
import CoreData

struct ContentView: View {
@State var showTextFieldAlert : Bool = false
@State var showSimpleAlert: Bool = false
@State var showList: Bool = true
@State var textFieldValue: String = ""

@Environment(\.managedObjectContext) private var viewContext

@FetchRequest<Lists>(sortDescriptors: [NSSortDescriptor(keyPath: \Lists.name, ascending: true)])
var itemList: FetchedResults<Lists>
var body: some View {
NavigationStack {
ZStack(alignment:.top){

if $showList.wrappedValue {
VStack{
List {
ForEach(itemList){list in
NavigationLink{
ListItems(list: list)
}label: {
Text(list.name ?? "")
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button {
delete(list)
} label: {
Image(systemName:"trash")
}
.tint(.red)
}
}
}
}
}

.navigationBarTitle("My Lists")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing){
Button(action: {
showTextFieldAlert.toggle()

}){
Text("Add List")
}
}
}
.foregroundColor(.cyan)
}
if $showTextFieldAlert.wrappedValue {
VStack{
CustomAlert(textFieldValue: $textFieldValue, showSimpleAlert:.constant(false), showAlertWithTextField: $showTextFieldAlert, title: "Add List",message: "", placeholder: "Insert list name", handler: save)
}
}
if $showSimpleAlert.wrappedValue {
VStack{
CustomAlert(textFieldValue: .constant(""), showSimpleAlert: $showSimpleAlert, showAlertWithTextField: .constant(false), title: "List already added",message: "List name already present", placeholder: "Insert List name", handler: {})
}
}
}
}

}
func save(){
let result = searchName(textFieldValue)
if result?.first != nil{
showSimpleAlert.toggle()
}else{
let entity = Lists(context: viewContext)
entity.name = textFieldValue

do{
try PersistenceContainer.shared.save()
}catch{
fatalError(error.localizedDescription)
}
}
}
func delete(_ object: NSManagedObject){
withAnimation(.linear(duration: 0.2)) {
do{
try PersistenceContainer.shared.delete(object)
}catch{
fatalError(error.localizedDescription)
}
}
}
func searchName(_ name: String) -> [Lists]?{
let fetchRequest: NSFetchRequest<Lists> = Lists.fetchRequest()

fetchRequest.entity = Lists.entity()

fetchRequest.predicate = NSPredicate(
format: "name CONTAINS %@", name
)
return try? viewContext.fetch(fetchRequest)
}
}

As you can see, each list points to its elements, which are represented by the ListItems structure. Notice that ListItems is passed the current list. The code of the ListItems structure is similar to that for managing lists,

Also, the list is checked to see if an element with the same name already exists before adding an element to the current list. To do this, I use the following method:

func searchName(_ name: String,_ from: Lists) -> [Item]?{
var listItem: [Item] = []
if let matches = list.toItem?.allObjects as? [Item]{
matches.forEach { item in
if item.name == name{
listItem.append(item)

}
}
}
return listItem
}

This method is called from the save() method. If an item with the same name is found in the current list, an alert with a warning message is displayed. Otherwise, the item is saved on the PersistenceContainer. Note that in addition to setting the item name and image, you assign the current list to the item’s relationship toLists. Here’s how to do that:

func save(){
let result = searchName(textFieldValue, list)
if result?.first != nil{
showSimpleAlert.toggle()
}else{
let entity = Item(context: viewContext)
entity.name = textFieldValue
entity.image = UIImage(systemName: "checkmark")?.pngData()

entity.toLists = list
do{
try PersistenceContainer.shared.save()
}catch{
fatalError(error.localizedDescription)
}
}
}

Furthermore, here’s the code to check if an item is a duplicate of another on the current list:

if item.toLists?.name == list.name

If the condition is true, then the item is displayed. The item image is saved as binary data, and when displayed, it is converted to UIImage and then to Image. The complete ListItems code is as follows:

import SwiftUI
import CoreData

struct ListItems: View {
@State var showTextFieldAlert : Bool = false
@State var showSimpleAlert: Bool = false
@State var showList: Bool = true
@State var textFieldValue: String = ""

@Environment(\.managedObjectContext) private var viewContext

@FetchRequest<Item>(sortDescriptors: [NSSortDescriptor(keyPath: \Item.name, ascending: true)])
var items: FetchedResults<Item>
var list : Lists

var body: some View {
ZStack(alignment:.top){

if $showList.wrappedValue {
VStack{
List {
ForEach(items){item in
if item.toLists?.name == list.name{
if let uiimage = UIImage(data:item.image!){
let image = Image(uiImage: uiimage)
HStack{
image
.resizable()
.scaledToFill()
.frame(width: 40, height: 40)
.cornerRadius(20)
Text(item.name ?? "")
}
.padding()
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button {
delete(item)
} label: {
Image(systemName:"trash")
}
.tint(.red)
}
}
}
}
}
.navigationBarTitle(list.name ?? "")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing){
Button(action: {
showTextFieldAlert.toggle()

}){
Text("Add Item")
}
}
}
.foregroundColor(.cyan)
}
}
if $showTextFieldAlert.wrappedValue {
VStack{
CustomAlert(textFieldValue: $textFieldValue, showSimpleAlert:.constant(false), showAlertWithTextField: $showTextFieldAlert, title: "Add Item",message: "", placeholder: "Item name", handler: save)
}
}
if $showSimpleAlert.wrappedValue {
VStack{
CustomAlert(textFieldValue: .constant(""), showSimpleAlert: $showSimpleAlert, showAlertWithTextField: .constant(false), title: "Item already added",message: "Item name already present", placeholder: "Item name", handler: save)
}
}
}
}

func save(){
let result = searchName(textFieldValue, list)
if result?.first != nil{
showSimpleAlert.toggle()
}else{
let entity = Item(context: viewContext)
entity.name = textFieldValue
entity.image = UIImage(systemName: "checkmark")?.pngData()

entity.toLists = list
do{
try PersistenceContainer.shared.save()
}catch{
fatalError(error.localizedDescription)
}
}
}

func delete(_ object: NSManagedObject){
withAnimation {
do{
try PersistenceContainer.shared.delete(object)
}catch{
fatalError(error.localizedDescription)
}
}
}

func searchName(_ name: String,_ from: Lists) -> [Item]?{
var listItem: [Item] = []
if let matches = list.toItem?.allObjects as? [Item]{
matches.forEach { item in
if item.name == name{
listItem.append(item)

}
}
}
return listItem
}
}

I hope you enjoyed this tutorial and found it helpful.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Teresa Bagalà
Teresa Bagalà

Written by Teresa Bagalà

Software engineer, iOS developer, code enthusiast

Write a response