How to Update User Interface on Core Data

How to update user interface on Core Data?

Let's take the easy (but not so elegant) route here. You'll have to pass over all the fetched objects to the detail VC like this:

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {

if segue.identifier == "yourSegueIdentifier"{

if let destinationVC = segue.destinationViewController as? DetailViewController{

destinationVC.managedObjectContext = yourContext
destinationVC.retrieveData = yourManagedObject
destinationVC.arrayOfFetchedObjects = yourFetchedResultsController.fetchedObjects
//pass over other data...

}
}
}

Then, in your detailVC, write a method that will be executed when you press the delete button. Something like this:

@IBAction func trashButton(sender: AnyObject) {

//make sure you have an array with YourObjects
guard let fetchedObjects = arrayOfFetchedObjects as? [YourObjectType] else {return}

//get index of the shown object in the array of fetched objects
let indexOfObject = fetchedObjects.indexOf(retrieveData)

//delete the object from the context
self.managedObjectContext.deleteObject(retrieveData)

do {
try self.managedObjectContext.save()

//delete the object from the fetchedObjects array
fetchedObjects.removeAtIndex(indexOfObject)
} catch {

}

//get the object that should be shown after the delete
if indexOfObject != 0{
//we want the object that represents the 'older' note
retrieveData = fetchedObjects[indexOfObject - 1]
updateUserInterface(true)

}
else{
//the index was 0, so the deleted object was the oldest. The object that is the oldest after the delete now takes index 0, so just use this index. Also check for an empty array.

if fetchedObjects.isEmpty{
updateUserInterface(false)
}
else{
retrieveData = fetchedObjects[0]
updateUserInterface(true)
}

}
}

func updateUserInterface(note: Bool){

switch note{

case true:

//update the user interface
if let demo = retrieveData.valueForKey("titleField") as? String {
self.containerLabel.text = demo
}

case false:

self.containerLabel.text = "no more notes"

}
}

Updating UI while importing large data sets in Core Data

I have currently solved this by initializing the ManagedObjectContext with concurrency type NSPrivateQueueConcurrencyType and setting the parent context to the main ManagedObjectContext.
For anybody having the same problem:

- (void)doSomething
{
_backgroundMOC = [[NSManagedObjectContext alloc]initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_backgroundMOC setParentContext:[kDelegate managedObjectContext]];

for (int i = 0; i < [cars count]; i++)
{
[_backgroundMOC performBlockAndWait:^{

Drive *drive = (Drive *)[NSEntityDescription insertNewObjectForEntityForName:@"Drive" inManagedObjectContext:_backgroundMOC.parentContext];

....do more stuff...

}];
}

[self performSelectorOnMainThread:@selector(showProgress:) withObject:[NSNumber numberWithFloat:((float)i/(float)[cars count])] waitUntilDone:NO];
}

For this to work you will have to change the managedObjectContext in the AppDelegate:

- (NSManagedObjectContext *)managedObjectContext
{
if (_managedObjectContext != nil)
{
return _managedObjectContext;
}

NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil)
{
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_managedObjectContext setPersistentStoreCoordinator:coordinator];

}

return _managedObjectContext;
}

SwifUI: How to get manual list to refresh when updating CoreData records?

OK it turned out to be really quite simple. All I actually had to do was remove some of the @Published and provide a UUID for the repeatedPerson record (and for == and hash).

import SwiftUI
import CoreData

let persistentContainerQueue = OperationQueue()
let firstNames = ["Michael", "Damon", "Jacques", "Mika", "Fernando", "Kimi", "Lewis", "Jenson", "Sebastion", "Nico"]
let lastNames = ["Schumacher", "Hill", "Villeneuve", "Hakkinen", "Alonso", "Raikkonen", "Hamilton", "Button", "Vettel", "Rosberg"]

class RepeatedPerson: ObservableObject, Hashable
{
var id: UUID = UUID()
var index: Int
var person: Person?

init (person: Person, index: Int)
{
self.person = person
self.index = index
}

func hash(into hasher: inout Hasher)
{
hasher.combine(id)
}

static func == (lhs: RepeatedPerson, rhs: RepeatedPerson) -> Bool
{
return lhs.id == rhs.id
}
}

