I try to load all data from CoreData into my TableView. But why do I load every data value 5 times?
var value: [String] = []
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let appDel: AppDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let context: NSManagedObjectContext = appDel.managedObjectContext
let request = NSFetchRequest(entityName: "Contacts")
request.returnsObjectsAsFaults = false
do {
let result: [AnyObject] = try context.executeFetchRequest(request)
for res in result {
value.append(res.valueForKey("name") as! String)
}
} catch {
fatalError("Failure to load context: \(error)")
}
return value.count
}
EDIT:
If I add the objects 'Test' and 'Test1' I get the following result:
Test
Test1
Test
Test1
Test
Test1
Test
Test1
Test
10.Test1
Thank's for any suggestion!
The numberOfRowsInSection method can be called multiple times when the tableView is loaded (and thereafter). Each time it is called, you append the values to your array - hence the duplicate values.
Put the code that builds the array into viewDidLoad instead.
You're working far too hard. NSFetchedResultsController exists precisely for the task of putting Core Data into a tableview.
Related
I have an array of struct called displayStruct
struct displayStruct{
let price : String!
let Description : String!
}
I am reading data from firebase and add it to my array of struct called myPost which is initialize below
var myPost:[displayStruct] = []
I made a function to add the data from the database to my array of struct like this
func addDataToPostArray(){
let databaseRef = Database.database().reference()
databaseRef.child("Post").queryOrderedByKey().observe(.childAdded, with: {
snapshot in
let snapshotValue = snapshot.value as? NSDictionary
let price = snapshotValue?["price"] as! String
let description = snapshotValue?["Description"] as! String
// print(description)
// print(price)
let postArr = displayStruct(price: price, Description: description)
self.myPost.append(postArr)
//if i print self.myPost.count i get the correct length
})
}
within this closure if I print myPost.count i get the correct length but outside this function if i print the length i get zero even thou i declare the array globally(I think)
I called this method inside viewDidLoad method
override func viewDidLoad() {
// setup after loading the view.
super.viewDidLoad()
addDataToPostArray()
print(myPeople.count) --> returns 0 for some reason
}
I want to use that length is my method below a fucntion of tableView
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myPost.count --> returns 0
}
Any help would be greatly appreciated!
You making a asynchronous network request inside closure and compiler doesn't wait for the response, so just Reload Table when get post data. replace the code with below it work works fine for you. All the best.
func addDataToPostArray(){
let databaseRef = Database.database().reference()
databaseRef.child("Post").queryOrderedByKey().observe(.childAdded, with: {
snapshot in
let snapshotValue = snapshot.value as? NSDictionary
let price = snapshotValue?["price"] as! String
let description = snapshotValue?["Description"] as! String
// print(description)
// print(price)
let postArr = displayStruct(price: price, Description: description)
self.myPost.append(postArr)
print(self.myPost.count)
print(self.myPost)
self.tableView.reloadData()
//if i print self.myPost.count i get the correct length
})
}
Firebase observe call to the database is asynchronous which means when you are requesting for the value it might not be available as it might be in process of fetching it.
That's why your both of the queries to count returns 0 in viewDidLoad and DataSource delegeate method.
databaseRef.child("Post").queryOrderedByKey().observe(.childAdded, with: { // inside closure }
Inside the closure, the code has been already executed and so you have the values.
What you need to do is you need to reload your Datasource in main thread inside the closure.
databaseRef.child("Post").queryOrderedByKey().observe(.childAdded, with: {
// After adding to array
DispatchQueue.main.asyc {
self.tableView.reloadData()
}
}
I'm trying to read the value of an attribute (called 'name') of an entity (called 'List'), stored in a Core Data database. The issue that I have is that it says that the value of that attribute is nil, which should be a String.
My code for retrieving all List-entities is as follows:
container?.performBackgroundTask { [weak self] context in
self?.wordLists = try! List.all(in: context)
DispatchQueue.main.async(execute: {
print("Main queue available, reloading tableview.")
self?.wordListSelector.reloadData()
})
}
class func all(in context: NSManagedObjectContext) throws -> [List] {
let listRequest: NSFetchRequest<List> = List.fetchRequest()
do {
let list = try context.fetch(listRequest)
print(list)
return list
} catch {
print("error")
throw error
}
}
This prints:
[<__Words.List: 0x6000000937e0> (entity: List; id: 0xd00000000004000c <x-coredata://999D0158-64BD-44FD-A0B1-AB4EC03B9386/List/p1> ; data: <fault>), <__Words.List: 0x600000093a60> (entity: List; id: 0xd00000000008000c <x-coredata://999D0158-64BD-44FD-A0B1-AB4EC03B9386/List/p2> ; data: <fault>), <__Words.List: 0x600000093ab0> (entity: List; id: 0xd0000000000c000c <x-coredata://999D0158-64BD-44FD-A0B1-AB4EC03B9386/List/p3> ; data: <fault>)]
Which shows me that there should be 3 lists in the database, which is as expected.
I have created a variable like this:
var wordLists: [List] = [] {
didSet {
print("Detected wordList update, waiting for main queue.")
DispatchQueue.main.async(execute: {
print("Main queue available, reloading tableview.")
self.wordListSelector.reloadData()
})
}
}
This variable holds the List-entities that I retrieved by calling that all() function which I mentioned previously.
The following two methods will fill my TableView:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print("numberOfRowsInSection: \(wordLists.count).")
return wordLists.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "CategoryCell"
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath)
print("cellForRowAt: \(indexPath.row) has name \(wordLists[indexPath.row].name).")
cell.textLabel?.text = wordLists[indexPath.row].name
return cell
}
This prints the following:
Detected wordList update, waiting for main queue.
Main queue available, reloading tableview.
numberOfRowsInSection: 3.
cellForRowAt: 0 has name nil.
cellForRowAt: 1 has name nil.
cellForRowAt: 2 has name nil.
Why is the name nil? Is this because the data is still 'faulted'? By viewing topics online I thought that Core Data automaticly unfaulted its data when you try to access it. What am I doing wrong?
Edit:
If I change the didset to the following:
var wordLists: [List] = [] {
didSet {
print("Wordlist was updated.")
for wordList in wordLists {
print(wordList)
print(wordList.name)
}
}
}
It does print the names (Optional("nameofitem1")). In the cellForRowAt it still prints 'nil'.
What you do looks like a good job for the NSFetchedResultsController. That will also help with the threading you might mix up as suggested in the comments. There is really good documentation on the NSFetchedResultsController
Below I have my existing query download and cell for table row code...
publicDB.perform(query, inZoneWith: nil)
{
(results, error) -> Void in
if (error != nil)
{
self.present(alert, animated: true, completion: nil)
}
else
{
for result in results!
{
self.restaurantArray.append(result)
}
OperationQueue.main.addOperation( { () -> Void in
self.tableView.reloadData()
}) } }}
downloadRestaurants()
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "restaurantcell") as? RestaurantTableCell
let restaurant: CKRecord = restaurantArray[indexPath.row]
cell?.name?.text = restaurant.value(forKey: "Name") as? String
let asset = restaurant.value(forKey: "Picture") as! CKAsset
let data = try! Data(contentsOf: asset.fileURL)
_ = UIImage(data: data)
cell?.picture?.image = UIImage(data: data)
return cell!
}
When I run this code, the app remains functional but scrolling through the 10 or so table cells is incredibly choppy. I am unsure what is causing this - all records, each containing an image, are downloaded during the query download portion of the top function. However, a problem or concept I'm missing is ever present during runtime. What am I missing here? Lazy loading? cache? something else? Unsure at this point, so any help would be incredibly helpful.
Update 1:
I've updated my code with a large thank you going to Pierce. I've had to update my code ever so slightly from his answer to maintain a ckrecord array to segue over to another controller via - restaurantArray but also create a new array for the NSObject class - tablerestaurantarray to be displayed in the current table controller.
var restaurantArray: Array<CKRecord> = []
var tablerestaurantarray: [Restaurant] = []
for result in results!
{
let tablerestaurant = Restaurant()
if let name = result.value(forKey: "Name") as! String? {
tablerestaurant.name = name
}
// Do same for image
if let imageAsset = result.object(forKey: "Picture") as! CKAsset? {
if let data = try? Data(contentsOf: imageAsset.fileURL) {
tablerestaurant.image = UIImage(data: data)
}
}
self.tablerestaurantarray.append(tablerestaurant)
self.restaurantArray.append(result)
}
OperationQueue.main.addOperation( { () -> Void in
self.tableView.reloadData()
})
}
}
}
downloadRestaurants()
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return restaurantArray.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "restaurantcell") as? RestaurantTableCell
let restaurant: Restaurant = tablerestaurantarray[indexPath.row]
cell?.name?.text = restaurant.name
cell?.picture?.image = restaurant.image
return cell!
}
The way your code is setup, whenever you scroll in your UITableView, your program is converting a CKAsset into Data, and then converting that into a UIImage, and that's within every cell! That's a rather inefficient process, so try creating an NSObject called something like Restaurant that has an image property, and when you go through all the records returned from your CKQuery, parse each record into a new Restaurant object. To create a new NSObject, go to File -> New -> File -> select 'Swift File' and add something like this:
import UIKit
class Restaurant: NSObject {
// Create a UIImage property
var image: UIImage?
// Add any other properties, i.e. name, address, etc.
var name: String = ""
}
Now for your query:
// Create an empty array of Restaurant objects
var restaurantArray: [Restaurant] = []
publicDB.perform(query, inZoneWith: nil) { (results, error) -> Void in
if (error != nil) {
self.present(alert, animated: true, completion: nil)
} else {
for result in results! {
// Create a new instance of Restaurant
let restaurant = Restaurant()
// Use optional binding to check if value exists
if let name = result.value(forKey: "Name") as! String? {
restaurant.name = name
}
// Do same for image
if let imageAsset = result.object(forKey: "Picture") as! CKAsset? {
if let data = try? Data(contentsOf: imageAsset.fileURL) {
restaurant.image = UIImage(data: data)
}
}
// Append the new Restaurant to the Restaurants array (which is now an array of Restaurant objects, NOT CKRecords)
self.restaurantArray.append(restaurant)
}
OperationQueue.main.addOperation( { () -> Void in
self.tableView.reloadData()
})
}
}
Now your cell setup is much simpler:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "restaurantcell") as? RestaurantTableCell
let restaurant: Restaurant = restaurantArray[indexPath.row]
cell?.name?.text = restaurant.name
cell?.picture?.image = restaurant.image
return cell!
}
You should use CKQueryOperation in order to implements pagination for your UITableView.
You have to set the resultLimit property to a number equals to the cell quantity visiable at one time on you table plus 3 or 4
Set recordFetchedBlock property where you have to implement the code that will apply to one CKRecord
Set queryCompletionBlock property. This is the most important part on your pagination code because this closure receive an Optional CKQueryCursor parameter.
If this CKQueryCursor is nil then you have reach the last record available for you query but if it's a non nil value, then you have more records to fetch using this CKQueryCursor as indicator to your next fetch.
When user scroll on your TableView and reach the last element you should perform another fetch with CKQueryCursor.
Other performance advice is CKAssets should be treated on separated execution queues.
I am working on a core data application and currently I have the methods setup correctly to save the primary object saves the name of the users deck but it doesn't save recall the secondary object even though the method used to save both is identical. The primary does save second though and I am wondering if it matters the order that objects are saved. I know it is a relational but I figured it wouldn't matter if the secondary was called to save prior to the primary. I am still new to core data so just a simple answer is enough. If I need to save the primary entity object first then I will build the app in such a way that such occurs, else I may have to relook at the code to figure out why it isn't recalling.
This is the code that is supposed to save prior to the name being saved in a relational manner:
#IBAction func buttonWarrior(sender: AnyObject) {
let entity = NSEntityDescription.entityForName("ClassSelection", inManagedObjectContext: classMOC!)
let newObject = ClassSelection(entity: entity!,insertIntoManagedObjectContext: classMOC)
newObject.classname = "Warrior"
var error: NSError?
classMOC?.save(&error)
if let err = error {
println(err)
} else {
self.performSegueWithIdentifier("popOver", sender: self)
}
}
This is the code used to store the primary object which is a different viewcontroller.swift file than the other one. This is presented as a popover box over the secondary object. This part works fine and recalls correctly :
#IBAction func enterButton(sender: AnyObject) {
let entityDescription = NSEntityDescription.entityForName("Deck",inManagedObjectContext: managedObjectContext!)
let storeDeck = Deck(entity: entityDescription!,insertIntoManagedObjectContext: managedObjectContext)
storeDeck.deckname = usersDeckName.text
var error: NSError?
managedObjectContext?.save(&error)
if let err = error {
status.text = err.localizedFailureReason
} else {
usersDeckName.text = ""
status.text = "Deck Saved"
self.performSegueWithIdentifier("showCardSelection", sender: self)
}
}
The recall method I am trying to use may not make sense in it's current iteration as I have been trying many different methods :
#IBOutlet weak var decksListed: UITableView!
let managedObjectContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
var savedDecksClass = [ClassSelection]()
var frc: NSFetchedResultsController = NSFetchedResultsController()
var frcClasses: NSFetchedResultsController = NSFetchedResultsController()
func getFetchedResultsController() -> NSFetchedResultsController {
frc = NSFetchedResultsController(fetchRequest: listFetchRequest(), managedObjectContext: managedObjectContext!, sectionNameKeyPath: nil, cacheName: nil)
return frc
}
func getClassesFetchedResultsController() -> NSFetchedResultsController {
frcClasses = NSFetchedResultsController(fetchRequest: classFetchRequest(), managedObjectContext: managedObjectContext!, sectionNameKeyPath: nil, cacheName: nil)
return frcClasses
}
func listFetchRequest() -> NSFetchRequest {
let fetchRequest = NSFetchRequest(entityName: "Deck")
let sortDescriptor = NSSortDescriptor(key: "deckname", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
return fetchRequest
}
func classFetchRequest() -> NSFetchRequest {
let fetchRequestClasses = NSFetchRequest(entityName: "Deck")
let classSortDescriptor = NSSortDescriptor(key: "classname", ascending: true)
fetchRequestClasses.sortDescriptors = [classSortDescriptor]
return fetchRequestClasses
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let numberofRowsInSection = frc.sections?[section].numberOfObjects
return numberofRowsInSection!
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("usersDeck", forIndexPath: indexPath) as! UITableViewCell
let listed = frc.objectAtIndexPath(indexPath) as! Deck
cell.textLabel?.text = listed.deckname
let listedClass = frcClasses.objectAtIndexPath(indexPath) as! ClassSelection
cell.detailTextLabel!.text = listedClass.classname
return cell
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
decksListed.reloadData()
}
override func viewDidLoad() {
super.viewDidLoad()
frcClasses = getClassesFetchedResultsController()
frcClasses.delegate = self
frc.performFetch(nil)
frc = getFetchedResultsController()
frc.delegate = self
frc.performFetch(nil)
}
I hope this is enough to give you an idea. I checked the relationships out and they all seem to be correct in the model. I apologize in advanced for the way some of the code looks I plan on shrinking it down after all the editing is done and working.
Thanks to pbasdf for helping me with this one. The chat he opened actually contained exactly what was needed to be done. I just wasn't saving the relationship and passing the object from one view controller to the next. After showing me exactly how to do so with an example I figured out the rest! Basically it would never have been able to recall the object as it never knew that they were related....foolish me! Thanks again!
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as UITableViewCell
var query = PFQuery(className:"category")
let object = objects[indexPath.row] as String
query.whereKey("type", equalTo:"DRUM")
query.findObjectsInBackgroundWithBlock {
(objects: [AnyObject]!, error: NSError!) -> Void in
if error == nil {
for object in objects {
NSLog("%#", object.objectId)
let abc = object["link"]
println("the web is \(abc)")
cell.textLabel!.text = "\(abc)"
}
} else {
NSLog("Error: %# %#", error, error.userInfo!)
}
}
return cell
}
after add the let object = objects[indexPath.row] as String can't load the view, delete the line show only one row successfully.
First I advise you to get your cell data outside cellForRowAtIndexPath. This function is not a good place to receive data from parse. Make another function and create a class variable and put handle getting data from there.
let object = objects[indexPath.row] as String
for object in objects
Try not to use same variable names for different stuff, as they will confuse you.
This line is not contributing to anything at the moment it seems. Try deleting it:
let object = objects[indexPath.row] as String
First lets have principles in mind. Don't ever update UI from a separate thread, its behavior is unexpected or undefined. It works or works weird.
Second, the problem you have is the when the VC gets loaded the tableView's datasource is called there and then on the main thread. Now you tried to add something on the cell by doing a Async call in separate thread which will take time and main thread is not waiting when the call to parse is being done. If you have difficulty in Async please take a look at the documentation its really important to get a good grasp of the few terms and the principles.
The thing is your main thread runs top to bottom without waiting each call to server thats async in the cell generation. So the result of that call will post later on and you are not posting on main thread too.
Moreover, i would suggest you don't do this approach for big projects or manageable code base. I generally do is:
when the view loads call the Parse with the needed information
Wait for that on a computed variable which i will observe to reload table views once I'm conformed i have the data.
Initially table view will have 0 rows and thats fine. Ill make a spinner dance during that time.
I hope i made some issues clear. Hope it helps you. Cheers!
//a computed var that is initialized to empty array of string or anything you like
//we are observing the value of datas. Observer Pattern.
var datas = [String](){
didSet{
dispatch_async(dispatch_get_main_queue(), {
//we might be called from the parse block which executes in seperate thread
tableView.reloadData()
})
}
}
func viewDidLoad(){
super.viewDidLoad()
//call the parse to fetch the data and store in the above variable
//when this succeeds then the table will be reloaded automatically
getDataFromParse()
}
//get the data: make it specific to your needs
func getDataFromParse(){
var query = PFQuery(className:"category")
//let object = objects[indexPath.row] as String //where do you use this in this block
var tempHolder = [String]()
query.whereKey("type", equalTo:"DRUM")
query.findObjectsInBackgroundWithBlock {
(objects: [AnyObject]?, error: NSError?) -> Void in
if error == nil && objects != nil {
for object in objects!{
//dont forget to cast it to PFObject
let abc = (object as! PFObject).objectForKey("link") as? String ?? "" //or as! String
println("the web is \(abc)")
tempHolder.append(abc)
}
} else {
print("error") //do some checks here
}
}
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! UITableViewCell
cell.textLabel!.text = datas[indexPath.row]
return cell
}