Swift - Unexpected rows added to CoreData - ios

I have a CoreData base with 6 rows in it.
I na ViewController, the data is displayed in a UITable, when I select a row in the table, the didSelectRow
lists 6 rows. That are all the rows.
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
caches = CoreData.getCaches()
print ("Amount \(caches.count)") // gives 6
performSegue(withIdentifier: "Select", sender: nil)
}
When the Segue is executed the prepareForSegue is executed. Now the same command results with the value 7.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
caches = CoreData.getCaches()
print ("Amount \(caches.count)") // gives 7
}
I suspect that something in the background is happening, but i can't find out what.
Below is the static method for reference:
static func getCaches() -> [Caches] {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
var resultArray: [Caches] = []
let request = NSFetchRequest<Caches>(entityName: "Caches")
request.returnsObjectsAsFaults = false
let sortDescriptor = NSSortDescriptor(key: "name", ascending: true)
let sortDescriptors = [sortDescriptor]
request.sortDescriptors = sortDescriptors
do {
resultArray = try context.fetch(request)
} catch {
print("Error - \(error)")
}
return resultArray
}

After a lot of searching I found it.
I execute a performSegueWithIdentifier. Which calls the prepareForSegue in the calling ViewController. But apparently before that, the variables/properties from the called VC are created. (Which is logical if you give it some thought)
In the called VC, a variable was initialised with the following code
(copied from somewhere on the net)
var cache = Caches((context: (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext))
This line of ode was causing the trouble. Cause it creates an entity in the persistentContainer (not written to the actual CoreData). I replaced it with a plain old:
var cache = Caches()
And everything is working okay now. Thanks for the support.

Related

How can I transfer multiple rows of a tableView to another ViewController

I'm trying to add a feature in my app, to add multiple members to one action at one time. The members are listed in a tableView and the user can select multiple rows at one time with the .allowsMultipleSelection = true function. I got the following code but this doesn't work. I think my idea would work but not in the way I have it in the code :
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let destination = segue.destination as? AddMultipleMemberTransactionViewController,
let selectedRows = multipleMemberTableView.indexPathsForSelectedRows else {
return
}
destination.members = members[selectedRows]
}
Does somebody out here know, how I can solve this problem, because there is an error :
Cannot subscript a value of type '[Member?]' with an index of type '[IndexPath]'
I have the same feature in the app but just for one member. There I in the let selectedRows line after the indexPathForSelectedRow a .row. Is there a similar function for indexPathsForSelectedRows ?
Or is this the wrong way to do it?
You need
destination.members = selectedRows.map{ members[$0.row] }
As the indexPathsForSelectedRows indicates, it returns an array of IndexPath. What you need to do is create an array of Member objects based on those path.
Assuming you have a "members" array that contain all the members the user can select from, and your table has only 1 section:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
var selectedMembers: [Member] = []
guard let destination = segue.destination as? AddMultipleMemberTransactionViewController,
let selectedIndexes = multipleMemberTableView.indexPathsForSelectedRows else {
return
}
for selectedIndex in selectedIndexes {
let selectedMember = members[selectedIndex.row]
selectedMembers.append(selectedMember)
}
destination.members = selectedMembers
}
You can also use the array map() function to change the for loop into a single line operation:
let selectedMembers: [Member] = selectedRows.map{ members[$0.row] }
Both should effectively do the same.

Swift iOS -How To Reload TableView Outside Of Firebase Observer .childAdded to Filter Out Duplicate Values?

