How To Avoid Creating Duplicate Entries in Core Data in a SwiftUI App: The MVVM Way

Teresa Bagalà
6 min readAug 30, 2023

--

Managing persistent ToDoList without inserting duplicate entries

Foto by <a href=”https://pixabay.com/it/users/roszie-6000120/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=7355222">Rosy</a> from <a href=”https://pixabay.com/it//?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=7355222">Pixabay</a>
Foto by Rosy from Pixabay

In my past article I showed how to save data on Core Data without duplicates, with the MVC pattern. Now let’s face the same issue with the MVVM way, for the same app.

I’ve drawn this article from the following content

This demo app was built with Xcode 14.3.1 and iOS 16.4

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 MoreList app. After that, create the Core Data Model for the demo app, MoreListsModel.xcdatamodeld. Here’s what the project look like:

To view the Core Data Model in detail, see my previous article, here we focus on the MVVM pattern.

Under the Model group I define the PersistenceContainer structure, for Core Data management, nothing new.

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

var viewContext: NSManagedObjectContext {
return container.viewContext
}

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{
guard viewContext.hasChanges else { return }
do {
try viewContext.save()
} catch {
print(error.localizedDescription)
}
}

func delete(_ object: NSManagedObject) throws{
viewContext.delete(object)
try viewContext.save()

}
}

In the project I add a new group now, the ViewModel group, where I define two classes, the ListViewModel class and the ItemViewModelclass: they are links between the underlying Core Data model (through the PersistenceContainer) and the corresponding View.

Let’s see the code of ListViewModel class:

class ListViewModel: ObservableObject{
private let container = PersistenceContainer.shared

@Published var listArray : [Lists] = []

init(){
fetchList()
}

func fetchList(){
let request = NSFetchRequest<Lists>(entityName: "Lists")
do{
listArray = try container.viewContext.fetch(request)
}catch{
print("Error: An error occured while fetching Lists")
}
}
func addList(_ name: String) -> Bool{
let result = searchName(name)
if result?.first != nil{
return true
//showSimpleAlert: see the method save() in ContentView view below
}else{
let entity = Lists(context: container.viewContext)
entity.name = name

save()
self.fetchList()
}
return false
}

func save(){
do{
try container.save()
}catch{
fatalError(error.localizedDescription)
}
}

func delete(_ object: NSManagedObject){
withAnimation {
do{
try container.delete(object)
}catch{
fatalError(error.localizedDescription)
}
}
self.fetchList()
}

func searchName(_ name: String) -> [Lists]?{
var lists : [Lists] = []
listArray.forEach { list in
if name == list.name{
lists.append(list)
}
}
return lists
}
}

The ListViewModel class is conform to ObservableObject protocol. There is a published property listArray of [Lists] type and all methods to fetch all Lists , to add or delete a Lists from Core Data. The addList() method calls the searchName() method to see if the list name I want to add already exists in Core Data. If so, it returns true to the caller and a simple warning message will be displayed in the corresponding ContentView; otherwise the new list name is saved and the method returns false to the caller. The communication with Core Data is made possible using the PersistenceContainer singleton. Note that delete() and add() methods call fetchList() method, in order to update the listArray.

Let’s see the ItemViewModel class:


class ItemViewModel: ObservableObject{
private let container = PersistenceContainer.shared

@Published var itemArray : [Item] = []
var list: Lists

init(list: Lists){
self.list = list
fetchItems()
}

func fetchItems(){
let itemSet = list.toItem as? Set<Item> ?? []
itemArray = itemSet.sorted(by: { item1, item2 in
if let name1 = item1.name, let name2 = item2.name{
return name1 < name2
}
return false
})
}

func AddItem(_ name: String,check list: Lists) -> Bool{
let result = searchName(name,in: list)
if result?.first != nil{
return true
//showSimpleAlert: see the method save() in ListItems view below
}else{
let entity = Item(context: container.viewContext)
entity.name = name
entity.image = UIImage(systemName: "checkmark")?.pngData()
entity.toLists = list

save()
self.fetchItems()
}
return false
}

func save(){
do{
try container.save()
}catch{
fatalError(error.localizedDescription)
}
}

func delete(_ object: NSManagedObject){
withAnimation {
do{
try container.delete(object)
}catch{
fatalError(error.localizedDescription)
}
}
self.fetchItems()
}

func searchName(_ name: String,in list: 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
}
}

