How To Save List Ordering in SwiftUI to CoreData

In this post, we will learn how to save the list order when customizing the list order when you drag the cells and save those changes back to CoreData.

First, we create a new Entity from CoreData. A simple Item entity with a couple of attributes

  • order (Integer 16)
  • timestamp (Date)

Next, we will create a Persistence manager struct to handle the CoreData persistence.

import CoreData
struct PersistenceController {
static let shared = PersistenceController()
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
for _ in 0..<10 {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
}
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
return result
}()
let container: NSPersistentContainer
var viewContext: NSManagedObjectContext {
return container.viewContext
}
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "Saving_CoreData_List_Swiftui")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
/*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
}
....
.....
}

We will add a couple of functions to make a request with a NSSortDescriptor with the “order” attribute as the key and another function to simply save the viewContext changes.

import CoreData
struct PersistenceController {
.......
func fetchItems(predicate: NSPredicate?) -> [Item] {
let request: NSFetchRequest<Item> = Item.fetchRequest()
//sort order
let sortOrder = NSSortDescriptor(key: "order", ascending: true)
request.sortDescriptors = [sortOrder]
if let predicate = predicate {
request.predicate = predicate
}
do {
return try viewContext.fetch(request)
} catch {
return []
}
}
}
import CoreData
struct PersistenceController {
.......
.......
//MARK: Save object graph
func save() {
do {
try viewContext.save()
} catch {
viewContext.rollback()
print(error)
}
}
}
view raw save hosted with ❤ by GitHub

Once that initial setup of the CoreData is done, we can create a List that iterates to all the Items. Create a new class and make it an ObservableObject. We will load the items on initilization.

import Foundation
class ContentVM: ObservableObject {
@Published var items: [Item] = []
init() {
loadItems()
}
func loadItems() {
items = PersistenceController.shared.fetchItems(predicate: nil)
}
}

Finally, we can create our view using SwiftUI.

Create the StateObject and call the ContentVM

import SwiftUI
import CoreData
struct ContentView: View {
@Environment(\.managedObjectContext) private var viewContext
@StateObject var vm = ContentVM()
var body: some View {
Text("Hello")
}
}

Create a NavigationView and add List which we will add ForEach to iterate to any loaded items.

We can add .onDelete() at the end of the ForEach.

Then the .onMove() call will contain our logic for sorting.

NavigationView {
List {
ForEach(vm.items) { item in
NavigationLink {
Text("Item at \(item.timestamp!, formatter: itemFormatter)")
} label: {
Text(item.timestamp!, formatter: itemFormatter)
}
}
.onDelete(perform: deleteItems)
.onMove(perform: move)
}
}

It basically creates temporary Items from the existing Items array and reorders the index and finally saves the viewContext.

private func move( from source: IndexSet, to destination: Int)
{
// Make an array of items from fetched results
var revisedItems: [ Item ] = vm.items.map{ $0 }
// change the order of the items in the array
revisedItems.move(fromOffsets: source, toOffset: destination )
// update the userOrder attribute in revisedItems to
// persist the new order. This is done in reverse order
// to minimize changes to the indices.
for reverseIndex in stride( from: revisedItems.count - 1,
through: 0,
by: -1 )
{
revisedItems[ reverseIndex ].order =
Int16( reverseIndex )
}
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}

Full source code can be downloaded in github here.

Leave a Reply

Your email address will not be published. Required fields are marked *