I'd like to update a CoreData Object.
Backgrund: I made an app which includes a UITableView. In the textLabel of the UITableViewCell is a name. In the detailTextLabel of this cell is a date which can be changed/updated. Now I'd like to change this date.
I wrote the following code:
var people = [NSManagedObject]()
func saveDate(date: NSDate) {
//1
let appDelegate = UIApplication.sharedApplication().delegate as AppDelegate
let managedContext = appDelegate.managedObjectContext!
//2
let entity = NSEntityDescription.entityForName("Person", inManagedObjectContext:managedContext)
let person = people[dateIndexPath.row]
//3
person.setValue(date, forKey: "datum")
//4
var error: NSError?
if !managedContext.save(&error) {
println("Could not save \(error), \(error?.userInfo)")
}
//5
people.append(person)
tableView.reloadData()
}
Now, if I run this code:
The date is successfully updated but the cell in which the date has been updated is displayed 2 times. For example if I added 3 cells and changed the date in the 3rd cell, I now get 4 cells displayed and 2 of them have the same content/are duplicated.
Does someone knows how to solve this problem?
You're adding an additional object to your array each time. Your updated Person is already in the array and will display the new information when you reload your table data. To fix this, just take out this line:
people.append(person)
You're going to want to associate some kind of unique identifier attribute to your Person class. This allows to retrieve that same object later using it identifier. I would suggest using a UUID string value, called personID, identifier, or something similar.
You can override the awakeFromInsert method on your Person class like so:
// This is called when a new Person is inserted into a context
override func awakeFromInsert()
{
super.awakeFromInsert()
// Automatically assign a randomly-generated UUID
self.identifier = NSUUID().UUIDString
}
When you want to edit an existing Person, you want to retrieve it by the UUID. I suggest a class function like so (in the Person class):
class func personWithIdentifier(identifier: String, inContext context: NSManagedObjectContext) -> Person?
{
let fetchRequest = NSFetchRequest(entityName: "Person")
fetchRequest.predicate = NSPredicate(format: "identifier ==[c] %#", identifier)
fetchRequest.fetchLimit = 1 // Only want the first result
var error : NSError?
let results = context.executeFetchRequest(fetchRequest, error: &error) as [Person]
// Handle error here
return results.first?
}
This way you can use the following function:
let identifier = ...
let context = ...
var person = Person.personWithIdentifier(identifier, inContext: context)
if let person = person
{
// Edit the person
person.value = // change the values as you need
}
else
{
// Person does not exist!
person = // possibly create a person?
}
Related
I am new to Swift and I am creating a survey application in which the user data gets saved throughout.
In the code below the search button refers to the retrieve function. I created a retrieve function and wanted the saved information to appear on the label. I need help to give the label the value of an element in my array. So basically once the retrieve button has been clicked it returns the saved information to the label.
#IBAction func searchButton(_ sender: Any) {
retrieveData()
}
#IBOutlet weak var statusLabel: UILabel!
// These are the labels that I have made.
////////////////////////////////////////////////////////////////////////
func retrieveData() {
//As we know that container is set up in the AppDelegates so we need to refer that container.
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
//We need to create a context from this container
let managedContext = appDelegate.persistentContainer.viewContext
//Prepare the request of type NSFetchRequest for the entity
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Data")
fetchRequest.fetchLimit = 1
fetchRequest.predicate = NSPredicate(format: "id = %#", idText.text!)
fetchRequest.sortDescriptors = [NSSortDescriptor.init(key: "id", ascending: false)]
//
do {
let result = try managedContext.fetch(fetchRequest)
for data in result as! [NSManagedObject] {
//print(data.value(forKey: "id")!)
idText.text = data.value(forKey:"id") as? String
}
statusLabel.text = "Data found"
showidLabel.text = // I WANT TO ASSIGN THE SAVED VALUE TO THIS LABEL
} catch {
print("Failed")
}
}
You should be able to set the text of the label to the text that you want to show. Either way, you need to define and have access to the label in your UIViewcontroller. I think that may be where you are missing it. You only have statusLabel with an outlet.
So, you can make a function that updates the value of your label and pass the value that you want it to be.
Or, if you only update the Label text when you are also doing something else, pass that value to the label text.
MyLabel.text = value
If your value is in an array and you know the id, you will say:
MyLabel.text = array[id]
in my ios swift application I have a database using Core Data.
It has many entities, all entities have an integer field called syncStatus. it can be 0, 1, or 2.
On startup, I want to loop through ALL the entities that have syncStatus = 1 and change it to 0
Is there a way to do it without fetching each type alone and changing it?
So what I want is:
fetch ALL entities with syncStatus = 1
Loop through them and set syncStatus = 0
Currently I'm doing them one by one:
fetch UserEntities with syncStatus = 1
Loop through them and set syncStatus = 0
fetch countryEntities with syncStatus = 1
Loop through them and set syncStatus = 0
Do the same for every entity one by one
code:
let allUsers = context?.fetch(FetchRequest<UserEntity>().filtered(with: "syncStatus", equalTo: "1"))
let allCountries = context?.fetch(FetchRequest<CountryEntity>().filtered(with: "syncStatus", equalTo: "1"))
.
.
.
I'm just trying to find a generic approach, in case later we add another entity/table we don't have to come back to this code and add it here also.
First of all, fetching all entries and filter them is much more expensive than applying a predicate.
I recommend to use a protocol extension with static methods. The benefit is that you can call the methods directly on the type
protocol SyncStatusResettable
{
associatedtype Entity: NSManagedObject = Self
var syncStatus : String {get set}
static var entityName : String { get }
static func resetSyncStatus(in context: NSManagedObjectContext) throws
}
extension SyncStatusResettable where Entity == Self
{
static var entityName : String {
return NSStringFromClass(self).components(separatedBy: ".").last!
}
static func resetSyncStatus(in context: NSManagedObjectContext) throws
{
let request = NSFetchRequest<Entity>(entityName: entityName)
request.predicate = NSPredicate(format: "syncStatus == 1")
let items = try context.fetch(request)
for var item in items { item.syncStatus = "0" }
if context.hasChanges { try context.save() }
}
}
To use it, adopt SyncStatusResettable for all NSManagedObject subclasses and call
do {
try UserEntity.resetSyncStatus(in: managedObjectContext)
try CountryEntity.resetSyncStatus(in: managedObjectContext)
} catch { print(error) }
managedObjectContext is the current NSManagedObjectContext instance
NSManagedObjectModel allows you to enumerate through the entities it contains, and NSEntityDescription can give you properties for each entity. Once you have a reference to the model:
let entitiesWithSync = model.entities.filter {
$0.properties.contains(where: { $0.name == "syncStatus" })
}
Will give you all of the relevant entities. You can then use this list of entities to drive your updates - note that using NSBatchUpdateRequest is faster if you're doing this on startup. You can create batch update requests using the entity descriptions obtained in the loop above.
In the past I have looped through all the entitiesByName from the object model:
lazy var managedObjectModel: NSManagedObjectModel = {
let modelUrl = NSBundle.mainBundle().URLForResource("SomeProject", withExtension: "momd")!
return NSManagedObjectModel(contentsOf: modelUrl)
}
func updateAllData() {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext
context.performAndWait {
let allEntities = self.managedObjectModel.entitiesByName
for (entity, items) in allEntities {
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: entity)
...
}
}
}
I'm pretty new to Swift/Xcode and I'm faced with a problem of trying to place CoreData attribute values into an array.
The app is a simple shopping list with an entity with 5 attributes:
entity is "Item"
image Binary data
name String
note String
qty String
status Boolean
The user creates a new shopping item which is displayed in a UITableVIewController (so far I'm fine)
I have to data loading into cells correctly.
I also want to display the total quantity(entity "qty") of items in a label.
I can't seem to put each qty into an array so that I can add them together to display them. There seems to be plenty of resources for Objective C but not much out there for swift.
These are what I've been looking at:
Filling an array with core data attributes
CoreData to Array in Swift
Swift: Fetch CoreData as Array
coredata - fetch one attribute into an array
UITableViewController Code:
let moc = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
var frc2 : NSFetchedResultsController = NSFetchedResultsController()
func fetchRequest2() -> NSFetchRequest {
let fetchRequest2 = NSFetchRequest(entityName: "Item")
let sortDescriptor2 = NSSortDescriptor(key: "name",ascending:true)
fetchRequest2.sortDescriptors = [sortDescriptor2]
return fetchRequest2
}
override func viewDidLoad() {
super.viewDidLoad()
//set frc
frc2 = getFRC()
frc2.delegate = self
do {
try frc2.performFetch()
} catch {
print("failed to perform initial fetch request")
return
}
self.tableView.reloadData()
}
Any help would be greatly appreciated.
I have entities Application and Process, one application can have many processes, but one process can only have one application. I get one specific application entity:
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let managedContext = appDelegate.managedObjectContext
let appfetchRequest = NSFetchRequest(entityName: "Application")
//appfetchRequest.returnsObjectsAsFaults = false
do{
let results = try managedContext.executeFetchRequest(appfetchRequest)
applications.addObjectsFromArray(results as! [NSMutableArray])
}catch let error as NSError{
print("Jakis blad z applications entity: \(error)")
}
let predicate:NSPredicate = NSPredicate(format: "name == %#", appTitle)
let application:NSArray = applications.filteredArrayUsingPredicate(predicate)
and in this I have relationship (named applicationprocesses).
I try to get an array with these entities in many ways, but no one work.
Actually, I have:
let processes = application.valueForKey("applicationprocesses").allObjects.first
print(processes?.valueForKey("applicationprocesses"))
And this give me:
Optional({(
<NSManagedObject: 0x7f945871b7a0> (entity: Application; id: 0xd000000000080002 <x-coredata://00C2FE4A-143B-436E-B39B-A0A32C300B68/Application/p2> ; data: {
appcolor = "#3F3F3F";
appicon = bed;
applabel = Proces;
applabelplural = Procesy;
applicationprocesses = (
"0xd000000000140004 <x-coredata://00C2FE4A-143B-436E-B39B-A0A32C300B68/Process/p5>",
"0xd000000000100004 <x-coredata://00C2FE4A-143B-436E-B39B-A0A32C300B68/Process/p4>",
"0xd000000000180004 <x-coredata://00C2FE4A-143B-436E-B39B-A0A32C300B68/Process/p6>",
"0xd0000000001c0004 <x-coredata://00C2FE4A-143B-436E-B39B-A0A32C300B68/Process/p7>",
"0xd0000000000c0004 <x-coredata://00C2FE4A-143B-436E-B39B-A0A32C300B68/Process/p3>"
);
companyid = 392;
id = 1261;
name = "aplikacja 1";
processescount = 5;
})
)})
I need to display these processes in a UITablewView, so I need an array.
I will be grateful for any help.
The problem you are having is not the result of a bad line of code somewhere. It is actually working as it is supposed to. But you can make it a lot easier to work with NSManagedObject
Any fetch from your database results in [AnyObject]. If you leave it like it is, you are forced to use key-value coding which is a pain and very easy to mess up.
However it is very simple to create Classes from CD Entities and downcast the fetch result. This is an awesome feature of CoreData that unfortunately is not stressed enough.
link to gist
Your related entities might look like this:
Go to Menu -> Editor -> Create....
Select the entities you want to create a subclass for.
New files will show up in your project :
Now you can use code like this :
Insert objects
Notice the .... as? Company this is the downcast.
It allows you to access the attributes from the CD Entity like you would access any attributes from a Struct or Class.
func createACompany() {
// no appDel here. appDel and managedContext are best declared in the class scope. See gist for entire ViewController
guard let moc = managedContext else { return }
guard let company = NSEntityDescription.insertNewObjectForEntityForName("Company", inManagedObjectContext: moc) as? Company else {
return // guard is handy to "abort"
}
company.name = "Apple"
guard let bob = NSEntityDescription.insertNewObjectForEntityForName("Employee", inManagedObjectContext: moc) as? Employee else {
return
}
bob.name = "Bob"
bob.company = company
do {
try moc.save()
} catch {
print("core data save error : \(error)")
}
moc.refreshAllObjects()
}
Fetch objects
func fetchCompanies() -> [Company] {
guard let moc = managedContext else { return [] }
let request = NSFetchRequest(entityName: "Company")
request.returnsObjectsAsFaults = true
do {
let results = try moc.executeFetchRequest(request)
guard let companies = results as? [Company] else {
return []
}
return companies
}catch let error as NSError{
print("core data fetch error: \(error)")
return []
}
}
Get related objects
Look closely at the guard statement.
let employeeSet = company.employees -> unwrap of optional NSSet
let employees = employeeSet.allObjects -> get all objects in the NSSet
as? [Employee] -> downcast the result of allObjects to an Array
of Employee
func getEmployees(forCompany company : Company) -> [Employee] {
guard let employeeSet = company.employees, let employees = employeeSet.allObjects as? [Employee] else {
return []
}
return employees
}
Side note:
If you change your mind about the naming of the class and you change it everywhere. Don't forget the update it here too :
The class field has to be updated.
Similar to the InterfaceBuilder. If you change the class name it will not find it anymore.
I believe that you want to get all the managed objects (process) relate to the application object you already got. We usually use the "entity" word for same things as table in database.
I'm sorry I didn't have swift version for this line of code. I'm not sure about my swift skill
NSArray *processes = [[application fristObject] objectIDsForRelationshipNamed:#"applicationprocesses"]
Then use API to convert objectID to ManagedObject
(__kindof NSManagedObject *)objectWithID:(NSManagedObjectID *)objectID;
The only problem is this API, (NSArray *)objectIDsForRelationshipNamed:(NSString *)key , only supported from version 8.3
I am using uicollectionview to display some photos and I have a button that allows the user to delete the selected photo.
It works perfectly unless I try to delete the last photo in the array of photos being used to populate the uicollectionview. Ie if there are 5 photos then there will be a problem if a user removes the 5th photo but not the 1st, 2nd, 3rd or 4th. When I try to delete the 5th photo it crashes on reloadData() with the following error
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to delete item 4 from section 0 which only contains 4 items before the update'
I don't understand why this would happen... The error even mentions "attempt to delete" but I never actually told it I was deleting anything. I just changed the source and then asked it to update. Also in the error message it says the section only contains 4 items before the update when their were actually 5 photos. Why?
A little bit more info about what I'm doing and how (Using Swift)...
I've got a ProgressPhotoSheetClass from which I have instantiated the object progressPhotoSheet
this progressPhotoSheet object has an array of photos and the photos can have priorities such as photo id, title etc
in my number of items in section I use
var numPics: Int = progressPhotoSheet.progressPhotos.count
return numPics
in my cellForItemAtIndexPath I use
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! PhotoHolder
cell.photoTitle.text = progressPhotoSheet.progressPhotos[indexPath.row].photoName
...etc..
When deleting the item I use
progressPhotoSheet.deletePhoto(progressPhotoSheet.progressPhotos[indexPath.row].photoId)
which deletes the photo from the device and from my core data and then reloads progressPhotoSheet.progressPhotos from using the modified core data so it is now exactly how it was before but without the deleted photo.
I then call
self.collectionView.reloadData()
Which should update the UICollectionView for the new data
I could understand if it felt there was a mismatch between what should be in the collection view and what is in the datasource if I were using
self.collectionView.deleteItemsAtIndexPaths(indexPaths)
because that would be saying ignored to get them to match we need to delete one item - here there is a possibility something could mismatch.. But surely using self.collectionView.reloadData() it doesn't matter what changes were made it should just look at what data is there now and update the UICollectionView accordingly....
So my question is... Why am I getting this error and what should I do to fix things so I don't get it?
Edit to include more info
Here is my telephoto Code
func deletePhoto(photoId: Int) {
// Set up Core Data Managed Object Context
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let managedContext = appDelegate.managedObjectContext!
// Fetch correct photo
let fetchRequest = NSFetchRequest(entityName: "CDProgressPhoto")
fetchRequest.predicate = NSPredicate(format: "photoId = %#", String(photoId))
// Save
if let fetchResults = managedContext.executeFetchRequest(fetchRequest, error: nil) as? [NSManagedObject] {
if fetchResults.count != 0{
// Will only be one photo with this photo id
var photo = fetchResults[0]
photo.setValue(true, forKey: "toDelete")
// Save the object
var error: NSError?
if !managedContext.save(&error) {
println("Could not save \(error), \(error?.userInfo)")
}
}
}
// Reload from core data
self.loadPhotoSheetFromCoreData()
}
self.loadPhotoSheetFromCoreData() then empties progressPhotoSheet.progressPhotos before getting the new data from core data... Code below...
private func loadPhotoSheetFromCoreData() {
if(self.hasPhotoSheet()) {
// Clear existing photos
self.progressPhotos = []
// Set up Core Data Managed Object Context
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let managedContext = appDelegate.managedObjectContext!
let request = NSFetchRequest(entityName: "CDProgressPhoto")
let predicate1 = NSPredicate(format: "photoSheetId == %#", String(self.sheetId))
let predicate2 = NSPredicate(format: "toDelete == %#", false)
request.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false) as NSSortDescriptor]
var predicatesArray: [NSPredicate] = [predicate1, predicate2]
//predicatesArray.append(predicate1)
request.predicate = NSCompoundPredicate.andPredicateWithSubpredicates(predicatesArray)
let existings = managedContext.executeFetchRequest(request, error: nil)
let existingPhotos: [CDProgressPhoto] = existings as! [CDProgressPhoto]
// for each photo make a ProgressPhoto object and add to progress photos array
for photo in existingPhotos {
var newPhoto: ProgressPhoto = ProgressPhoto()
newPhoto.photoSheetId = Int(photo.photoSheetId)
newPhoto.photoId = Int(photo.photoId)
newPhoto.photoName = photo.photoName
newPhoto.date = Int(photo.date)
newPhoto.url = photo.url
newPhoto.filename = photo.filename
newPhoto.height = Float(photo.height)
newPhoto.width = Float(photo.width)
newPhoto.selected = false
self.progressPhotos.append(newPhoto)
}
}
}
As you can see the photo isn't actually deleted at this point I just set a toDelete flag to true and then only re load items where toDelete is set to false. The photos are deleted later asynchronously depending on network connection etc because they are also stored on a server for use on the main website.
Have you tried calling invalidateLayout() on the collectionView? That might help incase your view is empty i.e. 0 elements are present.