class RepeatedPeople: ObservableObject
{
@Published var people: [RepeatedPerson] = []
}

func getRepeatedPeople() -> [RepeatedPerson]
{
var repeatedPeople:[RepeatedPerson] = []

let records = allRecords(Person.self)
for person in records
{
for index in 1...3
{
repeatedPeople.append(RepeatedPerson(person: person, index: index))
}
}
return repeatedPeople
}

struct ContentView: View
{
@Environment(\.managedObjectContext) private var viewContext

@ObservedObject var repeatedPeople = RepeatedPeople()

init()
{
repeatedPeople.people = getRepeatedPeople()
}

var body: some View
{
VStack
{
List()
{
ForEach(repeatedPeople.people, id: \.self)
{ repeatedPerson in
Text("\(repeatedPerson.index)) \(repeatedPerson.person!.firstName!) \(repeatedPerson.person!.lastName!)")
}
}
HStack
{
Button("Add Record", action:
{
addItem()
repeatedPeople.people = getRepeatedPeople()
})
Button("Change Record", action:
{
let q = allRecords(Person.self)
let r = q.randomElement()!
let oldLastName = r.lastName
r.lastName = lastNames.randomElement()!
print ("changed \(r.firstName!) \(oldLastName!) -> \(r.firstName!) \(r.lastName!)")
saveDatabase()
repeatedPeople.people = getRepeatedPeople()
})
Button("Reset Database", action:
{
print ("Reset database")
deleteAllRecords(Person.self)
repeatedPeople.people = getRepeatedPeople()
})
}
}
}

private func addItem()
{
withAnimation
{
let newItem = Person(context: viewContext)
newItem.timestamp = Date()
newItem.firstName = firstNames.randomElement()!
newItem.lastName = lastNames.randomElement()!
print ("added \(newItem.firstName!) \(newItem.lastName!)")
saveDatabase()
}
}
}

func query<T: NSManagedObject>(_ type : T.Type, predicate: NSPredicate? = nil, sort: NSSortDescriptor? = nil) -> [T]
{
let context = PersistenceController.shared.container.viewContext

let request = T.fetchRequest()
if let sortDescriptor = sort
{
request.sortDescriptors = [sortDescriptor]
}
if let predicate = predicate
{
request.predicate = predicate
}
do
{
let results = try context.fetch(request)
return results as! [T]
}
catch
{
print("Error with request: \(error)")
return []
}
}

func allRecords<T: NSManagedObject>(_ type : T.Type, sort: NSSortDescriptor? = nil) -> [T]
{
return query(T.self, sort: sort)
}

func deleteAllRecords<T: NSManagedObject>(_ type : T.Type)
{
let context = PersistenceController.shared.container.viewContext
let results = allRecords(T.self)
for record in results
{
context.delete(record as NSManagedObject)
}
saveDatabase()
}

func saveDatabase()
{
persistentContainerQueue.addOperation()
{
let context = PersistenceController.shared.container.viewContext
context.performAndWait
{
try? context.save()
}
}
}

SwiftUI List not updating when core data property is updated in other view

NSManagedObject is a reference type so when you change its properties your documents is not changed, so state does not refresh view.

Here is a possible approach to force-refresh List when you comes back

  1. add new state
@State var documents: [ScanDocument] = []
@State private var refreshID = UUID() // can be actually anything, but unique

  1. make List identified by it
List(documents, id: \.id) { item in
ZStack {
DocumentCell(document: item)
}
}.id(refreshID) // << here

  1. change refreshID when come back so forcing List rebuild
NavigationLink(destination: RenameDocumentView(document: documents[selectedDocumentIndex!])
.onDisappear(perform: {self.refreshID = UUID()}),
isActive: $pushActive) {
Text("")
}.hidden()

Alternate: Possible alternate is to make DocumentCell observe document, but code is not provided so it is not clear what's inside. Anyway you can try

struct DocumentCell: View {
@ObservedObject document: ScanDocument

...
}