I have a tabBar controller with 2 tabs: tabA which contains ClassA and tabB which contains ClassB. I send data to Firebase Database in tabA/ClassA and I observe the Database in tabB/ClassB where I retrieve it and add it to a tableView. Inside the tableView's cell I show the number of sneakers that are currently inside the database.
I know the difference between .observeSingleEvent( .value) vs .observe( .childAdded). I need live updates because while the data is getting sent in tabA, if I switch to tabB, I want to to see the new data get added to the tableView once tabA/ClassA is finished.
In ClassB I have my observer in viewWillAppear. I put it inside a pullDataFromFirebase() function and every time the view appears the function runs. I also have Notification observer that listens for the data to be sent in tabA/ClassA so that it will update the tableView. The notification event runs pullDataFromFirebase() again
In ClassA, inside the callback of the call to Firebase I have the Notification post to run the pullDataFromFirebase() function in ClassB.
The issue I'm running into is if I'm in tabB while the new data is updating, when it completes, the cell that displays the data has a count and the count is thrown off. I debugged it and the the sneakerModels array that holds the data is sometimes duplicating and triplicating the newly added data.
For example if I am in Class B and there are 2 pairs of sneakers in the database, the pullDataFromFirebase() func will run, and the tableView cell will show "You have 2 pairs of sneakers"
What was happening was if I switched to tabA/ClassA, then added 1 pair of sneakers, while it's updating I switched to tabB/ClassB, the cell would still say "You have 2 pairs of sneakers" but then once it updated the cell would say "You have 5 pairs of sneakers" and 5 cells would appear? If I switched tabs and came back it would correctly show "You have 3 pairs of sneakers" and the correct amount of cells.
That's where the Notification came in. Once I added that if I went through the same process and started with 2 sneakers the cell would say "You have 2 pairs of sneakers", I go to tabA, add another pair, switch back to tabB and still see "You have 2 pairs of sneakers". Once the data was sent the cell would briefly show "You have 5 pairs of sneakers" and show 5 cells, then it would correctly update to "You have 3 pairs of sneakers" and the correct amount of cells (I didn't have to switch tabs).
The Notification seemed to work but there was that brief incorrect moment.
I did some research and the most I could find were some posts that said I need to use a semaphore but apparently from several ppl who left comments below they said semaphores aren't meant to be used asynchronously. I had to update my question to exclude the semaphore reference.
Right now I'm running tableView.reloadData() in the completion handler of pullDataFromFirebase().
How do I reload the tableView outside of the observer once it's finished to prevent the duplicate values?
Model:
class SneakerModel{
var sneakerName:String?
}
tabB/ClassB:
ClassB: UIViewController, UITableViewDataSource, UITableViewDelegate{
var sneakerModels[SneakerModel]
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(pullDataFromFirebase), name: NSNotification.Name(rawValue: "pullFirebaseData"), object: nil)
}
override func viewWillAppear(_ animated: Bool){
super.viewWillAppear(animated)
pullDataFromFirebase()
}
func pullDataFromFirebase(){
sneakerRef?.observe( .childAdded, with: {
(snapshot) in
if let dict = snapshot.value as? [String:Any]{
let sneakerName = dict["sneakerName"] as? String
let sneakerModel = SneakerModel()
sneakerModel.sneakerName = sneakerName
self.sneakerModels.append(sneakerModel)
//firebase runs on main queue
self.tableView.reloadData()
}
})
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return sneakerModels.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SneakerCell", for: indexPath) as! SneakerCell
let name = sneakerModels[indePath.row]
//I do something else with the sneakerName and how pairs of each I have
cell.sneakerCount = "You have \(sneakerModels.count) pairs of sneakers"
return cell
}
}
}
tabA/ClassA:
ClassA : UIViewController{
#IBAction fileprivate func postTapped(_ sender: UIButton) {
dict = [String:Any]()
dict.updateValue("Adidas", forKey: "sneakerName")
sneakerRef.?.updateChildValues(dict, withCompletionBlock: {
(error, ref) in
//1. show alert everything was successful
//2. post notification to ClassB to update tableView
NotificationCenter.default.post(name: Notification.Name(rawValue: "pullFirebaseData"), object: nil)
}
}
}
In other parts of my app I use a filterDuplicates method that I added as an extension to an Array to filter out duplicate elements. I got it from filter array duplicates:
extension Array {
func filterDuplicates(_ includeElement: #escaping (_ lhs:Element, _ rhs:Element) -> Bool) -> [Element]{
var results = [Element]()
forEach { (element) in
let existingElements = results.filter {
return includeElement(element, $0)
}
if existingElements.count == 0 {
results.append(element)
}
}
return results
}
}
I couldn't find anything particular on SO to my situation so I used the filterDuplicates method which was very convenient.
In my original code I have a date property that I should've added to the question. Any way I'm adding it here and that date property is what I need to use inside the filterDuplicates method to solve my problem:
Model:
class SneakerModel{
var sneakerName:String?
var dateInSecs: NSNumber?
}
Inside tabA/ClassA there is no need to use the Notification inside the Firebase callback however add the dateInSecs to the dict.
tabA/ClassA:
ClassA : UIViewController{
#IBAction fileprivate func postTapped(_ sender: UIButton) {
//you must add this or whichever date formatter your using
let dateInSecs:NSNumber? = Date().timeIntervalSince1970 as NSNumber?
dict = [String:Any]()
dict.updateValue("Adidas", forKey: "sneakerName")
dict.updateValue(dateInSecs!, forKey: "dateInSecs")//you must add this
sneakerRef.?.updateChildValues(dict, withCompletionBlock: {
(error, ref) in
// 1. show alert everything was successful
// 2. no need to use the Notification so I removed it
}
}
}
And in tabB/ClassB inside the completion handler of the Firebase observer in the pullDataFromFirebase() function I used the filterDuplicates method to filter out the duplicate elements that were showing up.
tabB/ClassB:
func pullDataFromFirebase(){
sneakerRef?.observe( .childAdded, with: {
(snapshot) in
if let dict = snapshot.value as? [String:Any]{
let sneakerName = dict["sneakerName"] as? String
let sneakerModel = SneakerModel()
sneakerModel.sneakerName = sneakerName
self.sneakerModels.append(sneakerModel)
// use the filterDuplicates method here
self.sneakerModels = self.sneakerModels.filterDuplicates{$0.dateInSecs == $1.dateInSecs}
self.tableView.reloadData()
}
})
}
Basically the filterDuplicates method loops through the sneakerModels array comparing each element to the dateInSecs and when it finds them it excludes the copies. I then reinitialize the sneakerModels with the results and everything is well.
Also take note that there isn't any need for the Notification observer inside ClassB's viewDidLoad so I removed it.

