NSManagedObject collection properties become nil after a while - swift

I'm working in a subclass of NSViewController where in viewDidLoad I fetch my entities from CoreData as below:
let delegate = AppDelegate.init()
let context = delegate.persistentContainer.viewContext
let fetch = PicPathEntity.entityFetchRequest()
var tempEntities: [PicPathEntity] = []
context.performAndWait {
tempEntities = try! fetch.execute()
}
entities = tempEntities
Then during the itemForRepresentedObjectAt function, it appears that the count of entities is the same, but the property values have become nil/empty.
I did some searching and found similar problems with no clear answers (at least not for Swift)
I can't seem to find information on how to properly fetch and retain the data in my entities variable.

I figured out how to solve this after reading the how the relationship between objects and context work.
Turns out all I needed to make this work, was make the context into a property of the ViewController instead of a local variable of viewDidLoad.
var entities: [PicPathEntity] = []
var context: NSManagedObjectContext!
override func viewDidLoad() {
super.viewDidLoad()
let delegate = AppDelegate.init()
context = delegate.persistentContainer.viewContext
let fetch = PicPathEntity.entityFetchRequest()
context.performAndWait {
entities = try! fetch.execute()
}
}
Due to helpful comments to this answer, I have now updated the code to:
var entities: [PicPathEntity] = []
var context: NSManagedObjectContext!
override func viewDidLoad() {
super.viewDidLoad()
let delegate = NSApplication.shared.delegate as! AppDelegate
context = delegate.persistentContainer.viewContext
let fetch = PicPathEntity.entityFetchRequest()
context.perform {
self.entities = try! fetch.execute()
self.picFinderEntityCollectionView.reloadData()
}
configureCollectionView()
}

Related

Is there a way to update an object in a core data model by pressing bar button item?