How to use NSUndoManager with coredata and keep user interace and model in sync?

I have done something similar and the way I found to work best is to separate the UI controller code (the C in MVC) into two separate "paths".

One that observes changes in the core data model through listening for notifications from the core data model NSManagedObjectContextObjectsDidChangeNotification filtering out if the change affects the controllers UI and adjusts the display accordingly. This "path" is blindly following the coreData changes and need no interaction with the user and no undo knowledge.

The other path records changes the user requests and modifies the core data model accordingly. For example if we have a stepper control and a label with a number next to it. User clicks the stepper. Then the controller updates the relevant property on the core data object by adding or subtracting one. This automatically generate undo actions with the core data model. If the user change affects more than one property in the core data all the changes are wrapped in an undo grouping. Then this change to the core data object will trigger the other controller path to update all UI things (the label in the example).

Now undo works automatically opposite. By calling undo on the MOC undo manager coreData will revert the changes to the object which will trigger the first path again and the UI follows automatically.

If the user is editing a text field I usually do not bother tracking changes keystroke by keystroke and instead only capture the results when the text field informs that editing did end. With this approach an undo after edit removes all changes in the previous edit session which is usually what is wanted. If also undo within the text field (e.g. typing aa and cmd-z to undo second a) is desired that can be achieved by providing another undo manager to the window while the textfield is editing - thus avoiding all the keystroke undos in the same undo stack as the core data actions.

One thing to remember is that coreData does sometimes wait with executing some actions which make things appear out of sync. Calling -processPendingChanges on the MOC before ending an undo grouping will fix this.

The other thing to think about is what you want to undo. Are you looking to be able to undo user key entries or to undo changes in the data model. I have found sometimes both but not at the same time hence I have found multiple undo managers useful as noted before. Keep the doc undo manager for only changes to the data model which is things the user may care about long term. Then make a new undo manager and use that while the user is in an edit mode to track individual key presses. Once the user confirms he is happy with the edit as a whole by leaving the text field or pressing OK in a dialog etc throw away that undo manager and get the end result of the edit and stuff that into core data with the use of the doc undo manager. To me these two types of undos are fundamentally different and should not be intertwined in the undo stack.

Below is some code, first an example of the listener for changes (called after receiving a NSManagedObjectContextObjectsDidChangeNotification:

-(void)coreDataObjectsUpdated:(NSNotification *)notif {

// Filter for relevant change dicts
NSPredicate *isSectorObject = [NSPredicate predicateWithFormat: @"className == %@", @"Sector"];

NSSet *set;
BOOL changes = NO;

set = [[notif.userInfo objectForKey:NSDeletedObjectsKey] filteredSetUsingPredicate:isSectorObject];
if (set.count > 0) {
changes = YES;
}
else {
set = [[notif.userInfo objectForKey:NSInsertedObjectsKey] filteredSetUsingPredicate:isSectorObject];
if (set.count > 0) {
changes = YES;
}
else {
set = [[notif.userInfo objectForKey:NSUpdatedObjectsKey] filteredSetUsingPredicate:isSectorObject];
if (set.count > 0) {
changes = YES;
}
}
}
if (changes) {
[self.sectorTable reloadData];
}

}

This is an example of creating a compound undo action, edits are done in a separate sheet and this snippet moves all the changes into a core data object as a single undoable action with a name.

-(IBAction) editCDObject:(id)sender{ 

NSManagedObject *stk = [self.objects objectAtIndex:self.objectTableView.clickedRow];

[self.editSheetController EditObject:stk attachToWindow:self.window completionHandler: ^(NSModalResponse returnCode){

if (returnCode == NSModalResponseOK) { // Write back the changes else do nothing

NSUndoManager *um = self.moc.undoManager;
[um beginUndoGrouping];
[um setActionName:[NSString stringWithFormat:@"Edit object"]];

stk.overrideName = self.editSheetController.overrideName;
stk.sector = self.editSheetController.sector;

[um endUndoGrouping];
}
}];

}

Hope this gave some ideas.



Related Topics



Leave a reply



Submit