How do you load attribute data from the entity into a single (1) UIViewController?

I'm new to coding with Swift, and I've been using the tutorial from Udemy by Aaron Caines, which has been great. The tutorial he did was using a UITableViewController.
I have an app that uses a single UIViewController (ONLY ONE VIEW and it's not a UITableViewController). I've already loaded CoreData into the build phases. I've been able to verify that the data is saved in the attributes, but for some reason, I can't load the data back into the two text boxes and one image view that I have in the view controller.
I've placed a couple of questions as comments within the code.
It should be as easy as setting up the variables:
#IBOutlet var textName: UITextField!
#IBOutlet var descriptionName: UITextField!
#IBOutlet var imageView: UIImageView!
calling the entity and getting the persistent container ready to load and receive data:
let pc = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
fetching the data
var frc : NSFetchedResultsController = NSFetchedResultsController<NSFetchRequestResult>()
func fetchRequest() -> NSFetchRequest<NSFetchRequestResult> {
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "NamedEntity")
// DO I NEED A SORTER IF I'M NOT USING A TABLEVIEW?
//let sorter = NSSortDescriptor(key: "accounttext", ascending: false)
//fetchRequest.sortDescriptors = [sorter]
return fetchRequest
}
func getFRC() -> NSFetchedResultsController<NSFetchRequestResult> {
frc = NSFetchedResultsController(fetchRequest: fetchRequest(), managedObjectContext: pc, sectionNameKeyPath: nil, cacheName: nil)
//OCCASIONALLY THERE'S AN ISSUE WITH THE sectionNameKeyPath.
//THE ERROR INVOLVES TRYING TO "UNWRAP A NIL VALUE".
//IS THERE ANOTHER VALUE I SHOULD BE CONSIDERING?
return frc
}
fetching the data whenever the view loads or appears:
override func viewDidLoad() {
super.viewDidLoad()
frc = getFRC()
frc.delegate = self
do {
try frc.performFetch()
}
catch {
print(error)
return
}
// WHAT DO I USE HERE IF I'M NOT USING A TABLEVIEW?
self.tableView.reloadData()
}
override func viewDidAppear(_ animated: Bool) {
frc = getFRC()
frc.delegate = self
do {
try frc.performFetch()
}
catch {
print(error)
return
}
// WHAT DO I USE HERE IF I'M NOT USING A TABLEVIEW?
self.tableView.reloadData()
}
and then loading it into the appropriate boxes:
//THIS IS WHERE THINGS GET STUCK
// HOW DO I CALL THE ATTRIBUTES OF MY ENTITY AND UPDATE MY VARIABLES IF I'M NOT USING A TABLEVIEW?
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! cellAccountTableViewCell
let item = frc.object(at: indexPath) as! Entity
cell.nameText.text = item.accounttext
cell.descriptionText.text = item.amounttext
cell.imageView.image = UIImage(data: (item.image)! as Data)
return cell
}
I only have 2 text boxes and 1 image view. I've spent the last three days scouring dozens of useless forum topics and countless youtube videos for this answer, but it seems that everyone gives a tutorial on using a table view controller.
The most useful thing I've found was a video by Electronic Armory. This helped me understand the structure of the Entity(ies), attrubutes, and the persistentContainer. It also deals with the relational aspect the database.
https://www.youtube.com/watch?v=da6W7wDh0Dw
Can I use core data on ONE (1) single UIViewController, and if so, how do I call the data and load it into the appropriate fields? Let me know if there's any more info needed.
I'm really trying to understand the Core Data process. What am I missing, or what am I not understanding about the loading process? Any help would be appreciated!
Thanks,
Luke
I have an app that uses a single UIViewController (ONLY ONE VIEW and it's not a UITableViewController).
I only have 2 text boxes and 1 image view.
Assuming there's only one entity returned from the fetch (otherwise how are you going to show them all?), probably something like this:
// WHAT DO I USE HERE IF I'M NOT USING A TABLEVIEW?
//self.tableView.reloadData()
if let fetchResult = frc.fetchedObjects{
if let item = fetchResult.first as? Entity{
textName.text = item.accounttext
descriptionName.text = item.amounttext
imageView.image = UIImage(data: (item.image)! as Data)
}
}

Swift: TableViewController not woking with CoreData (NSRangeException when Loading table)

So I was trying out TableViewController and CoreData by making a relatively simple app. All my app does is tell me my credit card balance by adding my transactions up. Also, I'm doing this using two currencies, USD and EGP.
I stored the data permanently before using NSUserDefaults, and it worked. But then I realized that I needed to change to CoreData if I want to make a custom class that stores a lot of details about a transaction (Title, Amount, isEGP...) Also I'm planning on adding date.
Anyway. So I decided to try out CoreData using this RayWenderlich tutorial. I finished everything and when I ran it, it added one element correctly! But when I add another, it crashes with NSRangeException: "1 is out of Range for Bounds[0...0]" I'm modeling my data using a static array of type [NSManagedObject] for all the transactions. The problem seems to have to do with the fact that I'm using a TableViewController not a TableView (i'm guessing) since in the class file here:
class TransactionsTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print (LockScreenViewController.transactions.count)
return LockScreenViewController.transactions.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = UITableViewCell (style: UITableViewCellStyle.Default, reuseIdentifier: "Cell");
let transaction = LockScreenViewController.transactions[indexPath.row]
let title = transaction.valueForKey("title") as! String
let amount = transaction.valueForKey("amount") as! Double
let isEGP = transaction.valueForKey("isEGP") as! Bool
cell.textLabel!.text = isEGP ?
(title + ": " + String(amount) + " EGP") :
(title + ": $" + String(amount))
return cell;
}
}
it prints out the count once, and then crashes...instead of 3 or 4 times when there is only 1 element (or no elements). I have no idea how to fix it.
When a button is pressed, I call this function to save the transaction to my array:
func saveTransaction(title: String, amount: Double, isEGP: Bool) {
//1 Managed Context
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let managedContext = appDelegate.managedObjectContext
//2
let entity = NSEntityDescription.entityForName("Transaction", inManagedObjectContext: managedContext)
let transaction = NSManagedObject(entity: entity!, insertIntoManagedObjectContext: managedContext)
//3
transaction.setValue(title, forKey: "title");
transaction.setValue(amount, forKey: "amount");
transaction.setValue(isEGP, forKey: "isEGP");
//4
do {
try managedContext.save()
//5
LockScreenViewController.transactions.append(transaction)
} catch let error as NSError {
print("Could not save \(error), \(error.userInfo)")
}
}
The rest of the code should be fine, but if none of the past code seems to have anything wrong with it, the problem may be because I added a CoreData file after I created the project, instead of checking the "Use Core Data" Button in the beginning. (I fixed the AppDelegate manually, so I don't think that's the issue)
In any case, here is my project file:
http://dropcanvas.com/ogcfo
Thanks!
Core Data + UITableView = NSFetchedResultsController!
You should be using an NSFetchedResultsController. The easiest way to get started is to examine the Xcode template for "Master-Detail" where a table view with a fetched results controller is implemented in the master controller.
Note how the NSFetchedResultsControllerDelegate helps you update the table view dynamically without much effort.
This is by far the most robust solution, plus you get incredible scalability and optimizations.