Here too we have a published property itemArray of [Item] type , and the methods to fetch all items, to add and delete an Item. In the addList() method I check if the Item I want to add to itemArray is already present in the corresponding Lists: this is why the ItemViewModel declares a list variable of Lists type. Note that the PersistenceContainer singleton is present in each ViewModel.

Let’s see the views code now, starting from the ContentView, where are displayed the lists of all the Lists element, with the ability to add or delete elements:

struct ContentView: View {
@ObservedObject var viewModel : ListViewModel
@State var listName: String = ""

@State var showTextFieldAlert : Bool = false
@State var showSimpleAlert: Bool = false
@State var showList: Bool = true
@State var showAnimation = false

init(){
self.viewModel = ListViewModel()
}

var body: some View {

NavigationStack {
ZStack(alignment:.top){

if $showList.wrappedValue {
VStack{
List {
ForEach(viewModel.listArray){list in
NavigationLink{
ListItems(list: list)
}label: {
Text(list.name ?? "")
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button(role: .destructive) {
viewModel.delete(list)
} label: {
Label("Delete", systemImage: "trash")
}

}
}
}
}
}

.navigationBarTitle("My Lists")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing){
Button(action: {
showTextFieldAlert.toggle()
}){
Text("Add List")
}
}
}
.foregroundColor(.cyan)
}
if $showTextFieldAlert.wrappedValue {
VStack{
CustomAlert(textFieldValue: $listName, showSimpleAlert:.constant(false), showAlertWithTextField: $showTextFieldAlert,showAnimation: $showAnimation, title: "Add List",message: "", placeholder: "Insert list name", handler: save)
}
}
if $showSimpleAlert.wrappedValue {
VStack{
CustomAlert(textFieldValue: .constant(""), showSimpleAlert: $showSimpleAlert, showAlertWithTextField: .constant(false),showAnimation: $showAnimation, title: "List already added",message: "List name already present", placeholder: "Insert List name", handler: {})
}
}
}
}
}

func save(){
showAnimation = false
showSimpleAlert = viewModel.addList(listName)
}
}

Each change of viewModel published property will update the ContentView. All communication with CoreData are made only through the ListViewModel now.

Let’s see the ListItems view, where are displayed the lists of all the Item element:

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

init(list: Lists){
self.viewModel = ItemViewModel(list: list)
viewModel.fetchItems()
}

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

if $showList.wrappedValue {
VStack{
List {
ForEach(viewModel.itemArray){item in
if let uiimage = UIImage(data:item.image!){
let image = Image(uiImage: uiimage)
HStack{
image
.resizable()
.scaledToFill()
.frame(width: 30, height: 30)
.cornerRadius(20)
Text(item.name ?? "")
}
.padding()
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button(role: .destructive) {
viewModel.delete(item)
} label: {
Label("Delete", systemImage: "trash")
}

}
}
}
}
.navigationBarTitle(viewModel.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,showAnimation: $showAnimation, title: "Add Item",message: "", placeholder: "Item name", handler: save)
}
}
if $showSimpleAlert.wrappedValue {
VStack{
CustomAlert(textFieldValue: .constant(""), showSimpleAlert: $showSimpleAlert, showAlertWithTextField: .constant(false), showAnimation: $showAnimation, title: "Item already added",message: "Item name already present", placeholder: "Item name", handler: {})
}
}
}
}

func save(){
showAnimation = false
showSimpleAlert = viewModel.AddItem(textFieldValue, check: viewModel.list)
}

}

Here, in the init() method, I pass the current Lists. All operation on Core Data are performed through the ItemViewModel now, and each change of viewModel published property will update the ListItems view.

Finally here is MoreListApp :

@main
struct MoreListsApp: App {

var body: some Scene {
WindowGroup {
ContentView()

}
}
}

That’s all. You can find the app video in in my past article .

I hope you enjoyed this tutorial and found it helpful.

--

--