My goal here is to update an object in my core data by pressing the done button after editing the text.
The done button and the textfields below:
Here is some of my code,
#objc func doneTapped() {
do {
try context.save()
} catch {
print("Error saving the new information \(error)")
}
dateEditableTextF.resignFirstResponder()
selectedEventDate = dateEditableTextF.text
dateEditableTextF.isEnabled = false
costEditableTextF.resignFirstResponder()
selectedEventCost = costEditableTextF.text
costEditableTextF.isEnabled = false
gradesEditableTextF.resignFirstResponder()
selectedEventGrade = gradesEditableTextF.text
gradesEditableTextF.isEnabled = false
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(editTapped))
}
I expected that when I pressed the done button while running the app after editing the textfields, the information would update and that when I went back to the view controller, the information would be the same and my core data database would be update with an update attribute for that object.
What actually happened was when I finish editing the textfield, the data updates in the view controller, but when I leave the view controller and come back to it, the data reverts to the old entry.
I watched about 4 youtube videos of crud methods and they all were different scenarios and didn't match mine so I was hoping someone here could help. Thanks in advance.
Here's the rest of my view controller.
#IBOutlet weak var costEditableTextF: UITextField!
#IBOutlet weak var dateEditableTextF: UITextField!
#IBOutlet weak var gradesEditableTextF: UITextField!
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
var updateTheEvents = [Event]()
var selectedEventName: String?
var selectedEventDate: String?
var selectedEventCost: String?
var selectedEventGrade: String?
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = selectedEventName
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(editTapped))
if let dateToLoad = selectedEventDate {
dateEditableTextF.text = selectedEventDate
}
if let costToLoad = selectedEventCost {
costEditableTextF.text = selectedEventCost
}
if let gradesToLoad = selectedEventGrade {
gradesEditableTextF.text = selectedEventGrade
}
// Do any additional setup after loading the view.
}
#objc func editTapped() {
dateEditableTextF.becomeFirstResponder()
dateEditableTextF.isEnabled = true
costEditableTextF.isEnabled = true
gradesEditableTextF.isEnabled = true
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneTapped))
}
First you need to create a storage to manage your persistentContainer and the CRUD operations:
class PersistenceManager {
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "your_xcdatamodeld_name") //Here you should type the name of your xcdatamodeld file without the extension .xcdatamodeld
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy
return container
}()
}
Now to save data, you'll need a context. I strongly suggest that you use a global one. I had several issues accessing the store context from external functions (ie. stuff wasn't added/edited). Note that, despite it works great for me, I'm not sure wether a global context is the best practice. I have encountered any issues yet, however.
Inside of your PersistenceManager class, before the persistentContainer put the following code
static let shared = PersistenceManager()
var managedObjectContext: NSManagedObjectContext {
persistentContainer.viewContext
}
And, before and outside of the class put the following
let context = PersistenceManager.shared.managedObjectContext
...
class PersistenceManager { [...] }
Now you'll have to create your saving function like this:
func saveContext () {
if context.hasChanges {
do {
try context.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
This goes inside of the PersistenceManager class.
Now comes the fun part: You'll have to create the CRUD functions. These will all be inside of your PersistenceManager class. I'm going to show you a small demonstration about creating and editing entities.
Let's assume you have an entity named "Item" and it has the attributes name and price.
To save each item, you'll have a function like the following one:
func creaateNewItem(name: String, price: Int) -> Item {
let entity = NSEntityDescription.entity(forEntityName: Item, in: context)
let newItem = Item(entity: entity!, insertInto: context)
newItem.name = name
newItem.price = price
self.saveContext()
return newItem
}
To edit the item, you'll have to fetch it and then assign it the new values:
func editItem(currentItem: Item, newName: String, newPrice: Int) {
let currentName: String = currentItem.name! //Current name
//Looking for the item to edit
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Item")
request.predicate = NSPredicate(format: "name = %#", currentName)
request.returnsObjectsAsFaults = false
do { //Editing the item
let editedItem = (try context.fetch(request))[0] as! Item
editedItem.name = newName
editedItem.price = newPrice
self.saveContext()
} catch let error {
print("Error \n \((error))")
}
}
As you see here I passed some parameters which will allow you to customise your Items. Obviously if you need to assign default values you can remove those parameters.
Now, in your view controller, you'll create an Item array object:
my item : [Item]?
Which will be filled with your items.
To edit your saved items by pressing a bar button you'll now simply have to do the following:
#objc func editMyItem(){
let newName = "Edited Item"
let newPrice = 15
PersistenceManager().editItem(currentItem: item[indexOfYourChoice], newName: newName, newPrice: newPrice)
}
And your item will be edited!
Note that if you want the text to come from a textfield the newPrice constant will be equal to that textfield's text, for instance.

Why can't I initialize my NSManagedObject when passing it an argument?

I have a class (Swift 3) in my app that contains the basic CoreData stack which I moved out of AppDelegate. All of the operations the app needs to perform are in another class I call CoreDataHelper. When the app is launched, AppDelegate creates an instance of CoreDataHelper (let dataManager = CoreDataHelper()) which is the only thing that talks to the stack. Then in launchCalculatorViewController(), dataManager asks the stack for a new LiftEvent:
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
let dataManager = CoreDataHelper()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// check if data needs to be loaded and load it if needed...
launchCalculatorViewController()
return true
}
func launchCalculatorViewController() {
self.window = UIWindow(frame: UIScreen.main.bounds)
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
if let initialViewController: CalculatorViewController = mainStoryboard.instantiateInitialViewController() as? CalculatorViewController {
// create the liftEvent, the viewModel, and wire it up to the view controller
let liftEvent = dataManager.createNewLiftEvent() // go get me a new LiftEvent
let viewModel = calculatorLiftEventViewModelFromLiftEvent(withLiftEvent: liftEvent, dataManager: dataManager)
initialViewController.viewModel = viewModel
initialViewController.dataManager = dataManager
self.window?.rootViewController = initialViewController
self.window?.makeKeyAndVisible()
}
}
here's the createNewLiftEvent() method in dataManager:
func createNewLiftEvent() -> LiftEvent {
let newLiftEvent = LiftEvent(dataManager: self, insertIntoManagedObjectContext: stack.managedObjectContext)
return newLiftEvent
}
and the initialization of LiftEvent looks like this:
class LiftEvent: NSManagedObject, LiftEventProtocol {
var dataManager: CoreDataHelper!
override func awakeFromInsert() {
super.awakeFromInsert()
let date = Date()
self.date = date
let defaultUnit = UserDefaults.weightUnit()
if defaultUnit == "kg" {
weight = Measurement(value: 0.0, unit: .kilograms)
liftWeights[defaultUnit] = weight
} else {
weight = Measurement(value: 0.0, unit: .pounds)
liftWeights[defaultUnit] = weight
}
if let defaultFormula = dataManager.fetchDefaultFormula() { // unexpected nil error thrown here
self.formula = defaultFormula // this is an NSManagedObject
}
}
convenience init(dataManager: CoreDataHelper, insertIntoManagedObjectContext context: NSManagedObjectContext!) {
let entity = NSEntityDescription.entity(forEntityName: "LiftEvent", in: context)!
self.init(entity: entity, insertInto: context) // exits after this line
self.dataManager = dataManager // never gets executed so of course it's nil
}
}
I get the "unexpectedly found nil while unwrapping an Option value" error because dataManager is nil at this point. I try to set it in the convenience initializer but self.dataManager = dataManager is never executed because it exits right after self.init. Of course I can't put it before self.init because the object has to exist before I can set the value.
I'm passing in dataManager in an attempt to ensure that the Formula (also an NSManagedObject) and this new LiftEvent being created are in the same managed object context so I can set the relationship between them in awakeFromInsert(). Before trying this approach it was crashing because the LiftEvent and Formula were it two different managedObjectContexts (even though I did a lot of looking to make sure that dataManager and the stack were only instantiated once). The approach above was inspired by this thread, but I think I might not understand initialization as well as I thought I did.
How do I pass in the managed object context to create a LiftEvent so it's in the same managed object context as Formula so the relationship can be set? Or, is the totally the wrong approach for this?

CoreData: prepare for segue NSManagedObject issue in Swift 3

I am a beginner student in Swift 3 and I am currently studying CoreData. I am trying to do an App where I have a first controller that is a list view (tableviewcontroller) where I can see some students. Inside each cell, I have an image (UIImage) and 4 labels (Strings: name, preceptor, note and date) who fetch data from an Array that keeps the information from the entity "AlunosLista", who has one attribute for each item (image is binary data). I can add these information through another view controller (AddDataVC.swift) and list them perfectly. The app until here is fine. What i cannot do, and i have been trying a lot of things, many things, is to send the data from the row selected (clicked) to another viewcontroller for the detailed view (DetailsVC.swift). When i was using a simple Array, without CoreData, worked fine. But now i cannot do it. Now parts of the code:
File (1): TableviewController
class TabelaListagem: UITableViewController {....
import CoreData
import UIKit
var alunos: [NSManagedObject?] = []
var gerenciadorDeDados: NSManagedObjectContext? = nil
override func viewDidLoad() {
super.viewDidLoad()
//CORE DATA
let AppleObject = UIApplication.shared.delegate as! AppDelegate
gerenciadorDeDados = AppleObject.persistentContainer.viewContext
LoadFetch()
}
(.......)
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let path = alunos[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "celulaReuso", for: indexPath) as! ListagemCelulas
cell.lblAluno.text = path?.value(forKey: "nome") as? String
cell.lblPreceptor.text = path?.value(forKey: "preceptor") as? String
cell.lblData.text = path?.value(forKey: "dataHoje") as? String
cell.lblNotaAluno.text = path?.value(forKey: "nota") as? String
let caminhodaImagem = path?.value(forKey: "fotoAluno")
cell.imgAluno.image = UIImage(data: (caminhodaImagem as? NSData) as! Data)
return cell
}
Here should place the prepare(for segue), that I have tried many ways. This was the last one, who didn't worked too. "Build succeeded", but crashed.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "segueAluno" {
if let pathC = tableView.indexPathForSelectedRow{
let VCDestino = segue.destination as? DescriAluno
let objeto = FecthResultado?.object(at: pathC)
VCDestino?.alunoD = objeto as! NSManagedObject?
}
}
}
File (2) DetailViewController
import UIKit
import CoreData
class DescriAluno: UIViewController {
#IBOutlet weak var imgFotoAluno: UIImageView!
#IBOutlet weak var nomeAluno: UILabel
#IBOutlet weak var txtPreceptor: UILabel!
#IBOutlet weak var txtNotaAluno: UILabel!
#IBOutlet weak var txtDataHoje: UILabel!
var gerenciadorDeDados: NSManagedObjectContext!
var alunoD: NSManagedObject?
override func viewDidLoad() {
super.viewDidLoad()
//CORE DATA
let AppleObject = UIApplication.shared.delegate as! AppDelegate
gerenciadorDeDados = AppleObject.persistentContainer.viewContext
imgFotoAluno.image = alunoD?.value(forKey: "fotoAluno")
nomeAluno.text = alunoD?.value(forKey: "nome")
txtPreceptor.text = alunoD?.value(forKey: "preceptor")
txtNotaAluno.text = alunoD?.value(forKey: "nota")
txtDataHoje.text = alunoD?.value(forKey: "dataHoje")
}
Error message after crash:
Terminating app due to uncaught exception
'NSInvalidArgumentException', reason: 'executeFetchRequest:error:
is not a valid NSFetchRequest.'
I really don't know how to proceed. I have tried so many things, some of them told about NSFetchedResults, but i could write or understand them. If any one could help here, I appreciate. Thank you.
This is the Fetch request (func):
Ok. This is my fetch request:
func LoadFecth() {
let ordenacaoAZ = NSSortDescriptor(key: "nome", ascending: true)
let ordenacaoAZPrecep = NSSortDescriptor(key: "preceptor", ascending: true)
let recupoerardados = NSFetchRequest<NSFetchRequestResult>(entityName: "AlunosLista")
recupoerardados.sortDescriptors = [ordenacaoAZPrecep, ordenacaoAZ]
do{
let recupera = try gerenciadorDeDados?.fetch(recupoerardados)
self.alunos = recupera as! [NSManagedObject]
self.tableView.reloadData()
}catch let erro as NSError{
print("Erro ao carregar: \(erro.description)")
}
}
Your core data life will get easier in every way if you use NSManagedObject subclasses for your entities. This allows you to use convenience accessors to get typed fetch requests and to skip all this value(forKey: stuff and use properties.
The error is that your fetch request is not valid.
You may have mistyped the entity name ("AlunosLista").
It may be getting upset because the type of the fetch request, without a specific NSManagedObject subclass, is actually NSFetchRequest<NSManagedObject>.
The best thing to do is enter a subclass for the entity in the model editor, generate the subclass files, then use this:
let recupoerardados: NSFetchRequest<AlunosLista> = AlunosLista.fetchRequest()

String Returning Multiple values in Swift

I have a CoreData entity named 'Studio' with an attribute named 'name' with an NSManagedObject subclass created.
My app designed for a simple process, enter a name into a text box, and press 'save' and the name is saved into Studio.name - Press 'Update' and a text label is refreshed to show the value of Studio.name
However, it is not functioning as expected, if I, for example, enter the name 'Stack' and save the update I see 'Stack' in the text label, if i then enter 'Overflow' save/update the label reads 'Overflow', If i update it a third time to 'Swift' save/update the label again reads 'Stack'.
From there updates will give one of the three values seemingly at random.
Force quitting the app and relaunching it shows that the data is being saved to Core Data as pressing the update button will return a random previous value.
My question is, how does this happen with a string? (Shouldn't it only hold one value at a time?)
How can I correct this so it will only hold a single value and any subsequent values simply overwrite the previous value?
My code follows.
import UIKit
import CoreData
class ViewController: UIViewController {
#IBOutlet weak var studioBox: UITextField!
#IBOutlet weak var nameLabel: UILabel!
#IBAction func saveData(sender: AnyObject) {
var studio = writeStudioData()
studio.name = studioBox.text
}
#IBAction func Update(sender: AnyObject) {
var studio = getStudioData()
nameLabel.text = studio.name
}
func getStudioData() -> Studio {
let appDelegate = UIApplication.sharedApplication().delegate as AppDelegate
let managedContext = appDelegate.managedObjectContext!
let request = NSFetchRequest(entityName: "Studio")
request.returnsObjectsAsFaults = false
let result = managedContext.executeFetchRequest(request, error: nil) as [Studio]
return result[0]
}
func writeStudioData () -> Studio {
let appDelegate =
UIApplication.sharedApplication().delegate as AppDelegate
let managedContext = appDelegate.managedObjectContext!
let entityDescription = NSEntityDescription.entityForName("Studio", inManagedObjectContext: managedContext)
let result = Studio(entity: entityDescription!, insertIntoManagedObjectContext: managedContext)
return result
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
You don't have one object, you are creating a new object every time. As you aren't including a sort descriptor with your fetch request, the order you get them, and thus the corresponding name, is unspecified, meaning it could be any of them.
You could either perform a fetch first in writeStudioData to see if there's already an object, only creating one if there isn't, or you could create one object and keep it around in a property.

Return value from CoreData with entity.attribute?

I am new to swift and have been having trouble working with CoreData, I'm looking for a way to simplify fetching and writing to values, specifically being able to write them with something similar to:
entity.attribute = variable
and retrieve them with something similar to:
variable = entity.attribute
I have an entity called 'Studio' with an attribute called 'name' (string).
I've created an NSManagedObject Subclass (Studio.swift) and have updated the class to 'testapp.Studio'.
In my example code below the IBAction saveData will write studioBox.text into CoreData, however the IBAction update is throwing some errors.
var Studio = getStudioData()
Pattern Variable Binding Cannot Appear in an Expression
nameLabel.text = Studio.name
'Studio.Type' does not have a member named 'name'
and finally in the getStudioData function:
return studio
'NSArray' is not a subtype of 'Studio'
Below is my full code:
import UIKit
import CoreData
class ViewController: UIViewController {
#IBOutlet weak var studioBox: UITextField!
#IBOutlet weak var nameLabel: UILabel!
#IBAction func saveData(sender: AnyObject) {
var info: String = studioBox.text!
var Studio = dbconnect()
Studio.name = info as String
}
#IBAction func Update(sender: AnyObject) {
var info: String =
var Studio = getStudioData()
nameLabel.text = Studio.name
}
func getStudioData() -> Studio {
let appDelegate =
UIApplication.sharedApplication().delegate as AppDelegate
let managedContext = appDelegate.managedObjectContext!
let request = NSFetchRequest(entityName: "Studio")
request.returnsObjectsAsFaults = false
let result = managedContext.executeFetchRequest(request, error: nil)!
let studio = result
return studio
}
func dbconnect () -> Studio {
let appDelegate =
UIApplication.sharedApplication().delegate as AppDelegate
let managedContext = appDelegate.managedObjectContext!
let entityDescription = NSEntityDescription.entityForName("Studio", inManagedObjectContext: managedContext)
let studio = Studio(entity: entityDescription!, insertIntoManagedObjectContext: managedContext)
return studio
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
First, the fetch request returns an array of objects. So if you want to return one Studio you should return one object of this array (e.g. the first one). Don't forget to cast your fetch result as the right type array.
let result = managedContext.executeFetchRequest(request, error: nil) as [Studio]
// check if result.count > 0
return result[0]
This explains the first and third errors. The second one seems to be caused by the fact that with the capitalized Studio you are referring to the class, not an instance, i.e. a managed object. While it is "legal" to use variable names with capitalized first letters, it is bad practice because it makes your code less readable. Stick with names like Studio for class names, and studio for variable names.
nameLabel.text = studio.name