How To Avoid Creating Duplicate Entries in Core Data in a SwiftUI App: The MVVM Way
Managing persistent ToDoList without inserting duplicate entries
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 ItemViewModel
class: 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.