How To Avoid Creating Duplicate Entries in Core Data in a SwiftUI App
Manage persistent ToDoLists without inserting duplicate entries

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.