I would like delete a record in the Core Data when you tap on a close button in your CollectionView Cell. I made a UIButton in the Collection View Cell controller with an extension in the CollectionView Controller file.
The indexPath give a number and I made the let deleteCellNumber, but I received the error:
'Cannot convert value of type 'IndexPath.Element (aka 'Int') to expected argument type 'NSManagementObject'
extension soundboardVC: SoundboardCellDelegate {
func delete(cell: soundboardCellVC) {
if let indexPath = collectionView?.indexPath(for: cell) {
let soundRequest:NSFetchRequest<Soundboard> = Soundboard.fetchRequest()
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let deleteCellNumber = indexPath[1]
context.delete(deleteCellNumber)
(UIApplication.shared.delegate as! AppDelegate).saveContext()
do {
soundBoardData = try managedObjectContext.fetch(soundRequest)
} catch {
print("Fetching Failed")
}
}
}
}
The error is very clear:
context.delete(... expects an NSManagedObject instance but you pass an integer (by the way indexPath[1] is a pretty spooky but valid way to get the item / row value).
Assuming you have a data source array soundBoardData the usual way to delete Core Data objects in a collection or table view is
extension soundboardVC: SoundboardCellDelegate {
func delete(cell: soundboardCellVC) {
if let indexPath = collectionView?.indexPath(for: cell) {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext
let itemToDelete = soundBoardData[indexPath.item]
soundBoardData.remove(at: indexPath.item)
context.delete(itemToDelete)
collectionView!.deleteItems(at: indexPath)
appDelegate.saveContext()
}
}
}
Don't refetch the data, delete in the item in the data source array, then delete the item in the managed object context, remove the row from the collection view (with animation) and save the context.
Related
I am using Firebase to populate a TableView in my iOS app. The first few objects are loaded but once I get to the third item in my list the app crashes with the exception:
'NSRangeException', reason: '*** __boundsFail: index 3 beyond bounds [0 .. 2]'
I know that this means that I am referring to an array at an index that it does not contain however I do not know why.
I create the TableView with a TableViewController and initialize it like so:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print(posts.count)
return posts.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let post = posts[indexPath.row]
print(post)
let cell = tableView.dequeueReusableCell(withIdentifier: K.cellIdentifier, for: indexPath) as! PostCell
let firstReference = storageRef.child(post.firstImageUrl)
let secondReference = storageRef.child(post.secondImageUrl)
cell.firstTitle.setTitle(post.firstTitle, for: .normal)
cell.secondTitle.setTitle(post.secondTitle, for: .normal)
cell.firstImageView.sd_setImage(with: firstReference)
cell.secondImageView.sd_setImage(with: secondReference)
// Configure the cell...
return cell
}
I believe that the first function creates an array with the number of objects in posts and that the second function assigns values to the template for the cell. The print statement in the first method prints 4 which is the correct number of objects retrieved from firebase. I assume that means an array is created with 4 objects to be displayed in the TableView. This is what is really confusing because the error states that there are only 3 objects in the array. Am I misunderstanding how the TableView is instantiated?
Here is the code that fills the TableView:
func loadMessages(){
db.collectionGroup("userPosts")
.addSnapshotListener { (querySnapshot, error) in
self.posts = []
if let e = error{
print("An error occured trying to get documents. \(e)")
}else{
if let snapshotDocuments = querySnapshot?.documents{
for doc in snapshotDocuments{
let data = doc.data()
if let firstImage = data[K.FStore.firstImageField] as? String,
let firstTitle = data[K.FStore.firstTitleField] as? String,
let secondImage = data[K.FStore.secondImageField] as? String,
let secondTitle = data[K.FStore.secondTitleField] as? String{
let post = Post(firstImageUrl: firstImage, secondImageUrl: secondImage, firstTitle: firstTitle, secondTitle: secondTitle)
self.posts.insert(post, at: 0)
print("Posts: ")
print(self.posts.capacity)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
}
}
}
The app builds and runs and displays the first few items but crashes once I scroll to the bottom of the list. Any help is greatly appreciated.
Edit:
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.register(UINib(nibName: K.cellNibName, bundle: nil), forCellReuseIdentifier: K.cellIdentifier)
loadMessages()
}
You're getting an out-of-bounds error because you're dangerously populating the datasource. You have to remember that a table view is constantly adding and removing cells as it scrolls which makes updating its datasource a sensitive task. You reload the table on each document iteration and insert a new element in the datasource at index 0. Any scrolling during an update will throw an out-of-bounds error.
Therefore, populate a temporary datasource and hand that off to the actual datasource when it's ready (and then immediately reload the table, leaving no space in between an altered datasource and an active scroll fetching from that datasource).
private var posts = [Post]()
private let q = DispatchQueue(label: "userPosts") // serial queue
private func loadMessages() {
db.collectionGroup("userPosts").addSnapshotListener { [weak self] (snapshot, error) in
self?.q.async { // go into the background (and in serial)
guard let snapshot = snapshot else {
if let error = error {
print(error)
}
return
}
var postsTemp = [Post]() // setup temp collection
for doc in snapshot.documents {
if let firstImage = doc.get(K.FStore.firstImageField) as? String,
let firstTitle = doc.get(K.FStore.firstTitleField) as? String,
let secondImage = doc.get(K.FStore.secondImageField) as? String,
let secondTitle = doc.get(K.FStore.secondTitleField) as? String {
let post = Post(firstImageUrl: firstImage, secondImageUrl: secondImage, firstTitle: firstTitle, secondTitle: secondTitle)
postsTemp.insert(post, at: 0) // populate temp
}
}
DispatchQueue.main.async { // hop back onto the main queue
self?.posts = postsTemp // hand temp off (replace or append)
self?.tableView.reloadData() // reload
}
}
}
}
Beyond this, I would handle this in the background (Firestore returns on the main queue) and only reload the table if the datasource was modified.
After some fiddling around and implementing #bsod's response I was able to get my project running. The solution was in Main.Storyboard under the Attributes inspector I had to set the content to Dynamic Prototypes.
I have 2 UIViewControllers: ProductsVC and CartVC. In CartVC I have a function which is clearing all my products from my cart.
What I want to do is when I am in the ProductsVC and I delete a product from the CoreData then to clear all my products from the second VC which is CartVC. So I need to tell to that function to be executed there.
Here is my code:
// First VC
class ProductsViewController: UIViewController{
// Function to delete a Product from the table view and also from CoreData
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
let productEntity = Constants.productEntity
let managedContext = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let product = productsArray[indexPath.row]
// ----------- HERE I NEED TO TELL TO "clearAllProducts()" to be executed in the CartVC so when I click on the Cart button, there will be 0 products.
if editingStyle == .delete {
managedContext.delete(product)
do {
try managedContext.save()
} catch let error as NSError {
print(Constants.errorDeletingProduct + "\(error.userInfo)")
}
}
// fetch new data from DB and reload products table view
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: productEntity)
do {
productsArray = try managedContext.fetch(fetchRequest) as! [Product]
} catch let error as NSError {
print(Constants.errorFetchingData + "\(error.userInfo)")
}
productsTableView.reloadData()
}
}
// Second VC
class CartViewController: UIViewController {
// Clear all products from the cart
#IBAction func clearAllProducts(_ sender: Any) {
// Reset Cart tableView
productsInCartArray = [Product]()
productPricesArray = [Float]()
totalSum = 0
self.tabBarController?.tabBar.items?[1].badgeValue = String(0)
// Remove selected products from ProductsViewController
((self.tabBarController?.viewControllers![0] as! UINavigationController).topViewController as! ProductsViewController).selectedProductsArray = [Product]()
((self.tabBarController?.viewControllers![0] as! UINavigationController).topViewController as! ProductsViewController).priceForSelectedProductsArray = [Float]()
((self.tabBarController?.viewControllers![0] as! UINavigationController).topViewController as! ProductsViewController).counterItem = 0
((self.tabBarController?.viewControllers![0] as! UINavigationController).topViewController as! ProductsViewController).numberOfProductsInCartLabel.text = String(0)
UIApplication.shared.applicationIconBadgeNumber = 0
cartTableView.reloadData()
}
}
Here is a picture so see why I want to do that:
Thank you for your time !
You have two options:
1:
If one of your view controllers is invoked by the other one, then you can have a delegate property in the invoked VC and assign the value of the delegate to the first view controller and just normally call the delegate methods. e.g. self.delegate?.myFunc()
2:
If your view controllers are not directly related, then you may use NotificationCenter to send notifications to all of the listening views. In your case this one is probably more suitable.
I fixed my problem with a small function which is instantiate my arrays with empty values. The function is this one:
// Remove all products from the cart when one ore more products are deleted from the CoreData.
func removeAllProductsFromCart(){
selectedProductsArray = [Product]()
priceForSelectedProductsArray = [Float]()
counterItem = 0
numberOfProductsInCartLabel.text = String(0)
self.tabBarController?.tabBar.items?[1].badgeValue = String(0)
UIApplication.shared.applicationIconBadgeNumber = 0
}
I call this function here:
if editingStyle == .delete {
managedContext.delete(product)
do {
removeAllProductsFromCart()
try managedContext.save()
} catch let error as NSError {
print(Constants.errorDeletingProduct + "\(error.userInfo)")
}
}
I think the correct way to share data is to have a custom tab bar controller and your arrays should be a property of the tab bar.
That way both VC’s have access to the data and can clear it as needed using a custom method of the tab bar controller.
How can I store the data in CoreData ? I'm using this to display it in my tableView cells but how can I save and retrieve from my database ?
Here is the code:
var arrOfDict = [[String :AnyObject]]()
var dictToSaveNotest = [String :AnyObject]()
class SecondViewController: UIViewController {
#IBAction func addItem(_ sender: Any)
{
dictToSaveNotest.updateValue(notesField.text! as AnyObject, forKey: "notesField")
arrOfDict.append(dictToSaveNotest)
}
and then to view in tableViewCell, I used the code:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCellID") as! CustomCell
cell.textView.text = arrOfDict[indexPath.row]["notesField"] as! String!
}
I tried this in my addItem
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let task = Task(context: context)
task.notes=notesField.text!
//save the data to coreData
(UIApplication.shared.delegate as! AppDelegate).saveContext()
but not sure.
Yes, that would generally be how you'd save to Core Data. You create an instance of the object. Set the properties and then save. Are you receiving any errors? Or do you just want to be sure that your code works?
If you want to verify that your code works, the best thing to do would be to try to access the saved data. If you can see your saved data retrieved from your database, then your code worked :)
In order to save your input as a Task object, if you have a textField and a notesField on your screen to take input, you'd do something like this:
let task = Task(context: context)
task.title = textField.text
task.notes = notesField.text
//save the data to coreData
(UIApplication.shared.delegate as! AppDelegate).saveContext()
Currently I have the following code which saves an object however I am wanting to update/reload the tableview. The button isn't attached to a cell/row it's top right within my navigation controller (plus icon)
Note: Everything is happening within the same scene therefore any events attached to segue where I could reload table data is out of the question.
#IBAction func addWeek (sender: UIButton){
let newnumber:Int = routineWeeks.count + 1
// save data using cor data managed object context
if let managedObjectContext = (UIApplication.sharedApplication().delegate as? AppDelegate)?.managedObjectContext {
week = NSEntityDescription.insertNewObjectForEntityForName("Weeks", inManagedObjectContext: managedObjectContext) as! Weeks
week.weekNumber = newnumber
do {
try managedObjectContext.save()
} catch {
print(error)
return
}
}
//currently not reloading table with new data
tableView.reloadData()
//print information in console
print("end of save function, dismiss controller")
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellIdentifier = "Cell"
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath)
// Configure the cell...
cell.textLabel?.text = "Week \(routineWeeks[indexPath.row].weekNumber)"
return cell
}
UPDATE
Thanks Stackoverflow community for pointing me in the right direction.
routineWeeks.append(week)
print("this is a new week:\(week)")
tableView.reloadData()
I do not know if it will help you or not but I had the same problems and I added Refresher (to add "Pull To Refresh" function)
In my class :
override func viewDidLoad() {
super.viewDidLoad()
// Pull to refresh - DEBUT
tableFiches.addPullToRefreshWithAction {
NSOperationQueue().addOperationWithBlock {
//sleep(1)
NSOperationQueue.mainQueue().addOperationWithBlock {
self.loadMyData() // My func here to load data
self.tableFiches.stopPullToRefresh()
}
}
}
// Pull to refresh - FIN
}
func loadMyData(){
// request here
let recupJSON = JSON(value) // my data
if let resData = recupJSON["Data"].arrayObject {
//print("OK2")
self.ArrayFiches = resData as! [[String:AnyObject]] // Attribute json ==> ArrayFiches
}
if self.ArrayFiches.count > 0 {
self.tableFiches.reloadData()
}
}
And when I want reload my data, I use :
tableFiches.startPullToRefresh()
And it works :)
You are not updating routineWeeks array. Update it with your new data before reloading the tableView.
You seem to never add "week" to routineWeeks.
EDIT :
You should reload the datas in routineWeeks (from CoreData) right before your tableView.reloadData.
routineWeeks.append(week)
print("this is a new week:\(week)")
tableView.reloadData()
I have a one to many relationship from Set to Card for a basic Flashcard App modelled in my Core Data.
Each Set has a set name, set description, and a relationships many card1s. Each Card1 has a front, back, and photo. In my table view, I've managed to retrieve all saved Sets from core data and display them. Now I want to fetch each Set's cards when a user clicks on the appropriate cell in my next view controller.
This is my code for the table view controller:
// MARK: Properties
var finalArray = [NSManagedObject]()
override func viewDidLoad() {
getAllSets()
println(finalArray.count)
}
func getAllSets() {
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let managedContext = appDelegate.managedObjectContext!
let fetchRequest = NSFetchRequest(entityName:"Set")
var error: NSError?
let fetchedResults = managedContext.executeFetchRequest(fetchRequest,error: &error) as? [NSManagedObject]
println("Am in the getCardSets()")
if let results = fetchedResults {
finalArray = results
println(finalArray.count)
}
else {
println("Could not fetch \(error), \(error!.userInfo)")
}
}
// MARK: Displaying the data
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return finalArray.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! SetTableViewCell
let sets = finalArray[indexPath.row]
cell.setName.text = sets.valueForKey("setName")as? String
cell.setDescription.text = sets.valueForKey("setDescription")as? String
return cell
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "ShowDetail" {
let dest = segue.destinationViewController as! Display
// Get the cell that generated this segue.
if let selectedCell = sender as? SetTableViewCell {
let indexPath = tableView.indexPathForCell(selectedCell)!
let selectedSet = finalArray[indexPath.row]
dest.recievedSet = selectedSet
}
}
}
In my destination view controller, how would I go about retrieving all the cards in that the recievedSet? I've tried converting the NSSet to an array and casting it to a [Card1] array but when I attempt to display the first Card1's front String property onto the label, the app crashes, giving me the error
CoreData: error: Failed to call designated initializer on NSManagedObject class 'NSManagedObject'
fatal error: Array index out of range
This is my code for the detailed viewController.
#IBOutlet weak var front: UILabel!
var finalArray = [Card1]()
finalArray = retrievedSet.allObjects as![Card1]
front.text = finalArray[0].front
Give your detail controller a property of type CardSet (I use "CardSet" because "Set" is a Swift built-in type name). You pass the selected set to this controller.
You could have a property by which you sort, or generate an array without a particular order with allObjects.
var cardArray = [Card1]()
var cardSet: CardSet?
viewDidLoad() {
super.viewDidLoad()
if let validSet = cardSet {
cardArray = validSet.cards.allObjects as! [Card1]
}
}
Your code is not working because finalArray is of type [CardSet], so finalArray[indexPath.row] is of type CardSet which is not transformable into type NSSet. Rather the relationship to Card1s is the NSSet you are looking for.
Finally, I recommend to give the detail controller a NSFetchedResultsController, have an attribute to sort by and use the passed CardSet in the fetched results controller's predicate.