How to update managed object data?

I have started my first core data application. I am working with one entity right now called 'Folder'.
The first view controller displays all the Folders in a tableview, which I can add to and it reloads the data. This works fine because It uses the fetch request to populate the table.
override func viewWillAppear(animated: Bool) {
var error: NSError?
let request = NSFetchRequest(entityName: "Folder")
request.sortDescriptors = [NSSortDescriptor(key: "date", ascending: true)]
self.events = moc?.executeFetchRequest(request, error: &error) as! [Folder]
self.UITable.reloadData()
}
However when segueing to another view controller via the table cell I pass on the selected Folder data to the controller using the index path. e.g.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
if segue.identifier == "showDetails" {
let destinationVC = segue.destinationViewController as! FolderDetailsViewController
let indexPath = UITable.indexPathForSelectedRow()
let selectedFolder = folders[indexPath!.row]
destinationVC.selectedFolder = selectedFolder
}
}
My second view controller uses the data passed from the first table view to display in textfields:
var selectedFolder: Folder!
folderNameLabel.text = selectedFolder?.title
folderDetailsLabel.text = selectedFolder?.details
folderDateLabel.text = displayDate
I then have a modal to edit/save the folder data in a modal appearing from the second controller:
//Edit and save event
let context = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
//Error
var error: NSError?
//Storing Data from fields
SelectedFolder!.title = FolderName.text
SelectedFolder!.details = FolderDetails.text
SelectedFolder!.date = FolderDate.date
context?.save(&error)
self.dismissViewControllerAnimated(true, completion: {});
When dismissing the modulate data is not updated, I have to go back to the first controller to reload the data and segue again.
I think this is because I have no NSFetchRequest (or NSFetchResultsController) to get the most recent changes.
What is the best method to reload the data of the selectedFolder when I make the changes in the modal ?
You can refresh your second view in viewWillAppera() if your modal view is presented in full screen.
override func viewWillAppear(animated: Bool) {
{
folderNameLabel.text = selectedFolder?.title
folderDetailsLabel.text = selectedFolder?.details
folderDateLabel.text = displayDate
}
It seems like you would want to call moc.refreshObject(folder, mergeChanges:true)
See the documentation here.

Resources